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/dist/index.d.cts CHANGED
@@ -875,7 +875,10 @@ declare class TokenBucketRateLimiter {
875
875
  */
876
876
  release(): void;
877
877
  /**
878
- * Report a successful request (resets backoff)
878
+ * Report a successful request (gradually reduces backoff)
879
+ *
880
+ * Instead of immediately clearing backoff, we gradually reduce it to prevent
881
+ * immediately hitting the rate limit again after a single success.
879
882
  */
880
883
  reportSuccess(): void;
881
884
  /**
@@ -1004,6 +1007,7 @@ declare class HealthChecker {
1004
1007
  *
1005
1008
  * @param batchSize - Number of providers to test in parallel (default: 2)
1006
1009
  * @param batchDelayMs - Delay between batches in milliseconds (default: 500 to avoid rate limits)
1010
+ * If not provided, calculates delay based on lowest RPS in batch
1007
1011
  */
1008
1012
  testProviders(providers: ResolvedProvider[], batchSize?: number, batchDelayMs?: number): Promise<ProviderHealthResult[]>;
1009
1013
  /**
@@ -1084,6 +1088,8 @@ interface SelectionConfig {
1084
1088
  freshnessWeight: number;
1085
1089
  /** Minimum acceptable provider status */
1086
1090
  minStatus: ProviderStatus[];
1091
+ /** Cooldown period (ms) before retrying failed providers (default: 30000) */
1092
+ retryCooldownMs: number;
1087
1093
  }
1088
1094
  /**
1089
1095
  * Provider Selector
@@ -1196,6 +1202,22 @@ declare class ProviderSelector {
1196
1202
  * 2. Health check result browserCompatible flag (if health check was performed)
1197
1203
  */
1198
1204
  private filterBrowserCompatible;
1205
+ /**
1206
+ * Check if an error is retryable (temporary) or permanent
1207
+ *
1208
+ * Permanent errors (never retry):
1209
+ * - 404 (Not Found) - endpoint doesn't exist
1210
+ * - 401 (Unauthorized) - invalid API key
1211
+ * - Invalid API key errors
1212
+ *
1213
+ * Retryable errors (can retry after cooldown):
1214
+ * - 503 (Service Unavailable) - temporary server issue
1215
+ * - 502 (Bad Gateway) - temporary gateway issue
1216
+ * - Timeout errors - network issues
1217
+ * - Network errors - connection issues
1218
+ * - 429 (Rate Limit) - temporary rate limit (handled separately)
1219
+ */
1220
+ private isRetryableError;
1199
1221
  }
1200
1222
  /**
1201
1223
  * Create a provider selector
@@ -1554,18 +1576,26 @@ declare class BrowserAdapter {
1554
1576
  getEndpointWithRateLimit(timeoutMs?: number): Promise<string>;
1555
1577
  /**
1556
1578
  * Make a JSON-RPC call to the TON API
1579
+ *
1580
+ * Note: Uses rate limiting to prevent 429 errors.
1557
1581
  */
1558
1582
  jsonRpc<T = unknown>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
1559
1583
  /**
1560
1584
  * Get address state
1585
+ *
1586
+ * Note: Uses rate limiting to prevent 429 errors.
1561
1587
  */
1562
1588
  getAddressState(address: string, timeoutMs?: number): Promise<'uninit' | 'active' | 'frozen'>;
1563
1589
  /**
1564
1590
  * Get address balance
1591
+ *
1592
+ * Note: Uses rate limiting to prevent 429 errors.
1565
1593
  */
1566
1594
  getAddressBalance(address: string, timeoutMs?: number): Promise<bigint>;
1567
1595
  /**
1568
1596
  * Get address information
1597
+ *
1598
+ * Note: Uses rate limiting to prevent 429 errors.
1569
1599
  */
