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 +111 -14
- package/dist/index.cjs +189 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +189 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
//
|
|
63
|
-
const
|
|
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
|
|
66
|
-
const
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
408
|
-
|
|
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
|
|
1162
|
-
const
|
|
1163
|
-
const
|
|
1164
|
-
const
|
|
1165
|
-
const
|
|
1166
|
-
const
|
|
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 ||
|
|
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
|
|
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
|
|
1208
|
-
|
|
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
|
-
|
|
1628
|
+
if (!resolved) {
|
|
1629
|
+
resolved = true;
|
|
1630
|
+
cleanup();
|
|
1631
|
+
resolve(false);
|
|
1632
|
+
}
|
|
1604
1633
|
}
|
|
1605
1634
|
};
|
|
1606
|
-
const resolveCallback = () =>
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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.
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
2717
|
-
const
|
|
2718
|
-
const
|
|
2719
|
-
const
|
|
2720
|
-
const
|
|
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,
|
|
2724
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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();
|