ton-provider-system 0.2.1 → 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 +222 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +222 -71
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/rpc.json +1 -1
package/dist/index.js
CHANGED
|
@@ -750,17 +750,43 @@ var OnFinalityProvider = class extends BaseProvider {
|
|
|
750
750
|
if (normalized.includes("onfinality.io")) {
|
|
751
751
|
try {
|
|
752
752
|
const url = new URL(normalized);
|
|
753
|
-
const
|
|
753
|
+
const hasQueryParams = normalized.includes("?");
|
|
754
|
+
const queryString = hasQueryParams ? normalized.split("?")[1] : "";
|
|
755
|
+
const hasApiKeyInQuery = queryString.includes("apikey=");
|
|
754
756
|
if (!url.pathname || url.pathname === "/") {
|
|
755
757
|
if (this.provider.apiKey) {
|
|
756
|
-
|
|
758
|
+
const baseUrl2 = normalized.split("?")[0];
|
|
759
|
+
if (hasApiKeyInQuery) {
|
|
760
|
+
return normalized.replace(/\/?$/, "/rpc");
|
|
761
|
+
}
|
|
762
|
+
if (hasQueryParams) {
|
|
763
|
+
return `${baseUrl2.replace(/\/?$/, "/rpc")}?${queryString}&apikey=${encodeURIComponent(this.provider.apiKey)}`;
|
|
764
|
+
}
|
|
765
|
+
return `${baseUrl2.replace(/\/?$/, "/rpc")}?apikey=${encodeURIComponent(this.provider.apiKey)}`;
|
|
757
766
|
}
|
|
767
|
+
const baseUrl = normalized.split("?")[0];
|
|
758
768
|
return baseUrl.replace(/\/?$/, "/public");
|
|
759
769
|
}
|
|
760
770
|
if (url.pathname === "/rpc" || url.pathname === "/public") {
|
|
761
|
-
|
|
771
|
+
if (url.pathname === "/rpc") {
|
|
772
|
+
if (hasApiKeyInQuery) {
|
|
773
|
+
return normalized;
|
|
774
|
+
}
|
|
775
|
+
if (this.provider.apiKey) {
|
|
776
|
+
const baseUrl = normalized.split("?")[0];
|
|
777
|
+
if (hasQueryParams) {
|
|
778
|
+
return `${baseUrl}?${queryString}&apikey=${encodeURIComponent(this.provider.apiKey)}`;
|
|
779
|
+
}
|
|
780
|
+
return `${baseUrl}?apikey=${encodeURIComponent(this.provider.apiKey)}`;
|
|
781
|
+
}
|
|
782
|
+
return normalized;
|
|
783
|
+
}
|
|
784
|
+
if (url.pathname === "/public") {
|
|
785
|
+
return normalized.split("?")[0];
|
|
786
|
+
}
|
|
787
|
+
return normalized;
|
|
762
788
|
}
|
|
763
|
-
return
|
|
789
|
+
return normalized;
|
|
764
790
|
} catch {
|
|
765
791
|
if (normalized.includes("{key}")) {
|
|
766
792
|
return normalized.split("?")[0].replace(/\/?$/, "/public");
|
|
@@ -768,7 +794,7 @@ var OnFinalityProvider = class extends BaseProvider {
|
|
|
768
794
|
if (!normalized.includes("/rpc") && !normalized.includes("/public")) {
|
|
769
795
|
return normalized.split("?")[0] + "/public";
|
|
770
796
|
}
|
|
771
|
-
return normalized
|
|
797
|
+
return normalized;
|
|
772
798
|
}
|
|
773
799
|
}
|
|
774
800
|
return normalized;
|
|
@@ -777,9 +803,6 @@ var OnFinalityProvider = class extends BaseProvider {
|
|
|
777
803
|
const headers = {
|
|
778
804
|
"Content-Type": "application/json"
|
|
779
805
|
};
|
|
780
|
-
if (this.provider.apiKey) {
|
|
781
|
-
headers["apikey"] = this.provider.apiKey;
|
|
782
|
-
}
|
|
783
806
|
return headers;
|
|
784
807
|
}
|
|
785
808
|
parseResponse(data) {
|
|
@@ -1083,7 +1106,8 @@ var HealthChecker = class {
|
|
|
1083
1106
|
} catch (error) {
|
|
1084
1107
|
if (provider.type === "onfinality" && normalizedEndpoint.includes("/rpc") && provider.apiKey && error.message?.includes("backend error")) {
|
|
1085
1108
|
this.logger.debug(`OnFinality /rpc failed, retrying with /public endpoint`);
|
|
1086
|
-
const
|
|
1109
|
+
const baseUrl = normalizedEndpoint.split("?")[0];
|
|
1110
|
+
const publicEndpoint = baseUrl.replace("/rpc", "/public");
|
|
1087
1111
|
const publicProvider = { ...provider, apiKey: void 0 };
|
|
1088
1112
|
const publicProviderImpl = createProvider(publicProvider);
|
|
1089
1113
|
info = await this.callGetMasterchainInfo(publicEndpoint, publicProvider, publicProviderImpl);
|
|
@@ -1130,17 +1154,26 @@ var HealthChecker = class {
|
|
|
1130
1154
|
const endTime = performance.now();
|
|
1131
1155
|
const latencyMs = Math.round(endTime - startTime);
|
|
1132
1156
|
const errorMsg = error.message || String(error) || "Unknown error";
|
|
1157
|
+
const errorMsgLower = errorMsg.toLowerCase();
|
|
1133
1158
|
const isCorsError = this.isCorsError(error, errorMsg);
|
|
1134
|
-
const
|
|
1135
|
-
const
|
|
1136
|
-
const
|
|
1137
|
-
const
|
|
1138
|
-
const
|
|
1139
|
-
const
|
|
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"));
|
|
1140
1171
|
let status = "offline";
|
|
1141
1172
|
if (is429) {
|
|
1142
1173
|
status = "degraded";
|
|
1143
|
-
} else if (is404 ||
|
|
1174
|
+
} else if (is404 || is401 || is403) {
|
|
1175
|
+
status = "offline";
|
|
1176
|
+
} else if (is503 || is502 || isOnFinalityBackendError) {
|
|
1144
1177
|
status = "offline";
|
|
1145
1178
|
} else if (isTimeout) {
|
|
1146
1179
|
status = "offline";
|
|
@@ -1168,8 +1201,9 @@ var HealthChecker = class {
|
|
|
1168
1201
|
*
|
|
1169
1202
|
* @param batchSize - Number of providers to test in parallel (default: 2)
|
|
1170
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
|
|
1171
1205
|
*/
|
|
1172
|
-
async testProviders(providers, batchSize = 2, batchDelayMs
|
|
1206
|
+
async testProviders(providers, batchSize = 2, batchDelayMs) {
|
|
1173
1207
|
const results = [];
|
|
1174
1208
|
for (let i = 0; i < providers.length; i += batchSize) {
|
|
1175
1209
|
const batch = providers.slice(i, i + batchSize);
|
|
@@ -1177,8 +1211,15 @@ var HealthChecker = class {
|
|
|
1177
1211
|
batch.map((p) => this.testProvider(p))
|
|
1178
1212
|
);
|
|
1179
1213
|
results.push(...batchResults);
|
|
1180
|
-
if (i + batchSize < providers.length
|
|
1181
|
-
|
|
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
|
+
}
|
|
1182
1223
|
}
|
|
1183
1224
|
}
|
|
1184
1225
|
return results;
|
|
@@ -1567,24 +1608,36 @@ var TokenBucketRateLimiter = class {
|
|
|
1567
1608
|
const startTime = Date.now();
|
|
1568
1609
|
if (this.processing) {
|
|
1569
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
|
+
};
|
|
1570
1619
|
const checkTimeout = () => {
|
|
1571
1620
|
if (Date.now() - startTime > timeoutMs) {
|
|
1572
1621
|
const idx = this.requestQueue.indexOf(resolveCallback);
|
|
1573
1622
|
if (idx >= 0) {
|
|
1574
1623
|
this.requestQueue.splice(idx, 1);
|
|
1575
1624
|
}
|
|
1576
|
-
|
|
1625
|
+
if (!resolved) {
|
|
1626
|
+
resolved = true;
|
|
1627
|
+
cleanup();
|
|
1628
|
+
resolve(false);
|
|
1629
|
+
}
|
|
1577
1630
|
}
|
|
1578
1631
|
};
|
|
1579
|
-
const resolveCallback = () =>
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
const cleanup = () => clearInterval(timeoutInterval);
|
|
1583
|
-
Promise.resolve().then(() => {
|
|
1584
|
-
if (this.requestQueue.includes(resolveCallback)) ; else {
|
|
1632
|
+
const resolveCallback = () => {
|
|
1633
|
+
if (!resolved) {
|
|
1634
|
+
resolved = true;
|
|
1585
1635
|
cleanup();
|
|
1636
|
+
resolve(true);
|
|
1586
1637
|
}
|
|
1587
|
-
}
|
|
1638
|
+
};
|
|
1639
|
+
this.requestQueue.push(resolveCallback);
|
|
1640
|
+
timeoutInterval = setInterval(checkTimeout, 1e3);
|
|
1588
1641
|
});
|
|
1589
1642
|
if (!acquired) {
|
|
1590
1643
|
return false;
|
|
@@ -1592,24 +1645,25 @@ var TokenBucketRateLimiter = class {
|
|
|
1592
1645
|
}
|
|
1593
1646
|
this.processing = true;
|
|
1594
1647
|
try {
|
|
1595
|
-
this.refill();
|
|
1596
1648
|
if (this.currentBackoff > 0) {
|
|
1597
1649
|
this.logger.debug(`Applying backoff: ${this.currentBackoff}ms`);
|
|
1598
1650
|
await sleep(this.currentBackoff);
|
|
1599
1651
|
this.lastRefill = Date.now();
|
|
1600
|
-
this.currentBackoff = 0;
|
|
1601
1652
|
}
|
|
1653
|
+
this.refill();
|
|
1602
1654
|
while (this.tokens <= 0) {
|
|
1603
1655
|
if (Date.now() - startTime > timeoutMs) {
|
|
1604
1656
|
return false;
|
|
1605
1657
|
}
|
|
1606
|
-
|
|
1658
|
+
const waitTime = Math.min(100, this.config.minDelayMs);
|
|
1659
|
+
await sleep(waitTime);
|
|
1607
1660
|
this.refill();
|
|
1608
1661
|
}
|
|
1609
1662
|
this.tokens--;
|
|
1610
1663
|
const timeSinceLastRefill = Date.now() - this.lastRefill;
|
|
1611
1664
|
if (timeSinceLastRefill < this.config.minDelayMs) {
|
|
1612
|
-
|
|
1665
|
+
const remainingDelay = this.config.minDelayMs - timeSinceLastRefill;
|
|
1666
|
+
await sleep(remainingDelay);
|
|
1613
1667
|
}
|
|
1614
1668
|
this.lastRefill = Date.now();
|
|
1615
1669
|
return true;
|
|
@@ -1627,10 +1681,21 @@ var TokenBucketRateLimiter = class {
|
|
|
1627
1681
|
release() {
|
|
1628
1682
|
}
|
|
1629
1683
|
/**
|
|
1630
|
-
* Report a successful request (
|
|
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.
|
|
1631
1688
|
*/
|
|
1632
1689
|
reportSuccess() {
|
|
1633
|
-
this.currentBackoff
|
|
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
|
+
}
|
|
1634
1699
|
this.consecutiveErrors = 0;
|
|
1635
1700
|
}
|
|
1636
1701
|
/**
|
|
@@ -1821,7 +1886,9 @@ var DEFAULT_CONFIG2 = {
|
|
|
1821
1886
|
latencyWeight: 0.4,
|
|
1822
1887
|
priorityWeight: 0.3,
|
|
1823
1888
|
freshnessWeight: 0.3,
|
|
1824
|
-
minStatus: ["available", "degraded"]
|
|
1889
|
+
minStatus: ["available", "degraded"],
|
|
1890
|
+
retryCooldownMs: 3e4
|
|
1891
|
+
// 30 seconds
|
|
1825
1892
|
};
|
|
1826
1893
|
var ProviderSelector = class {
|
|
1827
1894
|
constructor(registry, healthChecker, config, logger, adapter = "node") {
|
|
@@ -1915,14 +1982,19 @@ var ProviderSelector = class {
|
|
|
1915
1982
|
return defaultProvider;
|
|
1916
1983
|
}
|
|
1917
1984
|
if (health.success === false && health.lastTested) {
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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}`
|
|
1923
1997
|
);
|
|
1924
|
-
this.activeProviderByNetwork.set(network, defaultProvider.id);
|
|
1925
|
-
return defaultProvider;
|
|
1926
1998
|
}
|
|
1927
1999
|
}
|
|
1928
2000
|
}
|
|
@@ -1936,14 +2008,19 @@ var ProviderSelector = class {
|
|
|
1936
2008
|
return provider;
|
|
1937
2009
|
}
|
|
1938
2010
|
if (health.success === false && health.lastTested) {
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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}`
|
|
1944
2023
|
);
|
|
1945
|
-
this.activeProviderByNetwork.set(network, provider.id);
|
|
1946
|
-
return provider;
|
|
1947
2024
|
}
|
|
1948
2025
|
}
|
|
1949
2026
|
}
|
|
@@ -1958,6 +2035,12 @@ var ProviderSelector = class {
|
|
|
1958
2035
|
this.logger.debug(
|
|
1959
2036
|
`Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)})`
|
|
1960
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
|
+
);
|
|
1961
2044
|
} else {
|
|
1962
2045
|
this.activeProviderByNetwork.set(network, best.id);
|
|
1963
2046
|
this.logger.debug(
|
|
@@ -2008,11 +2091,12 @@ var ProviderSelector = class {
|
|
|
2008
2091
|
return 0.01 * (1 / (provider.priority + 1));
|
|
2009
2092
|
}
|
|
2010
2093
|
if (health.success === false) {
|
|
2011
|
-
if (health.
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
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
|
+
}
|
|
2016
2100
|
}
|
|
2017
2101
|
}
|
|
2018
2102
|
return 0;
|
|
@@ -2207,6 +2291,34 @@ var ProviderSelector = class {
|
|
|
2207
2291
|
return true;
|
|
2208
2292
|
});
|
|
2209
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
|
+
}
|
|
2210
2322
|
};
|
|
2211
2323
|
function createSelector(registry, healthChecker, config, logger, adapter = "node") {
|
|
2212
2324
|
return new ProviderSelector(registry, healthChecker, config, logger, adapter);
|
|
@@ -2630,10 +2742,17 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2630
2742
|
}
|
|
2631
2743
|
const acquired = await this.rateLimiter.acquire(provider.id, timeoutMs);
|
|
2632
2744
|
if (!acquired) {
|
|
2633
|
-
|
|
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);
|
|
2634
2748
|
const next = this.selector.getNextProvider(this.network, [provider.id]);
|
|
2635
2749
|
if (next) {
|
|
2636
|
-
|
|
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`);
|
|
2637
2756
|
}
|
|
2638
2757
|
return this.getFallbackEndpoint();
|
|
2639
2758
|
}
|
|
@@ -2683,26 +2802,48 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2683
2802
|
if (!provider) {
|
|
2684
2803
|
provider = this.selector.getBestProvider(this.network);
|
|
2685
2804
|
}
|
|
2686
|
-
if (!provider)
|
|
2805
|
+
if (!provider) {
|
|
2806
|
+
this.options.logger.warn(`Cannot report error: no provider available for ${this.network}`);
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2687
2809
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2688
2810
|
const errorMsgLower = errorMsg.toLowerCase();
|
|
2689
|
-
const
|
|
2690
|
-
const
|
|
2691
|
-
const
|
|
2692
|
-
const
|
|
2693
|
-
const
|
|
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");
|
|
2694
2822
|
if (isRateLimitError(error) || is429) {
|
|
2695
2823
|
this.rateLimiter.reportRateLimitError(provider.id);
|
|
2696
|
-
this.healthChecker.markDegraded(provider.id, this.network,
|
|
2697
|
-
|
|
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) {
|
|
2698
2831
|
this.rateLimiter.reportError(provider.id);
|
|
2699
|
-
this.healthChecker.markOffline(provider.id, this.network,
|
|
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`);
|
|
2700
2835
|
} else {
|
|
2701
2836
|
this.rateLimiter.reportError(provider.id);
|
|
2702
|
-
this.healthChecker.markDegraded(provider.id, this.network,
|
|
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}`);
|
|
2703
2845
|
}
|
|
2704
2846
|
this.selector.clearCache(this.network);
|
|
2705
|
-
this.selector.handleProviderFailure(provider.id, this.network);
|
|
2706
2847
|
this.notifyListeners();
|
|
2707
2848
|
}
|
|
2708
2849
|
// ========================================================================
|
|
@@ -3253,9 +3394,11 @@ var BrowserAdapter = class {
|
|
|
3253
3394
|
// ========================================================================
|
|
3254
3395
|
/**
|
|
3255
3396
|
* Make a JSON-RPC call to the TON API
|
|
3397
|
+
*
|
|
3398
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3256
3399
|
*/
|
|
3257
3400
|
async jsonRpc(method, params = {}, timeoutMs = 1e4) {
|
|
3258
|
-
const endpoint = await this.manager.
|
|
3401
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3259
3402
|
const controller = new AbortController();
|
|
3260
3403
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3261
3404
|
try {
|
|
@@ -3294,9 +3437,11 @@ var BrowserAdapter = class {
|
|
|
3294
3437
|
// ========================================================================
|
|
3295
3438
|
/**
|
|
3296
3439
|
* Get address state
|
|
3440
|
+
*
|
|
3441
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3297
3442
|
*/
|
|
3298
3443
|
async getAddressState(address, timeoutMs = 1e4) {
|
|
3299
|
-
const endpoint = await this.manager.
|
|
3444
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3300
3445
|
const baseV2 = toV2Base(endpoint);
|
|
3301
3446
|
const url = `${baseV2}/getAddressState?address=${encodeURIComponent(address)}`;
|
|
3302
3447
|
const controller = new AbortController();
|
|
@@ -3326,9 +3471,11 @@ var BrowserAdapter = class {
|
|
|
3326
3471
|
}
|
|
3327
3472
|
/**
|
|
3328
3473
|
* Get address balance
|
|
3474
|
+
*
|
|
3475
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3329
3476
|
*/
|
|
3330
3477
|
async getAddressBalance(address, timeoutMs = 1e4) {
|
|
3331
|
-
const endpoint = await this.manager.
|
|
3478
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3332
3479
|
const baseV2 = toV2Base(endpoint);
|
|
3333
3480
|
const url = `${baseV2}/getAddressBalance?address=${encodeURIComponent(address)}`;
|
|
3334
3481
|
const controller = new AbortController();
|
|
@@ -3358,9 +3505,11 @@ var BrowserAdapter = class {
|
|
|
3358
3505
|
}
|
|
3359
3506
|
/**
|
|
3360
3507
|
* Get address information
|
|
3508
|
+
*
|
|
3509
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3361
3510
|
*/
|
|
3362
3511
|
async getAddressInfo(address, timeoutMs = 1e4) {
|
|
3363
|
-
const endpoint = await this.manager.
|
|
3512
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3364
3513
|
const baseV2 = toV2Base(endpoint);
|
|
3365
3514
|
const url = `${baseV2}/getAddressInformation?address=${encodeURIComponent(address)}`;
|
|
3366
3515
|
const controller = new AbortController();
|
|
@@ -3388,9 +3537,11 @@ var BrowserAdapter = class {
|
|
|
3388
3537
|
}
|
|
3389
3538
|
/**
|
|
3390
3539
|
* Run get method
|
|
3540
|
+
*
|
|
3541
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3391
3542
|
*/
|
|
3392
3543
|
async runGetMethod(address, method, stack = [], timeoutMs = 15e3) {
|
|
3393
|
-
const endpoint = await this.manager.
|
|
3544
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3394
3545
|
const baseV2 = toV2Base(endpoint);
|
|
3395
3546
|
const url = `${baseV2}/runGetMethod`;
|
|
3396
3547
|
const controller = new AbortController();
|