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.cjs
CHANGED
|
@@ -753,17 +753,43 @@ var OnFinalityProvider = class extends BaseProvider {
|
|
|
753
753
|
if (normalized.includes("onfinality.io")) {
|
|
754
754
|
try {
|
|
755
755
|
const url = new URL(normalized);
|
|
756
|
-
const
|
|
756
|
+
const hasQueryParams = normalized.includes("?");
|
|
757
|
+
const queryString = hasQueryParams ? normalized.split("?")[1] : "";
|
|
758
|
+
const hasApiKeyInQuery = queryString.includes("apikey=");
|
|
757
759
|
if (!url.pathname || url.pathname === "/") {
|
|
758
760
|
if (this.provider.apiKey) {
|
|
759
|
-
|
|
761
|
+
const baseUrl2 = normalized.split("?")[0];
|
|
762
|
+
if (hasApiKeyInQuery) {
|
|
763
|
+
return normalized.replace(/\/?$/, "/rpc");
|
|
764
|
+
}
|
|
765
|
+
if (hasQueryParams) {
|
|
766
|
+
return `${baseUrl2.replace(/\/?$/, "/rpc")}?${queryString}&apikey=${encodeURIComponent(this.provider.apiKey)}`;
|
|
767
|
+
}
|
|
768
|
+
return `${baseUrl2.replace(/\/?$/, "/rpc")}?apikey=${encodeURIComponent(this.provider.apiKey)}`;
|
|
760
769
|
}
|
|
770
|
+
const baseUrl = normalized.split("?")[0];
|
|
761
771
|
return baseUrl.replace(/\/?$/, "/public");
|
|
762
772
|
}
|
|
763
773
|
if (url.pathname === "/rpc" || url.pathname === "/public") {
|
|
764
|
-
|
|
774
|
+
if (url.pathname === "/rpc") {
|
|
775
|
+
if (hasApiKeyInQuery) {
|
|
776
|
+
return normalized;
|
|
777
|
+
}
|
|
778
|
+
if (this.provider.apiKey) {
|
|
779
|
+
const baseUrl = normalized.split("?")[0];
|
|
780
|
+
if (hasQueryParams) {
|
|
781
|
+
return `${baseUrl}?${queryString}&apikey=${encodeURIComponent(this.provider.apiKey)}`;
|
|
782
|
+
}
|
|
783
|
+
return `${baseUrl}?apikey=${encodeURIComponent(this.provider.apiKey)}`;
|
|
784
|
+
}
|
|
785
|
+
return normalized;
|
|
786
|
+
}
|
|
787
|
+
if (url.pathname === "/public") {
|
|
788
|
+
return normalized.split("?")[0];
|
|
789
|
+
}
|
|
790
|
+
return normalized;
|
|
765
791
|
}
|
|
766
|
-
return
|
|
792
|
+
return normalized;
|
|
767
793
|
} catch {
|
|
768
794
|
if (normalized.includes("{key}")) {
|
|
769
795
|
return normalized.split("?")[0].replace(/\/?$/, "/public");
|
|
@@ -771,7 +797,7 @@ var OnFinalityProvider = class extends BaseProvider {
|
|
|
771
797
|
if (!normalized.includes("/rpc") && !normalized.includes("/public")) {
|
|
772
798
|
return normalized.split("?")[0] + "/public";
|
|
773
799
|
}
|
|
774
|
-
return normalized
|
|
800
|
+
return normalized;
|
|
775
801
|
}
|
|
776
802
|
}
|
|
777
803
|
return normalized;
|
|
@@ -780,9 +806,6 @@ var OnFinalityProvider = class extends BaseProvider {
|
|
|
780
806
|
const headers = {
|
|
781
807
|
"Content-Type": "application/json"
|
|
782
808
|
};
|
|
783
|
-
if (this.provider.apiKey) {
|
|
784
|
-
headers["apikey"] = this.provider.apiKey;
|
|
785
|
-
}
|
|
786
809
|
return headers;
|
|
787
810
|
}
|
|
788
811
|
parseResponse(data) {
|
|
@@ -1086,7 +1109,8 @@ var HealthChecker = class {
|
|
|
1086
1109
|
} catch (error) {
|
|
1087
1110
|
if (provider.type === "onfinality" && normalizedEndpoint.includes("/rpc") && provider.apiKey && error.message?.includes("backend error")) {
|
|
1088
1111
|
this.logger.debug(`OnFinality /rpc failed, retrying with /public endpoint`);
|
|
1089
|
-
const
|
|
1112
|
+
const baseUrl = normalizedEndpoint.split("?")[0];
|
|
1113
|
+
const publicEndpoint = baseUrl.replace("/rpc", "/public");
|
|
1090
1114
|
const publicProvider = { ...provider, apiKey: void 0 };
|
|
1091
1115
|
const publicProviderImpl = createProvider(publicProvider);
|
|
1092
1116
|
info = await this.callGetMasterchainInfo(publicEndpoint, publicProvider, publicProviderImpl);
|
|
@@ -1133,17 +1157,26 @@ var HealthChecker = class {
|
|
|
1133
1157
|
const endTime = performance.now();
|
|
1134
1158
|
const latencyMs = Math.round(endTime - startTime);
|
|
1135
1159
|
const errorMsg = error.message || String(error) || "Unknown error";
|
|
1160
|
+
const errorMsgLower = errorMsg.toLowerCase();
|
|
1136
1161
|
const isCorsError = this.isCorsError(error, errorMsg);
|
|
1137
|
-
const
|
|
1138
|
-
const
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1142
|
-
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"));
|
|
1143
1174
|
let status = "offline";
|
|
1144
1175
|
if (is429) {
|
|
1145
1176
|
status = "degraded";
|
|
1146
|
-
} else if (is404 ||
|
|
1177
|
+
} else if (is404 || is401 || is403) {
|
|
1178
|
+
status = "offline";
|
|
1179
|
+
} else if (is503 || is502 || isOnFinalityBackendError) {
|
|
1147
1180
|
status = "offline";
|
|
1148
1181
|
} else if (isTimeout) {
|
|
1149
1182
|
status = "offline";
|
|
@@ -1171,8 +1204,9 @@ var HealthChecker = class {
|
|
|
1171
1204
|
*
|
|
1172
1205
|
* @param batchSize - Number of providers to test in parallel (default: 2)
|
|
1173
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
|
|
1174
1208
|
*/
|
|
1175
|
-
async testProviders(providers, batchSize = 2, batchDelayMs
|
|
1209
|
+
async testProviders(providers, batchSize = 2, batchDelayMs) {
|
|
1176
1210
|
const results = [];
|
|
1177
1211
|
for (let i = 0; i < providers.length; i += batchSize) {
|
|
1178
1212
|
const batch = providers.slice(i, i + batchSize);
|
|
@@ -1180,8 +1214,15 @@ var HealthChecker = class {
|
|
|
1180
1214
|
batch.map((p) => this.testProvider(p))
|
|
1181
1215
|
);
|
|
1182
1216
|
results.push(...batchResults);
|
|
1183
|
-
if (i + batchSize < providers.length
|
|
1184
|
-
|
|
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
|
+
}
|
|
1185
1226
|
}
|
|
1186
1227
|
}
|
|
1187
1228
|
return results;
|
|
@@ -1570,24 +1611,36 @@ var TokenBucketRateLimiter = class {
|
|
|
1570
1611
|
const startTime = Date.now();
|
|
1571
1612
|
if (this.processing) {
|
|
1572
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
|
+
};
|
|
1573
1622
|
const checkTimeout = () => {
|
|
1574
1623
|
if (Date.now() - startTime > timeoutMs) {
|
|
1575
1624
|
const idx = this.requestQueue.indexOf(resolveCallback);
|
|
1576
1625
|
if (idx >= 0) {
|
|
1577
1626
|
this.requestQueue.splice(idx, 1);
|
|
1578
1627
|
}
|
|
1579
|
-
|
|
1628
|
+
if (!resolved) {
|
|
1629
|
+
resolved = true;
|
|
1630
|
+
cleanup();
|
|
1631
|
+
resolve(false);
|
|
1632
|
+
}
|
|
1580
1633
|
}
|
|
1581
1634
|
};
|
|
1582
|
-
const resolveCallback = () =>
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
const cleanup = () => clearInterval(timeoutInterval);
|
|
1586
|
-
Promise.resolve().then(() => {
|
|
1587
|
-
if (this.requestQueue.includes(resolveCallback)) ; else {
|
|
1635
|
+
const resolveCallback = () => {
|
|
1636
|
+
if (!resolved) {
|
|
1637
|
+
resolved = true;
|
|
1588
1638
|
cleanup();
|
|
1639
|
+
resolve(true);
|
|
1589
1640
|
}
|
|
1590
|
-
}
|
|
1641
|
+
};
|
|
1642
|
+
this.requestQueue.push(resolveCallback);
|
|
1643
|
+
timeoutInterval = setInterval(checkTimeout, 1e3);
|
|
1591
1644
|
});
|
|
1592
1645
|
if (!acquired) {
|
|
1593
1646
|
return false;
|
|
@@ -1595,24 +1648,25 @@ var TokenBucketRateLimiter = class {
|
|
|
1595
1648
|
}
|
|
1596
1649
|
this.processing = true;
|
|
1597
1650
|
try {
|
|
1598
|
-
this.refill();
|
|
1599
1651
|
if (this.currentBackoff > 0) {
|
|
1600
1652
|
this.logger.debug(`Applying backoff: ${this.currentBackoff}ms`);
|
|
1601
1653
|
await sleep(this.currentBackoff);
|
|
1602
1654
|
this.lastRefill = Date.now();
|
|
1603
|
-
this.currentBackoff = 0;
|
|
1604
1655
|
}
|
|
1656
|
+
this.refill();
|
|
1605
1657
|
while (this.tokens <= 0) {
|
|
1606
1658
|
if (Date.now() - startTime > timeoutMs) {
|
|
1607
1659
|
return false;
|
|
1608
1660
|
}
|
|
1609
|
-
|
|
1661
|
+
const waitTime = Math.min(100, this.config.minDelayMs);
|
|
1662
|
+
await sleep(waitTime);
|
|
1610
1663
|
this.refill();
|
|
1611
1664
|
}
|
|
1612
1665
|
this.tokens--;
|
|
1613
1666
|
const timeSinceLastRefill = Date.now() - this.lastRefill;
|
|
1614
1667
|
if (timeSinceLastRefill < this.config.minDelayMs) {
|
|
1615
|
-
|
|
1668
|
+
const remainingDelay = this.config.minDelayMs - timeSinceLastRefill;
|
|
1669
|
+
await sleep(remainingDelay);
|
|
1616
1670
|
}
|
|
1617
1671
|
this.lastRefill = Date.now();
|
|
1618
1672
|
return true;
|
|
@@ -1630,10 +1684,21 @@ var TokenBucketRateLimiter = class {
|
|
|
1630
1684
|
release() {
|
|
1631
1685
|
}
|
|
1632
1686
|
/**
|
|
1633
|
-
* 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.
|
|
1634
1691
|
*/
|
|
1635
1692
|
reportSuccess() {
|
|
1636
|
-
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
|
+
}
|
|
1637
1702
|
this.consecutiveErrors = 0;
|
|
1638
1703
|
}
|
|
1639
1704
|
/**
|
|
@@ -1824,7 +1889,9 @@ var DEFAULT_CONFIG2 = {
|
|
|
1824
1889
|
latencyWeight: 0.4,
|
|
1825
1890
|
priorityWeight: 0.3,
|
|
1826
1891
|
freshnessWeight: 0.3,
|
|
1827
|
-
minStatus: ["available", "degraded"]
|
|
1892
|
+
minStatus: ["available", "degraded"],
|
|
1893
|
+
retryCooldownMs: 3e4
|
|
1894
|
+
// 30 seconds
|
|
1828
1895
|
};
|
|
1829
1896
|
var ProviderSelector = class {
|
|
1830
1897
|
constructor(registry, healthChecker, config, logger, adapter = "node") {
|
|
@@ -1918,14 +1985,19 @@ var ProviderSelector = class {
|
|
|
1918
1985
|
return defaultProvider;
|
|
1919
1986
|
}
|
|
1920
1987
|
if (health.success === false && health.lastTested) {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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}`
|
|
1926
2000
|
);
|
|
1927
|
-
this.activeProviderByNetwork.set(network, defaultProvider.id);
|
|
1928
|
-
return defaultProvider;
|
|
1929
2001
|
}
|
|
1930
2002
|
}
|
|
1931
2003
|
}
|
|
@@ -1939,14 +2011,19 @@ var ProviderSelector = class {
|
|
|
1939
2011
|
return provider;
|
|
1940
2012
|
}
|
|
1941
2013
|
if (health.success === false && health.lastTested) {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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}`
|
|
1947
2026
|
);
|
|
1948
|
-
this.activeProviderByNetwork.set(network, provider.id);
|
|
1949
|
-
return provider;
|
|
1950
2027
|
}
|
|
1951
2028
|
}
|
|
1952
2029
|
}
|
|
@@ -1961,6 +2038,12 @@ var ProviderSelector = class {
|
|
|
1961
2038
|
this.logger.debug(
|
|
1962
2039
|
`Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)})`
|
|
1963
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
|
+
);
|
|
1964
2047
|
} else {
|
|
1965
2048
|
this.activeProviderByNetwork.set(network, best.id);
|
|
1966
2049
|
this.logger.debug(
|
|
@@ -2011,11 +2094,12 @@ var ProviderSelector = class {
|
|
|
2011
2094
|
return 0.01 * (1 / (provider.priority + 1));
|
|
2012
2095
|
}
|
|
2013
2096
|
if (health.success === false) {
|
|
2014
|
-
if (health.
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
+
}
|
|
2019
2103
|
}
|
|
2020
2104
|
}
|
|
2021
2105
|
return 0;
|
|
@@ -2210,6 +2294,34 @@ var ProviderSelector = class {
|
|
|
2210
2294
|
return true;
|
|
2211
2295
|
});
|
|
2212
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
|
+
}
|
|
2213
2325
|
};
|
|
2214
2326
|
function createSelector(registry, healthChecker, config, logger, adapter = "node") {
|
|
2215
2327
|
return new ProviderSelector(registry, healthChecker, config, logger, adapter);
|
|
@@ -2633,10 +2745,17 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2633
2745
|
}
|
|
2634
2746
|
const acquired = await this.rateLimiter.acquire(provider.id, timeoutMs);
|
|
2635
2747
|
if (!acquired) {
|
|
2636
|
-
|
|
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);
|
|
2637
2751
|
const next = this.selector.getNextProvider(this.network, [provider.id]);
|
|
2638
2752
|
if (next) {
|
|
2639
|
-
|
|
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`);
|
|
2640
2759
|
}
|
|
2641
2760
|
return this.getFallbackEndpoint();
|
|
2642
2761
|
}
|
|
@@ -2686,26 +2805,48 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2686
2805
|
if (!provider) {
|
|
2687
2806
|
provider = this.selector.getBestProvider(this.network);
|
|
2688
2807
|
}
|
|
2689
|
-
if (!provider)
|
|
2808
|
+
if (!provider) {
|
|
2809
|
+
this.options.logger.warn(`Cannot report error: no provider available for ${this.network}`);
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2690
2812
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2691
2813
|
const errorMsgLower = errorMsg.toLowerCase();
|
|
2692
|
-
const
|
|
2693
|
-
const
|
|
2694
|
-
const
|
|
2695
|
-
const
|
|
2696
|
-
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");
|
|
2697
2825
|
if (isRateLimitError(error) || is429) {
|
|
2698
2826
|
this.rateLimiter.reportRateLimitError(provider.id);
|
|
2699
|
-
this.healthChecker.markDegraded(provider.id, this.network,
|
|
2700
|
-
|
|
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) {
|
|
2701
2834
|
this.rateLimiter.reportError(provider.id);
|
|
2702
|
-
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`);
|
|
2703
2838
|
} else {
|
|
2704
2839
|
this.rateLimiter.reportError(provider.id);
|
|
2705
|
-
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}`);
|
|
2706
2848
|
}
|
|
2707
2849
|
this.selector.clearCache(this.network);
|
|
2708
|
-
this.selector.handleProviderFailure(provider.id, this.network);
|
|
2709
2850
|
this.notifyListeners();
|
|
2710
2851
|
}
|
|
2711
2852
|
// ========================================================================
|
|
@@ -3256,9 +3397,11 @@ var BrowserAdapter = class {
|
|
|
3256
3397
|
// ========================================================================
|
|
3257
3398
|
/**
|
|
3258
3399
|
* Make a JSON-RPC call to the TON API
|
|
3400
|
+
*
|
|
3401
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3259
3402
|
*/
|
|
3260
3403
|
async jsonRpc(method, params = {}, timeoutMs = 1e4) {
|
|
3261
|
-
const endpoint = await this.manager.
|
|
3404
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3262
3405
|
const controller = new AbortController();
|
|
3263
3406
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3264
3407
|
try {
|
|
@@ -3297,9 +3440,11 @@ var BrowserAdapter = class {
|
|
|
3297
3440
|
// ========================================================================
|
|
3298
3441
|
/**
|
|
3299
3442
|
* Get address state
|
|
3443
|
+
*
|
|
3444
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3300
3445
|
*/
|
|
3301
3446
|
async getAddressState(address, timeoutMs = 1e4) {
|
|
3302
|
-
const endpoint = await this.manager.
|
|
3447
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3303
3448
|
const baseV2 = toV2Base(endpoint);
|
|
3304
3449
|
const url = `${baseV2}/getAddressState?address=${encodeURIComponent(address)}`;
|
|
3305
3450
|
const controller = new AbortController();
|
|
@@ -3329,9 +3474,11 @@ var BrowserAdapter = class {
|
|
|
3329
3474
|
}
|
|
3330
3475
|
/**
|
|
3331
3476
|
* Get address balance
|
|
3477
|
+
*
|
|
3478
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3332
3479
|
*/
|
|
3333
3480
|
async getAddressBalance(address, timeoutMs = 1e4) {
|
|
3334
|
-
const endpoint = await this.manager.
|
|
3481
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3335
3482
|
const baseV2 = toV2Base(endpoint);
|
|
3336
3483
|
const url = `${baseV2}/getAddressBalance?address=${encodeURIComponent(address)}`;
|
|
3337
3484
|
const controller = new AbortController();
|
|
@@ -3361,9 +3508,11 @@ var BrowserAdapter = class {
|
|
|
3361
3508
|
}
|
|
3362
3509
|
/**
|
|
3363
3510
|
* Get address information
|
|
3511
|
+
*
|
|
3512
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3364
3513
|
*/
|
|
3365
3514
|
async getAddressInfo(address, timeoutMs = 1e4) {
|
|
3366
|
-
const endpoint = await this.manager.
|
|
3515
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3367
3516
|
const baseV2 = toV2Base(endpoint);
|
|
3368
3517
|
const url = `${baseV2}/getAddressInformation?address=${encodeURIComponent(address)}`;
|
|
3369
3518
|
const controller = new AbortController();
|
|
@@ -3391,9 +3540,11 @@ var BrowserAdapter = class {
|
|
|
3391
3540
|
}
|
|
3392
3541
|
/**
|
|
3393
3542
|
* Run get method
|
|
3543
|
+
*
|
|
3544
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3394
3545
|
*/
|
|
3395
3546
|
async runGetMethod(address, method, stack = [], timeoutMs = 15e3) {
|
|
3396
|
-
const endpoint = await this.manager.
|
|
3547
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3397
3548
|
const baseV2 = toV2Base(endpoint);
|
|
3398
3549
|
const url = `${baseV2}/runGetMethod`;
|
|
3399
3550
|
const controller = new AbortController();
|