1570
1600
  getAddressInfo(address: string, timeoutMs?: number): Promise<{
1571
1601
  state: 'uninit' | 'active' | 'frozen';
@@ -1575,6 +1605,8 @@ declare class BrowserAdapter {
1575
1605
  }>;
1576
1606
  /**
1577
1607
  * Run get method
1608
+ *
1609
+ * Note: Uses rate limiting to prevent 429 errors.
1578
1610
  */
1579
1611
  runGetMethod(address: string, method: string, stack?: unknown[], timeoutMs?: number): Promise<{
1580
1612
  exit_code: number;
package/dist/index.d.ts CHANGED
@@ -875,7 +875,10 @@ declare class TokenBucketRateLimiter {
875
875
  */
876
876
  release(): void;
877
877
  /**
878
- * Report a successful request (resets backoff)
878
+ * Report a successful request (gradually reduces backoff)
879
+ *
880
+ * Instead of immediately clearing backoff, we gradually reduce it to prevent
881
+ * immediately hitting the rate limit again after a single success.
879
882
  */
880
883
  reportSuccess(): void;
881
884
  /**
@@ -1004,6 +1007,7 @@ declare class HealthChecker {
1004
1007
  *
1005
1008
  * @param batchSize - Number of providers to test in parallel (default: 2)
1006
1009
  * @param batchDelayMs - Delay between batches in milliseconds (default: 500 to avoid rate limits)
1010
+ * If not provided, calculates delay based on lowest RPS in batch
1007
1011
  */
1008
1012
  testProviders(providers: ResolvedProvider[], batchSize?: number, batchDelayMs?: number): Promise<ProviderHealthResult[]>;
1009
1013
  /**
@@ -1084,6 +1088,8 @@ interface SelectionConfig {
1084
1088
  freshnessWeight: number;
1085
1089
  /** Minimum acceptable provider status */
1086
1090
  minStatus: ProviderStatus[];
1091
+ /** Cooldown period (ms) before retrying failed providers (default: 30000) */
1092
+ retryCooldownMs: number;
1087
1093
  }
1088
1094
  /**
1089
1095
  * Provider Selector
@@ -1196,6 +1202,22 @@ declare class ProviderSelector {
1196
1202
  * 2. Health check result browserCompatible flag (if health check was performed)
1197
1203
  */
1198
1204
  private filterBrowserCompatible;
1205
+ /**
1206
+ * Check if an error is retryable (temporary) or permanent
1207
+ *
1208
+ * Permanent errors (never retry):
1209
+ * - 404 (Not Found) - endpoint doesn't exist
1210
+ * - 401 (Unauthorized) - invalid API key
1211
+ * - Invalid API key errors
1212
+ *
1213
+ * Retryable errors (can retry after cooldown):
1214
+ * - 503 (Service Unavailable) - temporary server issue
1215
+ * - 502 (Bad Gateway) - temporary gateway issue
1216
+ * - Timeout errors - network issues
1217
+ * - Network errors - connection issues
1218
+ * - 429 (Rate Limit) - temporary rate limit (handled separately)
1219
+ */
1220
+ private isRetryableError;
1199
1221
  }
1200
1222
  /**
1201
1223
  * Create a provider selector
@@ -1554,18 +1576,26 @@ declare class BrowserAdapter {
1554
1576
  getEndpointWithRateLimit(timeoutMs?: number): Promise<string>;
1555
1577
  /**
1556
1578
  * Make a JSON-RPC call to the TON API
1579
+ *
1580
+ * Note: Uses rate limiting to prevent 429 errors.
1557
1581
  */
1558
1582
  jsonRpc<T = unknown>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
1559
1583
  /**
1560
1584
  * Get address state
1585
+ *
1586
+ * Note: Uses rate limiting to prevent 429 errors.
1561
1587
  */
1562
1588
  getAddressState(address: string, timeoutMs?: number): Promise<'uninit' | 'active' | 'frozen'>;
1563
1589
  /**
1564
1590
  * Get address balance
1591
+ *
1592
+ * Note: Uses rate limiting to prevent 429 errors.
1565
1593
  */
1566
1594
  getAddressBalance(address: string, timeoutMs?: number): Promise<bigint>;
1567
1595
  /**
1568
1596
  * Get address information
1597
+ *
1598
+ * Note: Uses rate limiting to prevent 429 errors.
1569
1599
  */
1570
1600
  getAddressInfo(address: string, timeoutMs?: number): Promise<{
1571
1601
  state: 'uninit' | 'active' | 'frozen';
@@ -1575,6 +1605,8 @@ declare class BrowserAdapter {
1575
1605
  }>;
1576
1606
  /**
1577
1607
  * Run get method
1608
+ *
1609
+ * Note: Uses rate limiting to prevent 429 errors.
1578
1610
  */
1579
1611
  runGetMethod(address: string, method: string, stack?: unknown[], timeoutMs?: number): Promise<{
1580
1612
  exit_code: number;
package/dist/index.js CHANGED
@@ -1154,17 +1154,26 @@ var HealthChecker = class {
1154
1154
  const endTime = performance.now();
1155
1155
  const latencyMs = Math.round(endTime - startTime);
1156
1156
  const errorMsg = error.message || String(error) || "Unknown error";
1157
+ const errorMsgLower = errorMsg.toLowerCase();
1157
1158
  const isCorsError = this.isCorsError(error, errorMsg);
1158
- const is429 = errorMsg.includes("429") || errorMsg.toLowerCase().includes("rate limit");
1159
- const is404 = errorMsg.includes("404") || errorMsg.toLowerCase().includes("not found");
1160
- const is503 = errorMsg.includes("503") || errorMsg.toLowerCase().includes("service unavailable");
1161
- const is502 = errorMsg.includes("502") || errorMsg.toLowerCase().includes("bad gateway");
1162
- const isTimeout = error.name === "AbortError" || errorMsg.includes("timeout");
1163
- const isOnFinalityBackendError = provider.type === "onfinality" && (errorMsg.includes("Backend error") || errorMsg.includes("backend error"));
1159
+ const responseStatus = error?.response?.status || error?.status || error?.statusCode || null;
1160
+ const statusMatch = errorMsg.match(/\b(\d{3})\b/);
1161
+ const statusFromMsg = statusMatch ? parseInt(statusMatch[1], 10) : null;
1162
+ const httpStatus = responseStatus || statusFromMsg;
1163
+ const is429 = httpStatus === 429 || errorMsgLower.includes("429") || errorMsgLower.includes("rate limit") || errorMsgLower.includes("too many requests");
1164
+ const is404 = httpStatus === 404 || errorMsgLower.includes("404") || errorMsgLower.includes("not found");
1165
+ const is401 = httpStatus === 401 || errorMsgLower.includes("401") || errorMsgLower.includes("unauthorized") || errorMsgLower.includes("invalid api key") || errorMsgLower.includes("authentication failed");
1166
+ const is403 = httpStatus === 403 || errorMsgLower.includes("403") || errorMsgLower.includes("forbidden");
1167
+ const is503 = httpStatus === 503 || errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
1168
+ const is502 = httpStatus === 502 || errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
1169
+ const isTimeout = error.name === "AbortError" || errorMsgLower.includes("timeout") || errorMsgLower.includes("timed out") || errorMsgLower.includes("aborted");
1170
+ const isOnFinalityBackendError = provider.type === "onfinality" && (errorMsgLower.includes("backend error") || errorMsgLower.includes("backend error"));
1164
1171
  let status = "offline";
1165
1172
  if (is429) {
1166
1173
  status = "degraded";
1167
- } else if (is404 || is503 || is502 || isOnFinalityBackendError) {
1174
+ } else if (is404 || is401 || is403) {
1175
+ status = "offline";
1176
+ } else if (is503 || is502 || isOnFinalityBackendError) {
1168
1177
  status = "offline";
1169
1178
  } else if (isTimeout) {
1170
1179
  status = "offline";
@@ -1192,8 +1201,9 @@ var HealthChecker = class {
1192
1201
  *
1193
1202
  * @param batchSize - Number of providers to test in parallel (default: 2)
1194
1203
  * @param batchDelayMs - Delay between batches in milliseconds (default: 500 to avoid rate limits)
1204
+ * If not provided, calculates delay based on lowest RPS in batch
1195
1205
  */
1196
- async testProviders(providers, batchSize = 2, batchDelayMs = 500) {
1206
+ async testProviders(providers, batchSize = 2, batchDelayMs) {
1197
1207
  const results = [];
1198
1208
  for (let i = 0; i < providers.length; i += batchSize) {
1199
1209
  const batch = providers.slice(i, i + batchSize);
@@ -1201,8 +1211,15 @@ var HealthChecker = class {
1201
1211
  batch.map((p) => this.testProvider(p))
1202
1212
  );
1203
1213
  results.push(...batchResults);
1204
- if (i + batchSize < providers.length && batchDelayMs > 0) {
1205
- await this.sleep(batchDelayMs);
1214
+ if (i + batchSize < providers.length) {
1215
+ let delay = batchDelayMs;
1216
+ if (delay === void 0) {
1217
+ const minRps = Math.min(...batch.map((p) => p.rps || 1));
1218
+ delay = Math.max(500, Math.ceil(1e3 / minRps * 1.5));
1219
+ }
1220
+ if (delay > 0) {
1221
+ await this.sleep(delay);
1222
+ }
1206
1223
  }
1207
1224
  }
1208
1225
  return results;
@@ -1591,24 +1608,36 @@ var TokenBucketRateLimiter = class {
1591
1608
  const startTime = Date.now();
1592
1609
  if (this.processing) {
1593
1610
  const acquired = await new Promise((resolve) => {
1611
+ let timeoutInterval = null;
1612
+ let resolved = false;
1613
+ const cleanup = () => {
1614
+ if (timeoutInterval !== null) {
1615
+ clearInterval(timeoutInterval);
1616
+ timeoutInterval = null;
1617
+ }
1618
+ };
1594
1619
  const checkTimeout = () => {
1595
1620
  if (Date.now() - startTime > timeoutMs) {
1596
1621
  const idx = this.requestQueue.indexOf(resolveCallback);
1597
1622
  if (idx >= 0) {
1598
1623
  this.requestQueue.splice(idx, 1);
1599
1624
  }
1600
- resolve(false);
1625
+ if (!resolved) {
1626
+ resolved = true;
1627
+ cleanup();
1628
+ resolve(false);
1629
+ }
1601
1630
  }
1602
1631
  };
1603
- const resolveCallback = () => resolve(true);
1604
- this.requestQueue.push(resolveCallback);
1605
- const timeoutInterval = setInterval(checkTimeout, 1e3);
1606
- const cleanup = () => clearInterval(timeoutInterval);
1607
- Promise.resolve().then(() => {
1608
- if (this.requestQueue.includes(resolveCallback)) ; else {
1632
+ const resolveCallback = () => {
1633
+ if (!resolved) {
1634
+ resolved = true;
1609
1635
  cleanup();
1636
+ resolve(true);
1610
1637
  }
1611
- });
1638
+ };
1639
+ this.requestQueue.push(resolveCallback);
1640
+ timeoutInterval = setInterval(checkTimeout, 1e3);
1612
1641
  });
1613
1642
  if (!acquired) {
1614
1643
  return false;
@@ -1616,24 +1645,25 @@ var TokenBucketRateLimiter = class {
1616
1645
  }
1617
1646
  this.processing = true;
1618
1647
  try {
1619
- this.refill();
1620
1648
  if (this.currentBackoff > 0) {
1621
1649
  this.logger.debug(`Applying backoff: ${this.currentBackoff}ms`);
1622
1650
  await sleep(this.currentBackoff);
1623
1651
  this.lastRefill = Date.now();
1624
- this.currentBackoff = 0;
1625
1652
  }
1653
+ this.refill();
1626
1654
  while (this.tokens <= 0) {
1627
1655
  if (Date.now() - startTime > timeoutMs) {
1628
1656
  return false;
1629
1657
  }
1630
- await sleep(Math.min(100, this.config.minDelayMs));
1658
+ const waitTime = Math.min(100, this.config.minDelayMs);
1659
+ await sleep(waitTime);
1631
1660
  this.refill();
1632
1661
  }
1633
1662
  this.tokens--;
1634
1663
  const timeSinceLastRefill = Date.now() - this.lastRefill;
1635
1664
  if (timeSinceLastRefill < this.config.minDelayMs) {
1636
- await sleep(this.config.minDelayMs - timeSinceLastRefill);
1665
+ const remainingDelay = this.config.minDelayMs - timeSinceLastRefill;
1666
+ await sleep(remainingDelay);
1637
1667
  }
1638
1668
  this.lastRefill = Date.now();
1639
1669
  return true;
@@ -1651,10 +1681,21 @@ var TokenBucketRateLimiter = class {
1651
1681
  release() {
1652
1682
  }
1653
1683
  /**
1654
- * Report a successful request (resets backoff)
1684
+ * Report a successful request (gradually reduces backoff)
1685
+ *
1686
+ * Instead of immediately clearing backoff, we gradually reduce it to prevent
1687
+ * immediately hitting the rate limit again after a single success.
1655
1688
  */
1656
1689
  reportSuccess() {
1657
- this.currentBackoff = 0;
1690
+ if (this.currentBackoff > 0) {
1691
+ this.currentBackoff = Math.max(
1692
+ this.config.minDelayMs,
1693
+ Math.floor(this.currentBackoff / 2)
1694
+ );
1695
+ if (this.currentBackoff <= this.config.minDelayMs) {
1696
+ this.currentBackoff = 0;
1697
+ }
1698
+ }
1658
1699
  this.consecutiveErrors = 0;
1659
1700
  }
1660
1701
  /**
@@ -1845,7 +1886,9 @@ var DEFAULT_CONFIG2 = {
1845
1886
  latencyWeight: 0.4,
1846
1887
  priorityWeight: 0.3,
1847
1888
  freshnessWeight: 0.3,
1848
- minStatus: ["available", "degraded"]
1889
+ minStatus: ["available", "degraded"],
1890
+ retryCooldownMs: 3e4
1891
+ // 30 seconds
1849
1892
  };
1850
1893
  var ProviderSelector = class {
1851
1894
  constructor(registry, healthChecker, config, logger, adapter = "node") {
@@ -1939,14 +1982,19 @@ var ProviderSelector = class {
1939
1982
  return defaultProvider;
1940
1983
  }
1941
1984
  if (health.success === false && health.lastTested) {
1942
- const timeSinceFailure = Date.now() - health.lastTested.getTime();
1943
- const cooldownMs = 3e4;
1944
- if (timeSinceFailure > cooldownMs) {
1945
- this.logger.warn(
1946
- `No healthy providers for ${network}, retrying failed default after cooldown: ${defaultProvider.id}`
1985
+ if (this.isRetryableError(health.error)) {
1986
+ const timeSinceFailure = Date.now() - health.lastTested.getTime();
1987
+ if (timeSinceFailure > this.config.retryCooldownMs) {
1988
+ this.logger.warn(
1989
+ `No healthy providers for ${network}, retrying failed default after cooldown: ${defaultProvider.id}`
1990
+ );
1991
+ this.activeProviderByNetwork.set(network, defaultProvider.id);
1992
+ return defaultProvider;
1993
+ }
1994
+ } else {
1995
+ this.logger.debug(
1996
+ `Skipping provider ${defaultProvider.id} with permanent error: ${health.error}`
1947
1997
  );
1948
- this.activeProviderByNetwork.set(network, defaultProvider.id);
1949
- return defaultProvider;
1950
1998
  }
1951
1999
  }
1952
2000
  }
@@ -1960,14 +2008,19 @@ var ProviderSelector = class {
1960
2008
  return provider;
1961
2009
  }
1962
2010
  if (health.success === false && health.lastTested) {
1963
- const timeSinceFailure = Date.now() - health.lastTested.getTime();
1964
- const cooldownMs = 3e4;
1965
- if (timeSinceFailure > cooldownMs) {
1966
- this.logger.warn(
1967
- `No healthy providers for ${network}, retrying failed provider after cooldown: ${provider.id}`
2011
+ if (this.isRetryableError(health.error)) {
2012
+ const timeSinceFailure = Date.now() - health.lastTested.getTime();
2013
+ if (timeSinceFailure > this.config.retryCooldownMs) {
2014
+ this.logger.warn(
2015
+ `No healthy providers for ${network}, retrying failed provider after cooldown: ${provider.id}`
2016
+ );
2017
+ this.activeProviderByNetwork.set(network, provider.id);
2018
+ return provider;
2019
+ }
2020
+ } else {
2021
+ this.logger.debug(
2022
+ `Skipping provider ${provider.id} with permanent error: ${health.error}`
1968
2023
  );
1969
- this.activeProviderByNetwork.set(network, provider.id);
1970
- return provider;
1971
2024
  }
1972
2025
  }
1973
2026
  }
@@ -1982,6 +2035,12 @@ var ProviderSelector = class {
1982
2035
  this.logger.debug(
1983
2036
  `Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)})`
1984
2037
  );
2038
+ } else if (bestHealth && bestHealth.success === false) {
2039
+ this.bestProviderByNetwork.delete(network);
2040
+ this.activeProviderByNetwork.set(network, best.id);
2041
+ this.logger.debug(
2042
+ `Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)}, failed - not cached)`
2043
+ );
1985
2044
  } else {
1986
2045
  this.activeProviderByNetwork.set(network, best.id);
1987
2046
  this.logger.debug(
@@ -2032,11 +2091,12 @@ var ProviderSelector = class {
2032
2091
  return 0.01 * (1 / (provider.priority + 1));
2033
2092
  }
2034
2093
  if (health.success === false) {
2035
- if (health.lastTested) {
2036
- const timeSinceFailure = Date.now() - health.lastTested.getTime();
2037
- const cooldownMs = 3e4;
2038
- if (timeSinceFailure > cooldownMs) {
2039
- return 1e-3 * (1 / (provider.priority + 1));
2094
+ if (this.isRetryableError(health.error)) {
2095
+ if (health.lastTested) {
2096
+ const timeSinceFailure = Date.now() - health.lastTested.getTime();
2097
+ if (timeSinceFailure > this.config.retryCooldownMs) {
2098
+ return 1e-3 * (1 / (provider.priority + 1));
2099
+ }
2040
2100
  }
2041
2101
  }
2042
2102
  return 0;
@@ -2231,6 +2291,34 @@ var ProviderSelector = class {
2231
2291
  return true;
2232
2292
  });
2233
2293
  }
2294
+ /**
2295
+ * Check if an error is retryable (temporary) or permanent
2296
+ *
2297
+ * Permanent errors (never retry):
2298
+ * - 404 (Not Found) - endpoint doesn't exist
2299
+ * - 401 (Unauthorized) - invalid API key
2300
+ * - Invalid API key errors
2301
+ *
2302
+ * Retryable errors (can retry after cooldown):
2303
+ * - 503 (Service Unavailable) - temporary server issue
2304
+ * - 502 (Bad Gateway) - temporary gateway issue
2305
+ * - Timeout errors - network issues
2306
+ * - Network errors - connection issues
2307
+ * - 429 (Rate Limit) - temporary rate limit (handled separately)
2308
+ */
2309
+ isRetryableError(error) {
2310
+ if (!error) {
2311
+ return true;
2312
+ }
2313
+ const errorLower = error.toLowerCase();
2314
+ 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")) {
2315
+ return false;
2316
+ }
2317
+ 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")) {
2318
+ return true;
2319
+ }
2320
+ return true;
2321
+ }
2234
2322
  };
2235
2323
  function createSelector(registry, healthChecker, config, logger, adapter = "node") {
2236
2324
  return new ProviderSelector(registry, healthChecker, config, logger, adapter);
@@ -2654,10 +2742,17 @@ var _ProviderManager = class _ProviderManager {
2654
2742
  }
2655
2743
  const acquired = await this.rateLimiter.acquire(provider.id, timeoutMs);
2656
2744
  if (!acquired) {
2657
- this.options.logger.warn(`Rate limit timeout for ${provider.id}`);
2745
+ const timeoutError = new Error(`Rate limit token acquisition timeout for ${provider.id} after ${timeoutMs || 6e4}ms`);
2746
+ this.options.logger.warn(timeoutError.message);
2747
+ this.reportError(timeoutError);
2658
2748
  const next = this.selector.getNextProvider(this.network, [provider.id]);
2659
2749
  if (next) {
2660
- return normalizeV2Endpoint(next.endpointV2, next);
2750
+ const nextTimeout = timeoutMs ? Math.min(timeoutMs, 1e4) : 1e4;
2751
+ const nextAcquired = await this.rateLimiter.acquire(next.id, nextTimeout);
2752
+ if (nextAcquired) {
2753
+ return normalizeV2Endpoint(next.endpointV2, next);
2754
+ }
2755
+ this.options.logger.warn(`Rate limit timeout for next provider ${next.id}, using fallback`);
2661
2756
  }
2662
2757
  return this.getFallbackEndpoint();
2663
2758
  }
@@ -2707,26 +2802,48 @@ var _ProviderManager = class _ProviderManager {
2707
2802
  if (!provider) {
2708
2803
  provider = this.selector.getBestProvider(this.network);
2709
2804
  }
2710
- if (!provider) return;
2805
+ if (!provider) {
2806
+ this.options.logger.warn(`Cannot report error: no provider available for ${this.network}`);
2807
+ return;
2808
+ }
2711
2809
  const errorMsg = error instanceof Error ? error.message : String(error);
2712
2810
  const errorMsgLower = errorMsg.toLowerCase();
2713
- const is429 = errorMsgLower.includes("429") || errorMsgLower.includes("rate limit");
2714
- const is503 = errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
2715
- const is502 = errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
2716
- const is404 = errorMsgLower.includes("404") || errorMsgLower.includes("not found");
2717
- const isTimeout = errorMsgLower.includes("timeout") || errorMsgLower.includes("abort");
2811
+ const enhancedErrorMsg = `Provider ${provider.id} (${provider.name}): ${errorMsg}`;
2812
+ const responseStatus = error instanceof Error && error?.response?.status || error instanceof Error && error?.status || error instanceof Error && error?.statusCode || null;
2813
+ const statusMatch = errorMsg.match(/\b(\d{3})\b/);
2814
+ const statusFromMsg = statusMatch ? parseInt(statusMatch[1], 10) : null;
2815
+ const httpStatus = responseStatus || statusFromMsg;
2816
+ const is429 = httpStatus === 429 || errorMsgLower.includes("429") || errorMsgLower.includes("rate limit") || errorMsgLower.includes("too many requests");
2817
+ const is503 = httpStatus === 503 || errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
2818
+ const is502 = httpStatus === 502 || errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
2819
+ const is404 = httpStatus === 404 || errorMsgLower.includes("404") || errorMsgLower.includes("not found");
2820
+ const is401 = httpStatus === 401 || errorMsgLower.includes("401") || errorMsgLower.includes("unauthorized") || errorMsgLower.includes("invalid api key") || errorMsgLower.includes("authentication failed");
2821
+ const isTimeout = error instanceof Error && error.name === "AbortError" || errorMsgLower.includes("timeout") || errorMsgLower.includes("timed out") || errorMsgLower.includes("abort");
2718
2822
  if (isRateLimitError(error) || is429) {
2719
2823
  this.rateLimiter.reportRateLimitError(provider.id);
2720
- this.healthChecker.markDegraded(provider.id, this.network, errorMsg);
2721
- } else if (is503 || is502 || is404 || isTimeout) {
2824
+ this.healthChecker.markDegraded(provider.id, this.network, enhancedErrorMsg);
2825
+ this.options.logger.warn(`${enhancedErrorMsg} - Rate limit detected, switching to next provider`);
2826
+ } else if (is404 || is401) {
2827
+ this.rateLimiter.reportError(provider.id);
2828
+ this.healthChecker.markOffline(provider.id, this.network, enhancedErrorMsg);
2829
+ this.options.logger.error(`${enhancedErrorMsg} - Permanent error (${is404 ? "404" : "401"}), provider marked offline`);
2830
+ } else if (is503 || is502 || isTimeout) {
2722
2831
  this.rateLimiter.reportError(provider.id);
2723
- this.healthChecker.markOffline(provider.id, this.network, errorMsg);
2832
+ this.healthChecker.markOffline(provider.id, this.network, enhancedErrorMsg);
2833
+ const errorType = is503 ? "503" : is502 ? "502" : "timeout";
2834
+ this.options.logger.warn(`${enhancedErrorMsg} - Server error (${errorType}), switching to next provider`);
2724
2835
  } else {
2725
2836
  this.rateLimiter.reportError(provider.id);
2726
- this.healthChecker.markDegraded(provider.id, this.network, errorMsg);
2837
+ this.healthChecker.markDegraded(provider.id, this.network, enhancedErrorMsg);
2838
+ this.options.logger.warn(`${enhancedErrorMsg} - Unknown error, switching to next provider`);
2839
+ }
2840
+ const nextProvider = this.selector.handleProviderFailure(provider.id, this.network);
2841
+ if (nextProvider) {
2842
+ this.options.logger.info(`Failover: switched from ${provider.id} to ${nextProvider.id}`);
2843
+ } else {
2844
+ this.options.logger.warn(`Failover: no alternative provider available for ${this.network}`);
2727
2845
  }
2728
2846
  this.selector.clearCache(this.network);
2729
- this.selector.handleProviderFailure(provider.id, this.network);
2730
2847
  this.notifyListeners();
2731
2848
  }
2732
2849
  // ========================================================================
@@ -3277,9 +3394,11 @@ var BrowserAdapter = class {
3277
3394
  // ========================================================================
3278
3395
  /**
3279
3396
  * Make a JSON-RPC call to the TON API
3397
+ *
3398
+ * Note: Uses rate limiting to prevent 429 errors.
3280
3399
  */
3281
3400
  async jsonRpc(method, params = {}, timeoutMs = 1e4) {
3282
- const endpoint = await this.manager.getEndpoint();
3401
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3283
3402
  const controller = new AbortController();
3284
3403
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
3285
3404
  try {
@@ -3318,9 +3437,11 @@ var BrowserAdapter = class {
3318
3437
  // ========================================================================
3319
3438
  /**
3320
3439
  * Get address state
3440
+ *
3441
+ * Note: Uses rate limiting to prevent 429 errors.
3321
3442
  */
3322
3443
  async getAddressState(address, timeoutMs = 1e4) {
3323
- const endpoint = await this.manager.getEndpoint();
3444
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3324
3445
  const baseV2 = toV2Base(endpoint);
3325
3446
  const url = `${baseV2}/getAddressState?address=${encodeURIComponent(address)}`;
3326
3447
  const controller = new AbortController();
@@ -3350,9 +3471,11 @@ var BrowserAdapter = class {
3350
3471
  }
3351
3472
  /**
3352
3473
  * Get address balance
3474
+ *
3475
+ * Note: Uses rate limiting to prevent 429 errors.
3353
3476
  */
3354
3477
  async getAddressBalance(address, timeoutMs = 1e4) {
3355
- const endpoint = await this.manager.getEndpoint();
3478
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3356
3479
  const baseV2 = toV2Base(endpoint);
3357
3480
  const url = `${baseV2}/getAddressBalance?address=${encodeURIComponent(address)}`;
3358
3481
  const controller = new AbortController();
@@ -3382,9 +3505,11 @@ var BrowserAdapter = class {
3382
3505
  }
3383
3506
  /**
3384
3507
  * Get address information
3508
+ *
3509
+ * Note: Uses rate limiting to prevent 429 errors.
3385
3510
  */
3386
3511
  async getAddressInfo(address, timeoutMs = 1e4) {
3387
- const endpoint = await this.manager.getEndpoint();
3512
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3388
3513
  const baseV2 = toV2Base(endpoint);
3389
3514
  const url = `${baseV2}/getAddressInformation?address=${encodeURIComponent(address)}`;
3390
3515
  const controller = new AbortController();
@@ -3412,9 +3537,11 @@ var BrowserAdapter = class {
3412
3537
  }
3413
3538
  /**
3414
3539
  * Run get method
3540
+ *
3541
+ * Note: Uses rate limiting to prevent 429 errors.
3415
3542
  */
3416
3543
  async runGetMethod(address, method, stack = [], timeoutMs = 15e3) {
3417
- const endpoint = await this.manager.getEndpoint();
3544
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3418
3545
  const baseV2 = toV2Base(endpoint);
3419
3546
  const url = `${baseV2}/runGetMethod`;
3420
3547
  const controller = new AbortController();