ton-provider-system 0.2.2 → 0.3.0
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 +480 -264
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -2
- package/dist/index.d.ts +59 -2
- package/dist/index.js +480 -265
- package/dist/index.js.map +1 -1
- package/package.json +17 -19
package/dist/index.js
CHANGED
|
@@ -1035,6 +1035,238 @@ function createProvider(provider) {
|
|
|
1035
1035
|
}
|
|
1036
1036
|
}
|
|
1037
1037
|
|
|
1038
|
+
// src/utils/endpoint.ts
|
|
1039
|
+
function normalizeV2Endpoint(endpoint, provider) {
|
|
1040
|
+
if (provider) {
|
|
1041
|
+
const providerImpl = createProvider(provider);
|
|
1042
|
+
return providerImpl.normalizeEndpoint(endpoint);
|
|
1043
|
+
}
|
|
1044
|
+
return normalizeV2EndpointFallback(endpoint);
|
|
1045
|
+
}
|
|
1046
|
+
function normalizeV2EndpointFallback(endpoint) {
|
|
1047
|
+
let normalized = endpoint.trim();
|
|
1048
|
+
if (normalized.endsWith("/")) {
|
|
1049
|
+
normalized = normalized.slice(0, -1);
|
|
1050
|
+
}
|
|
1051
|
+
if (normalized.toLowerCase().endsWith("/jsonrpc")) {
|
|
1052
|
+
return normalized;
|
|
1053
|
+
}
|
|
1054
|
+
if (normalized.includes("gateway.tatum.io")) {
|
|
1055
|
+
try {
|
|
1056
|
+
const url = new URL(normalized);
|
|
1057
|
+
if (!url.pathname || url.pathname === "/") {
|
|
1058
|
+
return normalized + "/jsonRPC";
|
|
1059
|
+
}
|
|
1060
|
+
if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
|
|
1061
|
+
return normalized + "/jsonRPC";
|
|
1062
|
+
}
|
|
1063
|
+
} catch {
|
|
1064
|
+
return normalized + "/jsonRPC";
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (normalized.includes("onfinality.io")) {
|
|
1068
|
+
try {
|
|
1069
|
+
const url = new URL(normalized);
|
|
1070
|
+
const baseUrl = normalized.split("?")[0];
|
|
1071
|
+
if (!url.pathname || url.pathname === "/") {
|
|
1072
|
+
const apikey = url.searchParams.get("apikey");
|
|
1073
|
+
if (apikey && apikey !== "{key}" && apikey.length > 0) {
|
|
1074
|
+
return baseUrl.replace(/\/?$/, "/rpc");
|
|
1075
|
+
}
|
|
1076
|
+
return baseUrl.replace(/\/?$/, "/public");
|
|
1077
|
+
}
|
|
1078
|
+
if (url.pathname === "/rpc" || url.pathname === "/public") {
|
|
1079
|
+
return baseUrl;
|
|
1080
|
+
}
|
|
1081
|
+
return baseUrl;
|
|
1082
|
+
} catch {
|
|
1083
|
+
if (normalized.includes("{key}")) {
|
|
1084
|
+
return normalized.split("?")[0].replace(/\/?$/, "/public");
|
|
1085
|
+
}
|
|
1086
|
+
if (!normalized.includes("/rpc") && !normalized.includes("/public")) {
|
|
1087
|
+
return normalized.split("?")[0] + "/public";
|
|
1088
|
+
}
|
|
1089
|
+
return normalized.split("?")[0];
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (normalized.endsWith("/api/v2")) {
|
|
1093
|
+
return normalized + "/jsonRPC";
|
|
1094
|
+
}
|
|
1095
|
+
if (normalized.endsWith("/api/v3")) {
|
|
1096
|
+
return normalized.replace("/api/v3", "/api/v2/jsonRPC");
|
|
1097
|
+
}
|
|
1098
|
+
if (normalized.includes("quiknode.pro") || normalized.includes("getblock.io")) {
|
|
1099
|
+
try {
|
|
1100
|
+
const url = new URL(normalized);
|
|
1101
|
+
if (!url.pathname || url.pathname === "/") {
|
|
1102
|
+
return normalized + "/jsonRPC";
|
|
1103
|
+
}
|
|
1104
|
+
if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
|
|
1105
|
+
return normalized + "/jsonRPC";
|
|
1106
|
+
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
return normalized + "/jsonRPC";
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
const url = new URL(normalized);
|
|
1113
|
+
if (!url.pathname || url.pathname === "/") {
|
|
1114
|
+
return normalized + "/jsonRPC";
|
|
1115
|
+
}
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
return normalized;
|
|
1119
|
+
}
|
|
1120
|
+
function toV2Base(endpoint) {
|
|
1121
|
+
let normalized = endpoint.trim();
|
|
1122
|
+
if (normalized.endsWith("/")) {
|
|
1123
|
+
normalized = normalized.slice(0, -1);
|
|
1124
|
+
}
|
|
1125
|
+
if (normalized.toLowerCase().endsWith("/jsonrpc")) {
|
|
1126
|
+
normalized = normalized.slice(0, -8);
|
|
1127
|
+
}
|
|
1128
|
+
normalized = normalized.replace(/\/api\/v3\b/, "/api/v2");
|
|
1129
|
+
if (!normalized.endsWith("/api/v2")) {
|
|
1130
|
+
if (normalized.includes("/api/v2")) {
|
|
1131
|
+
const idx = normalized.indexOf("/api/v2");
|
|
1132
|
+
normalized = normalized.slice(0, idx + 7);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return normalized;
|
|
1136
|
+
}
|
|
1137
|
+
function toV3Base(endpoint) {
|
|
1138
|
+
const normalized = toV2Base(endpoint);
|
|
1139
|
+
return normalized.replace("/api/v2", "/api/v3");
|
|
1140
|
+
}
|
|
1141
|
+
function getBaseUrl(endpoint) {
|
|
1142
|
+
try {
|
|
1143
|
+
const url = new URL(endpoint);
|
|
1144
|
+
return `${url.protocol}//${url.host}`;
|
|
1145
|
+
} catch {
|
|
1146
|
+
return endpoint;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function isChainstackUrl(url) {
|
|
1150
|
+
try {
|
|
1151
|
+
const parsed = new URL(url.trim());
|
|
1152
|
+
return parsed.hostname.includes("chainstack.com");
|
|
1153
|
+
} catch {
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
function isQuickNodeUrl(url) {
|
|
1158
|
+
try {
|
|
1159
|
+
const parsed = new URL(url.trim());
|
|
1160
|
+
return parsed.hostname.includes("quiknode.pro");
|
|
1161
|
+
} catch {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
function isTonCenterUrl(url) {
|
|
1166
|
+
try {
|
|
1167
|
+
const parsed = new URL(url.trim());
|
|
1168
|
+
return parsed.hostname.includes("toncenter.com");
|
|
1169
|
+
} catch {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function isOrbsUrl(url) {
|
|
1174
|
+
try {
|
|
1175
|
+
const parsed = new URL(url.trim());
|
|
1176
|
+
return parsed.hostname.includes("orbs.network") || parsed.hostname.includes("ton-access");
|
|
1177
|
+
} catch {
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function buildRestUrl(baseEndpoint, method) {
|
|
1182
|
+
const base = toV2Base(baseEndpoint);
|
|
1183
|
+
return `${base}/${method}`;
|
|
1184
|
+
}
|
|
1185
|
+
function buildGetAddressStateUrl(baseEndpoint, address) {
|
|
1186
|
+
const base = toV2Base(baseEndpoint);
|
|
1187
|
+
return `${base}/getAddressState?address=${encodeURIComponent(address)}`;
|
|
1188
|
+
}
|
|
1189
|
+
function buildGetAddressBalanceUrl(baseEndpoint, address) {
|
|
1190
|
+
const base = toV2Base(baseEndpoint);
|
|
1191
|
+
return `${base}/getAddressBalance?address=${encodeURIComponent(address)}`;
|
|
1192
|
+
}
|
|
1193
|
+
function buildGetAddressInfoUrl(baseEndpoint, address) {
|
|
1194
|
+
const base = toV2Base(baseEndpoint);
|
|
1195
|
+
return `${base}/getAddressInformation?address=${encodeURIComponent(address)}`;
|
|
1196
|
+
}
|
|
1197
|
+
function detectNetworkFromEndpoint(endpoint) {
|
|
1198
|
+
const lower = endpoint.toLowerCase();
|
|
1199
|
+
if (lower.includes("testnet") || lower.includes("test") || lower.includes("sandbox")) {
|
|
1200
|
+
return "testnet";
|
|
1201
|
+
}
|
|
1202
|
+
if (lower.includes("mainnet") || lower.includes("main") || // TonCenter mainnet doesn't have 'mainnet' in URL
|
|
1203
|
+
lower.includes("toncenter.com") && !lower.includes("testnet")) {
|
|
1204
|
+
return "mainnet";
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
function isValidHttpUrl(str) {
|
|
1209
|
+
try {
|
|
1210
|
+
const url = new URL(str);
|
|
1211
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
1212
|
+
} catch {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function isValidWsUrl(str) {
|
|
1217
|
+
try {
|
|
1218
|
+
const url = new URL(str);
|
|
1219
|
+
return url.protocol === "ws:" || url.protocol === "wss:";
|
|
1220
|
+
} catch {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function isCredentialLike(segment) {
|
|
1225
|
+
return /^[0-9a-f]{16,}$/i.test(segment) || // hex API keys (e.g. Chainstack/GetBlock)
|
|
1226
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment) || // UUID
|
|
1227
|
+
/^[A-Za-z0-9_-]{20,}$/.test(segment);
|
|
1228
|
+
}
|
|
1229
|
+
function redactUrl(url) {
|
|
1230
|
+
const MASK = "***";
|
|
1231
|
+
const SENSITIVE_PARAM = /^(apikey|api[-_]?key|key|token|secret|password)$/i;
|
|
1232
|
+
const STRUCTURAL = /* @__PURE__ */ new Set([
|
|
1233
|
+
"api",
|
|
1234
|
+
"v1",
|
|
1235
|
+
"v2",
|
|
1236
|
+
"v3",
|
|
1237
|
+
"v4",
|
|
1238
|
+
"jsonrpc",
|
|
1239
|
+
"rpc",
|
|
1240
|
+
"public",
|
|
1241
|
+
"ws",
|
|
1242
|
+
"testnet",
|
|
1243
|
+
"mainnet",
|
|
1244
|
+
"toncenter-api-v2"
|
|
1245
|
+
]);
|
|
1246
|
+
try {
|
|
1247
|
+
const u = new URL(url);
|
|
1248
|
+
for (const name of Array.from(u.searchParams.keys())) {
|
|
1249
|
+
if (SENSITIVE_PARAM.test(name)) {
|
|
1250
|
+
u.searchParams.set(name, MASK);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
u.pathname = u.pathname.split("/").map(
|
|
1254
|
+
(seg) => seg && !STRUCTURAL.has(seg.toLowerCase()) && isCredentialLike(seg) ? MASK : seg
|
|
1255
|
+
).join("/");
|
|
1256
|
+
const hostParts = u.hostname.split(".");
|
|
1257
|
+
if (hostParts.length > 2 && isCredentialLike(hostParts[0])) {
|
|
1258
|
+
hostParts[0] = MASK;
|
|
1259
|
+
u.hostname = hostParts.join(".");
|
|
1260
|
+
}
|
|
1261
|
+
return u.toString();
|
|
1262
|
+
} catch {
|
|
1263
|
+
return url.replace(
|
|
1264
|
+
/\b(apikey|api[-_]?key|key|token|secret|password)=[^&\s]+/gi,
|
|
1265
|
+
`$1=${MASK}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1038
1270
|
// src/core/healthChecker.ts
|
|
1039
1271
|
var consoleLogger2 = {
|
|
1040
1272
|
debug: (msg, data) => console.debug(`[HealthChecker] ${msg}`, data || ""),
|
|
@@ -1051,6 +1283,8 @@ var HealthChecker = class {
|
|
|
1051
1283
|
constructor(config, logger, rateLimiter) {
|
|
1052
1284
|
this.results = /* @__PURE__ */ new Map();
|
|
1053
1285
|
this.highestSeqno = /* @__PURE__ */ new Map();
|
|
1286
|
+
/** Rolling window of recent valid seqnos per network (for outlier-robust blocksBehind). */
|
|
1287
|
+
this.seqnoSamples = /* @__PURE__ */ new Map();
|
|
1054
1288
|
this.rateLimiter = null;
|
|
1055
1289
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1056
1290
|
this.logger = logger || consoleLogger2;
|
|
@@ -1096,22 +1330,36 @@ var HealthChecker = class {
|
|
|
1096
1330
|
if (!validation.valid) {
|
|
1097
1331
|
throw new Error(validation.error || "Provider configuration invalid");
|
|
1098
1332
|
}
|
|
1099
|
-
|
|
1333
|
+
let normalizedEndpoint = providerImpl.normalizeEndpoint(endpoint);
|
|
1100
1334
|
if (provider.type === "onfinality") {
|
|
1101
|
-
this.logger.debug(`OnFinality endpoint: ${endpoint} -> ${normalizedEndpoint}, API key: ${provider.apiKey ? "set" : "not set"}`);
|
|
1335
|
+
this.logger.debug(`OnFinality endpoint: ${redactUrl(endpoint)} -> ${redactUrl(normalizedEndpoint)}, API key: ${provider.apiKey ? "set" : "not set"}`);
|
|
1102
1336
|
}
|
|
1337
|
+
const orbsRetries = provider.isDynamic && provider.type === "orbs" ? 2 : 0;
|
|
1103
1338
|
let info;
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1339
|
+
for (let attempt = 0; ; attempt++) {
|
|
1340
|
+
try {
|
|
1341
|
+
info = await this.callGetMasterchainInfo(normalizedEndpoint, provider, providerImpl);
|
|
1342
|
+
break;
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (provider.type === "onfinality" && normalizedEndpoint.includes("/rpc") && provider.apiKey && error.message?.includes("backend error")) {
|
|
1345
|
+
this.logger.debug(`OnFinality /rpc failed, retrying with /public endpoint`);
|
|
1346
|
+
const baseUrl = normalizedEndpoint.split("?")[0];
|
|
1347
|
+
const publicEndpoint = baseUrl.replace("/rpc", "/public");
|
|
1348
|
+
const publicProvider = { ...provider, apiKey: void 0 };
|
|
1349
|
+
const publicProviderImpl = createProvider(publicProvider);
|
|
1350
|
+
info = await this.callGetMasterchainInfo(publicEndpoint, publicProvider, publicProviderImpl);
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
if (attempt < orbsRetries && this.isNonJsonResponseError(error)) {
|
|
1354
|
+
this.logger.debug(
|
|
1355
|
+
`Orbs gateway returned a non-JSON response; re-discovering endpoint (retry ${attempt + 1}/${orbsRetries})`
|
|
1356
|
+
);
|
|
1357
|
+
const freshEndpoint = await this.getEndpoint(provider);
|
|
1358
|
+
if (freshEndpoint) {
|
|
1359
|
+
normalizedEndpoint = providerImpl.normalizeEndpoint(freshEndpoint);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1115
1363
|
throw error;
|
|
1116
1364
|
}
|
|
1117
1365
|
}
|
|
@@ -1126,7 +1374,8 @@ var HealthChecker = class {
|
|
|
1126
1374
|
if (seqno > currentHighest) {
|
|
1127
1375
|
this.highestSeqno.set(provider.network, seqno);
|
|
1128
1376
|
}
|
|
1129
|
-
const
|
|
1377
|
+
const baselineSeqno = this.recordSeqnoAndGetBaseline(provider.network, seqno);
|
|
1378
|
+
const blocksBehind = Math.max(0, baselineSeqno - seqno);
|
|
1130
1379
|
let status = "available";
|
|
1131
1380
|
if (blocksBehind > this.config.maxBlocksBehind) {
|
|
1132
1381
|
status = "stale";
|
|
@@ -1154,17 +1403,26 @@ var HealthChecker = class {
|
|
|
1154
1403
|
const endTime = performance.now();
|
|
1155
1404
|
const latencyMs = Math.round(endTime - startTime);
|
|
1156
1405
|
const errorMsg = error.message || String(error) || "Unknown error";
|
|
1406
|
+
const errorMsgLower = errorMsg.toLowerCase();
|
|
1157
1407
|
const isCorsError = this.isCorsError(error, errorMsg);
|
|
1158
|
-
const
|
|
1159
|
-
const
|
|
1160
|
-
const
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
const
|
|
1408
|
+
const responseStatus = error?.response?.status || error?.status || error?.statusCode || null;
|
|
1409
|
+
const statusMatch = errorMsg.match(/\b(\d{3})\b/);
|
|
1410
|
+
const statusFromMsg = statusMatch ? parseInt(statusMatch[1], 10) : null;
|
|
1411
|
+
const httpStatus = responseStatus || statusFromMsg;
|
|
1412
|
+
const is429 = httpStatus === 429 || errorMsgLower.includes("429") || errorMsgLower.includes("rate limit") || errorMsgLower.includes("too many requests");
|
|
1413
|
+
const is404 = httpStatus === 404 || errorMsgLower.includes("404") || errorMsgLower.includes("not found");
|
|
1414
|
+
const is401 = httpStatus === 401 || errorMsgLower.includes("401") || errorMsgLower.includes("unauthorized") || errorMsgLower.includes("invalid api key") || errorMsgLower.includes("authentication failed");
|
|
1415
|
+
const is403 = httpStatus === 403 || errorMsgLower.includes("403") || errorMsgLower.includes("forbidden");
|
|
1416
|
+
const is503 = httpStatus === 503 || errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
|
|
1417
|
+
const is502 = httpStatus === 502 || errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
|
|
1418
|
+
const isTimeout = error.name === "AbortError" || errorMsgLower.includes("timeout") || errorMsgLower.includes("timed out") || errorMsgLower.includes("aborted");
|
|
1419
|
+
const isOnFinalityBackendError = provider.type === "onfinality" && (errorMsgLower.includes("backend error") || errorMsgLower.includes("backend error"));
|
|
1164
1420
|
let status = "offline";
|
|
1165
1421
|
if (is429) {
|
|
1166
1422
|
status = "degraded";
|
|
1167
|
-
} else if (is404 ||
|
|
1423
|
+
} else if (is404 || is401 || is403) {
|
|
1424
|
+
status = "offline";
|
|
1425
|
+
} else if (is503 || is502 || isOnFinalityBackendError) {
|
|
1168
1426
|
status = "offline";
|
|
1169
1427
|
} else if (isTimeout) {
|
|
1170
1428
|
status = "offline";
|
|
@@ -1192,8 +1450,9 @@ var HealthChecker = class {
|
|
|
1192
1450
|
*
|
|
1193
1451
|
* @param batchSize - Number of providers to test in parallel (default: 2)
|
|
1194
1452
|
* @param batchDelayMs - Delay between batches in milliseconds (default: 500 to avoid rate limits)
|
|
1453
|
+
* If not provided, calculates delay based on lowest RPS in batch
|
|
1195
1454
|
*/
|
|
1196
|
-
async testProviders(providers, batchSize = 2, batchDelayMs
|
|
1455
|
+
async testProviders(providers, batchSize = 2, batchDelayMs) {
|
|
1197
1456
|
const results = [];
|
|
1198
1457
|
for (let i = 0; i < providers.length; i += batchSize) {
|
|
1199
1458
|
const batch = providers.slice(i, i + batchSize);
|
|
@@ -1201,8 +1460,15 @@ var HealthChecker = class {
|
|
|
1201
1460
|
batch.map((p) => this.testProvider(p))
|
|
1202
1461
|
);
|
|
1203
1462
|
results.push(...batchResults);
|
|
1204
|
-
if (i + batchSize < providers.length
|
|
1205
|
-
|
|
1463
|
+
if (i + batchSize < providers.length) {
|
|
1464
|
+
let delay = batchDelayMs;
|
|
1465
|
+
if (delay === void 0) {
|
|
1466
|
+
const minRps = Math.min(...batch.map((p) => p.rps || 1));
|
|
1467
|
+
delay = Math.max(500, Math.ceil(1e3 / minRps * 1.5));
|
|
1468
|
+
}
|
|
1469
|
+
if (delay > 0) {
|
|
1470
|
+
await this.sleep(delay);
|
|
1471
|
+
}
|
|
1206
1472
|
}
|
|
1207
1473
|
}
|
|
1208
1474
|
return results;
|
|
@@ -1253,6 +1519,7 @@ var HealthChecker = class {
|
|
|
1253
1519
|
clearResults() {
|
|
1254
1520
|
this.results.clear();
|
|
1255
1521
|
this.highestSeqno.clear();
|
|
1522
|
+
this.seqnoSamples.clear();
|
|
1256
1523
|
}
|
|
1257
1524
|
/**
|
|
1258
1525
|
* Mark a provider as degraded (e.g., on 429 error)
|
|
@@ -1322,6 +1589,31 @@ var HealthChecker = class {
|
|
|
1322
1589
|
getResultKey(providerId, network) {
|
|
1323
1590
|
return `${providerId}-${network}`;
|
|
1324
1591
|
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Record a valid seqno sample and return an outlier-robust freshness baseline
|
|
1594
|
+
* (median of recent samples for the network). Using the median instead of the
|
|
1595
|
+
* raw maximum prevents a single provider that reports an inflated seqno from
|
|
1596
|
+
* poisoning `blocksBehind` for every other (healthy) provider. See P2-2.
|
|
1597
|
+
*/
|
|
1598
|
+
recordSeqnoAndGetBaseline(network, seqno) {
|
|
1599
|
+
const samples = this.seqnoSamples.get(network) ?? [];
|
|
1600
|
+
samples.push(seqno);
|
|
1601
|
+
if (samples.length > 20) {
|
|
1602
|
+
samples.shift();
|
|
1603
|
+
}
|
|
1604
|
+
this.seqnoSamples.set(network, samples);
|
|
1605
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
1606
|
+
const mid = Math.floor((sorted.length - 1) / 2);
|
|
1607
|
+
return sorted[mid];
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Detect a "non-JSON response" error (e.g. an HTML error page returned by a
|
|
1611
|
+
* misbehaving gateway), used to decide whether to re-discover an Orbs endpoint.
|
|
1612
|
+
*/
|
|
1613
|
+
isNonJsonResponseError(error) {
|
|
1614
|
+
const msg = (error?.message || String(error) || "").toLowerCase();
|
|
1615
|
+
return msg.includes("invalid response type") || msg.includes("expected json") || msg.includes("got text/html");
|
|
1616
|
+
}
|
|
1325
1617
|
/**
|
|
1326
1618
|
* Get endpoint URL for a provider (handles dynamic providers like Orbs)
|
|
1327
1619
|
*/
|
|
@@ -1591,24 +1883,36 @@ var TokenBucketRateLimiter = class {
|
|
|
1591
1883
|
const startTime = Date.now();
|
|
1592
1884
|
if (this.processing) {
|
|
1593
1885
|
const acquired = await new Promise((resolve) => {
|
|
1886
|
+
let timeoutInterval = null;
|
|
1887
|
+
let resolved = false;
|
|
1888
|
+
const cleanup = () => {
|
|
1889
|
+
if (timeoutInterval !== null) {
|
|
1890
|
+
clearInterval(timeoutInterval);
|
|
1891
|
+
timeoutInterval = null;
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1594
1894
|
const checkTimeout = () => {
|
|
1595
1895
|
if (Date.now() - startTime > timeoutMs) {
|
|
1596
1896
|
const idx = this.requestQueue.indexOf(resolveCallback);
|
|
1597
1897
|
if (idx >= 0) {
|
|
1598
1898
|
this.requestQueue.splice(idx, 1);
|
|
1599
1899
|
}
|
|
1600
|
-
|
|
1900
|
+
if (!resolved) {
|
|
1901
|
+
resolved = true;
|
|
1902
|
+
cleanup();
|
|
1903
|
+
resolve(false);
|
|
1904
|
+
}
|
|
1601
1905
|
}
|
|
1602
1906
|
};
|
|
1603
|
-
const resolveCallback = () =>
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
const cleanup = () => clearInterval(timeoutInterval);
|
|
1607
|
-
Promise.resolve().then(() => {
|
|
1608
|
-
if (this.requestQueue.includes(resolveCallback)) ; else {
|
|
1907
|
+
const resolveCallback = () => {
|
|
1908
|
+
if (!resolved) {
|
|
1909
|
+
resolved = true;
|
|
1609
1910
|
cleanup();
|
|
1911
|
+
resolve(true);
|
|
1610
1912
|
}
|
|
1611
|
-
}
|
|
1913
|
+
};
|
|
1914
|
+
this.requestQueue.push(resolveCallback);
|
|
1915
|
+
timeoutInterval = setInterval(checkTimeout, 1e3);
|
|
1612
1916
|
});
|
|
1613
1917
|
if (!acquired) {
|
|
1614
1918
|
return false;
|
|
@@ -1616,24 +1920,25 @@ var TokenBucketRateLimiter = class {
|
|
|
1616
1920
|
}
|
|
1617
1921
|
this.processing = true;
|
|
1618
1922
|
try {
|
|
1619
|
-
this.refill();
|
|
1620
1923
|
if (this.currentBackoff > 0) {
|
|
1621
1924
|
this.logger.debug(`Applying backoff: ${this.currentBackoff}ms`);
|
|
1622
1925
|
await sleep(this.currentBackoff);
|
|
1623
1926
|
this.lastRefill = Date.now();
|
|
1624
|
-
this.currentBackoff = 0;
|
|
1625
1927
|
}
|
|
1928
|
+
this.refill();
|
|
1626
1929
|
while (this.tokens <= 0) {
|
|
1627
1930
|
if (Date.now() - startTime > timeoutMs) {
|
|
1628
1931
|
return false;
|
|
1629
1932
|
}
|
|
1630
|
-
|
|
1933
|
+
const waitTime = Math.min(100, this.config.minDelayMs);
|
|
1934
|
+
await sleep(waitTime);
|
|
1631
1935
|
this.refill();
|
|
1632
1936
|
}
|
|
1633
1937
|
this.tokens--;
|
|
1634
1938
|
const timeSinceLastRefill = Date.now() - this.lastRefill;
|
|
1635
1939
|
if (timeSinceLastRefill < this.config.minDelayMs) {
|
|
1636
|
-
|
|
1940
|
+
const remainingDelay = this.config.minDelayMs - timeSinceLastRefill;
|
|
1941
|
+
await sleep(remainingDelay);
|
|
1637
1942
|
}
|
|
1638
1943
|
this.lastRefill = Date.now();
|
|
1639
1944
|
return true;
|
|
@@ -1651,10 +1956,21 @@ var TokenBucketRateLimiter = class {
|
|
|
1651
1956
|
release() {
|
|
1652
1957
|
}
|
|
1653
1958
|
/**
|
|
1654
|
-
* Report a successful request (
|
|
1959
|
+
* Report a successful request (gradually reduces backoff)
|
|
1960
|
+
*
|
|
1961
|
+
* Instead of immediately clearing backoff, we gradually reduce it to prevent
|
|
1962
|
+
* immediately hitting the rate limit again after a single success.
|
|
1655
1963
|
*/
|
|
1656
1964
|
reportSuccess() {
|
|
1657
|
-
this.currentBackoff
|
|
1965
|
+
if (this.currentBackoff > 0) {
|
|
1966
|
+
this.currentBackoff = Math.max(
|
|
1967
|
+
this.config.minDelayMs,
|
|
1968
|
+
Math.floor(this.currentBackoff / 2)
|
|
1969
|
+
);
|
|
1970
|
+
if (this.currentBackoff <= this.config.minDelayMs) {
|
|
1971
|
+
this.currentBackoff = 0;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1658
1974
|
this.consecutiveErrors = 0;
|
|
1659
1975
|
}
|
|
1660
1976
|
/**
|
|
@@ -1845,7 +2161,9 @@ var DEFAULT_CONFIG2 = {
|
|
|
1845
2161
|
latencyWeight: 0.4,
|
|
1846
2162
|
priorityWeight: 0.3,
|
|
1847
2163
|
freshnessWeight: 0.3,
|
|
1848
|
-
minStatus: ["available", "degraded"]
|
|
2164
|
+
minStatus: ["available", "degraded"],
|
|
2165
|
+
retryCooldownMs: 3e4
|
|
2166
|
+
// 30 seconds
|
|
1849
2167
|
};
|
|
1850
2168
|
var ProviderSelector = class {
|
|
1851
2169
|
constructor(registry, healthChecker, config, logger, adapter = "node") {
|
|
@@ -1939,14 +2257,19 @@ var ProviderSelector = class {
|
|
|
1939
2257
|
return defaultProvider;
|
|
1940
2258
|
}
|
|
1941
2259
|
if (health.success === false && health.lastTested) {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2260
|
+
if (this.isRetryableError(health.error)) {
|
|
2261
|
+
const timeSinceFailure = Date.now() - health.lastTested.getTime();
|
|
2262
|
+
if (timeSinceFailure > this.config.retryCooldownMs) {
|
|
2263
|
+
this.logger.warn(
|
|
2264
|
+
`No healthy providers for ${network}, retrying failed default after cooldown: ${defaultProvider.id}`
|
|
2265
|
+
);
|
|
2266
|
+
this.activeProviderByNetwork.set(network, defaultProvider.id);
|
|
2267
|
+
return defaultProvider;
|
|
2268
|
+
}
|
|
2269
|
+
} else {
|
|
2270
|
+
this.logger.debug(
|
|
2271
|
+
`Skipping provider ${defaultProvider.id} with permanent error: ${health.error}`
|
|
1947
2272
|
);
|
|
1948
|
-
this.activeProviderByNetwork.set(network, defaultProvider.id);
|
|
1949
|
-
return defaultProvider;
|
|
1950
2273
|
}
|
|
1951
2274
|
}
|
|
1952
2275
|
}
|
|
@@ -1960,14 +2283,19 @@ var ProviderSelector = class {
|
|
|
1960
2283
|
return provider;
|
|
1961
2284
|
}
|
|
1962
2285
|
if (health.success === false && health.lastTested) {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2286
|
+
if (this.isRetryableError(health.error)) {
|
|
2287
|
+
const timeSinceFailure = Date.now() - health.lastTested.getTime();
|
|
2288
|
+
if (timeSinceFailure > this.config.retryCooldownMs) {
|
|
2289
|
+
this.logger.warn(
|
|
2290
|
+
`No healthy providers for ${network}, retrying failed provider after cooldown: ${provider.id}`
|
|
2291
|
+
);
|
|
2292
|
+
this.activeProviderByNetwork.set(network, provider.id);
|
|
2293
|
+
return provider;
|
|
2294
|
+
}
|
|
2295
|
+
} else {
|
|
2296
|
+
this.logger.debug(
|
|
2297
|
+
`Skipping provider ${provider.id} with permanent error: ${health.error}`
|
|
1968
2298
|
);
|
|
1969
|
-
this.activeProviderByNetwork.set(network, provider.id);
|
|
1970
|
-
return provider;
|
|
1971
2299
|
}
|
|
1972
2300
|
}
|
|
1973
2301
|
}
|
|
@@ -1982,6 +2310,12 @@ var ProviderSelector = class {
|
|
|
1982
2310
|
this.logger.debug(
|
|
1983
2311
|
`Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)})`
|
|
1984
2312
|
);
|
|
2313
|
+
} else if (bestHealth && bestHealth.success === false) {
|
|
2314
|
+
this.bestProviderByNetwork.delete(network);
|
|
2315
|
+
this.activeProviderByNetwork.set(network, best.id);
|
|
2316
|
+
this.logger.debug(
|
|
2317
|
+
`Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)}, failed - not cached)`
|
|
2318
|
+
);
|
|
1985
2319
|
} else {
|
|
1986
2320
|
this.activeProviderByNetwork.set(network, best.id);
|
|
1987
2321
|
this.logger.debug(
|
|
@@ -2032,11 +2366,12 @@ var ProviderSelector = class {
|
|
|
2032
2366
|
return 0.01 * (1 / (provider.priority + 1));
|
|
2033
2367
|
}
|
|
2034
2368
|
if (health.success === false) {
|
|
2035
|
-
if (health.
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2369
|
+
if (this.isRetryableError(health.error)) {
|
|
2370
|
+
if (health.lastTested) {
|
|
2371
|
+
const timeSinceFailure = Date.now() - health.lastTested.getTime();
|
|
2372
|
+
if (timeSinceFailure > this.config.retryCooldownMs) {
|
|
2373
|
+
return 1e-3 * (1 / (provider.priority + 1));
|
|
2374
|
+
}
|
|
2040
2375
|
}
|
|
2041
2376
|
}
|
|
2042
2377
|
return 0;
|
|
@@ -2120,7 +2455,7 @@ var ProviderSelector = class {
|
|
|
2120
2455
|
*/
|
|
2121
2456
|
setCustomEndpoint(endpoint) {
|
|
2122
2457
|
this.customEndpoint = endpoint?.trim() || null;
|
|
2123
|
-
this.logger.info(`Custom endpoint: ${this.customEndpoint
|
|
2458
|
+
this.logger.info(`Custom endpoint: ${this.customEndpoint ? redactUrl(this.customEndpoint) : "(none)"}`);
|
|
2124
2459
|
}
|
|
2125
2460
|
/**
|
|
2126
2461
|
* Get custom endpoint
|
|
@@ -2231,196 +2566,37 @@ var ProviderSelector = class {
|
|
|
2231
2566
|
return true;
|
|
2232
2567
|
});
|
|
2233
2568
|
}
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
if (normalized.toLowerCase().endsWith("/jsonrpc")) {
|
|
2253
|
-
return normalized;
|
|
2254
|
-
}
|
|
2255
|
-
if (normalized.includes("gateway.tatum.io")) {
|
|
2256
|
-
try {
|
|
2257
|
-
const url = new URL(normalized);
|
|
2258
|
-
if (!url.pathname || url.pathname === "/") {
|
|
2259
|
-
return normalized + "/jsonRPC";
|
|
2260
|
-
}
|
|
2261
|
-
if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
|
|
2262
|
-
return normalized + "/jsonRPC";
|
|
2263
|
-
}
|
|
2264
|
-
} catch {
|
|
2265
|
-
return normalized + "/jsonRPC";
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
if (normalized.includes("onfinality.io")) {
|
|
2269
|
-
try {
|
|
2270
|
-
const url = new URL(normalized);
|
|
2271
|
-
const baseUrl = normalized.split("?")[0];
|
|
2272
|
-
if (!url.pathname || url.pathname === "/") {
|
|
2273
|
-
const apikey = url.searchParams.get("apikey");
|
|
2274
|
-
if (apikey && apikey !== "{key}" && apikey.length > 0) {
|
|
2275
|
-
return baseUrl.replace(/\/?$/, "/rpc");
|
|
2276
|
-
}
|
|
2277
|
-
return baseUrl.replace(/\/?$/, "/public");
|
|
2278
|
-
}
|
|
2279
|
-
if (url.pathname === "/rpc" || url.pathname === "/public") {
|
|
2280
|
-
return baseUrl;
|
|
2281
|
-
}
|
|
2282
|
-
return baseUrl;
|
|
2283
|
-
} catch {
|
|
2284
|
-
if (normalized.includes("{key}")) {
|
|
2285
|
-
return normalized.split("?")[0].replace(/\/?$/, "/public");
|
|
2286
|
-
}
|
|
2287
|
-
if (!normalized.includes("/rpc") && !normalized.includes("/public")) {
|
|
2288
|
-
return normalized.split("?")[0] + "/public";
|
|
2289
|
-
}
|
|
2290
|
-
return normalized.split("?")[0];
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
if (normalized.endsWith("/api/v2")) {
|
|
2294
|
-
return normalized + "/jsonRPC";
|
|
2295
|
-
}
|
|
2296
|
-
if (normalized.endsWith("/api/v3")) {
|
|
2297
|
-
return normalized.replace("/api/v3", "/api/v2/jsonRPC");
|
|
2298
|
-
}
|
|
2299
|
-
if (normalized.includes("quiknode.pro") || normalized.includes("getblock.io")) {
|
|
2300
|
-
try {
|
|
2301
|
-
const url = new URL(normalized);
|
|
2302
|
-
if (!url.pathname || url.pathname === "/") {
|
|
2303
|
-
return normalized + "/jsonRPC";
|
|
2304
|
-
}
|
|
2305
|
-
if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
|
|
2306
|
-
return normalized + "/jsonRPC";
|
|
2307
|
-
}
|
|
2308
|
-
} catch {
|
|
2309
|
-
return normalized + "/jsonRPC";
|
|
2569
|
+
/**
|
|
2570
|
+
* Check if an error is retryable (temporary) or permanent
|
|
2571
|
+
*
|
|
2572
|
+
* Permanent errors (never retry):
|
|
2573
|
+
* - 404 (Not Found) - endpoint doesn't exist
|
|
2574
|
+
* - 401 (Unauthorized) - invalid API key
|
|
2575
|
+
* - Invalid API key errors
|
|
2576
|
+
*
|
|
2577
|
+
* Retryable errors (can retry after cooldown):
|
|
2578
|
+
* - 503 (Service Unavailable) - temporary server issue
|
|
2579
|
+
* - 502 (Bad Gateway) - temporary gateway issue
|
|
2580
|
+
* - Timeout errors - network issues
|
|
2581
|
+
* - Network errors - connection issues
|
|
2582
|
+
* - 429 (Rate Limit) - temporary rate limit (handled separately)
|
|
2583
|
+
*/
|
|
2584
|
+
isRetryableError(error) {
|
|
2585
|
+
if (!error) {
|
|
2586
|
+
return true;
|
|
2310
2587
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
if (!url.pathname || url.pathname === "/") {
|
|
2315
|
-
return normalized + "/jsonRPC";
|
|
2588
|
+
const errorLower = error.toLowerCase();
|
|
2589
|
+
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")) {
|
|
2590
|
+
return false;
|
|
2316
2591
|
}
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
return normalized;
|
|
2320
|
-
}
|
|
2321
|
-
function toV2Base(endpoint) {
|
|
2322
|
-
let normalized = endpoint.trim();
|
|
2323
|
-
if (normalized.endsWith("/")) {
|
|
2324
|
-
normalized = normalized.slice(0, -1);
|
|
2325
|
-
}
|
|
2326
|
-
if (normalized.toLowerCase().endsWith("/jsonrpc")) {
|
|
2327
|
-
normalized = normalized.slice(0, -8);
|
|
2328
|
-
}
|
|
2329
|
-
normalized = normalized.replace(/\/api\/v3\b/, "/api/v2");
|
|
2330
|
-
if (!normalized.endsWith("/api/v2")) {
|
|
2331
|
-
if (normalized.includes("/api/v2")) {
|
|
2332
|
-
const idx = normalized.indexOf("/api/v2");
|
|
2333
|
-
normalized = normalized.slice(0, idx + 7);
|
|
2592
|
+
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")) {
|
|
2593
|
+
return true;
|
|
2334
2594
|
}
|
|
2595
|
+
return true;
|
|
2335
2596
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
const normalized = toV2Base(endpoint);
|
|
2340
|
-
return normalized.replace("/api/v2", "/api/v3");
|
|
2341
|
-
}
|
|
2342
|
-
function getBaseUrl(endpoint) {
|
|
2343
|
-
try {
|
|
2344
|
-
const url = new URL(endpoint);
|
|
2345
|
-
return `${url.protocol}//${url.host}`;
|
|
2346
|
-
} catch {
|
|
2347
|
-
return endpoint;
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
function isChainstackUrl(url) {
|
|
2351
|
-
try {
|
|
2352
|
-
const parsed = new URL(url.trim());
|
|
2353
|
-
return parsed.hostname.includes("chainstack.com");
|
|
2354
|
-
} catch {
|
|
2355
|
-
return false;
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
function isQuickNodeUrl(url) {
|
|
2359
|
-
try {
|
|
2360
|
-
const parsed = new URL(url.trim());
|
|
2361
|
-
return parsed.hostname.includes("quiknode.pro");
|
|
2362
|
-
} catch {
|
|
2363
|
-
return false;
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
function isTonCenterUrl(url) {
|
|
2367
|
-
try {
|
|
2368
|
-
const parsed = new URL(url.trim());
|
|
2369
|
-
return parsed.hostname.includes("toncenter.com");
|
|
2370
|
-
} catch {
|
|
2371
|
-
return false;
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
function isOrbsUrl(url) {
|
|
2375
|
-
try {
|
|
2376
|
-
const parsed = new URL(url.trim());
|
|
2377
|
-
return parsed.hostname.includes("orbs.network") || parsed.hostname.includes("ton-access");
|
|
2378
|
-
} catch {
|
|
2379
|
-
return false;
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
function buildRestUrl(baseEndpoint, method) {
|
|
2383
|
-
const base = toV2Base(baseEndpoint);
|
|
2384
|
-
return `${base}/${method}`;
|
|
2385
|
-
}
|
|
2386
|
-
function buildGetAddressStateUrl(baseEndpoint, address) {
|
|
2387
|
-
const base = toV2Base(baseEndpoint);
|
|
2388
|
-
return `${base}/getAddressState?address=${encodeURIComponent(address)}`;
|
|
2389
|
-
}
|
|
2390
|
-
function buildGetAddressBalanceUrl(baseEndpoint, address) {
|
|
2391
|
-
const base = toV2Base(baseEndpoint);
|
|
2392
|
-
return `${base}/getAddressBalance?address=${encodeURIComponent(address)}`;
|
|
2393
|
-
}
|
|
2394
|
-
function buildGetAddressInfoUrl(baseEndpoint, address) {
|
|
2395
|
-
const base = toV2Base(baseEndpoint);
|
|
2396
|
-
return `${base}/getAddressInformation?address=${encodeURIComponent(address)}`;
|
|
2397
|
-
}
|
|
2398
|
-
function detectNetworkFromEndpoint(endpoint) {
|
|
2399
|
-
const lower = endpoint.toLowerCase();
|
|
2400
|
-
if (lower.includes("testnet") || lower.includes("test") || lower.includes("sandbox")) {
|
|
2401
|
-
return "testnet";
|
|
2402
|
-
}
|
|
2403
|
-
if (lower.includes("mainnet") || lower.includes("main") || // TonCenter mainnet doesn't have 'mainnet' in URL
|
|
2404
|
-
lower.includes("toncenter.com") && !lower.includes("testnet")) {
|
|
2405
|
-
return "mainnet";
|
|
2406
|
-
}
|
|
2407
|
-
return null;
|
|
2408
|
-
}
|
|
2409
|
-
function isValidHttpUrl(str) {
|
|
2410
|
-
try {
|
|
2411
|
-
const url = new URL(str);
|
|
2412
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
2413
|
-
} catch {
|
|
2414
|
-
return false;
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
function isValidWsUrl(str) {
|
|
2418
|
-
try {
|
|
2419
|
-
const url = new URL(str);
|
|
2420
|
-
return url.protocol === "ws:" || url.protocol === "wss:";
|
|
2421
|
-
} catch {
|
|
2422
|
-
return false;
|
|
2423
|
-
}
|
|
2597
|
+
};
|
|
2598
|
+
function createSelector(registry, healthChecker, config, logger, adapter = "node") {
|
|
2599
|
+
return new ProviderSelector(registry, healthChecker, config, logger, adapter);
|
|
2424
2600
|
}
|
|
2425
2601
|
|
|
2426
2602
|
// src/core/manager.ts
|
|
@@ -2654,10 +2830,17 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2654
2830
|
}
|
|
2655
2831
|
const acquired = await this.rateLimiter.acquire(provider.id, timeoutMs);
|
|
2656
2832
|
if (!acquired) {
|
|
2657
|
-
|
|
2833
|
+
const timeoutError = new Error(`Rate limit token acquisition timeout for ${provider.id} after ${timeoutMs || 6e4}ms`);
|
|
2834
|
+
this.options.logger.warn(timeoutError.message);
|
|
2835
|
+
this.reportError(timeoutError);
|
|
2658
2836
|
const next = this.selector.getNextProvider(this.network, [provider.id]);
|
|
2659
2837
|
if (next) {
|
|
2660
|
-
|
|
2838
|
+
const nextTimeout = timeoutMs ? Math.min(timeoutMs, 1e4) : 1e4;
|
|
2839
|
+
const nextAcquired = await this.rateLimiter.acquire(next.id, nextTimeout);
|
|
2840
|
+
if (nextAcquired) {
|
|
2841
|
+
return normalizeV2Endpoint(next.endpointV2, next);
|
|
2842
|
+
}
|
|
2843
|
+
this.options.logger.warn(`Rate limit timeout for next provider ${next.id}, using fallback`);
|
|
2661
2844
|
}
|
|
2662
2845
|
return this.getFallbackEndpoint();
|
|
2663
2846
|
}
|
|
@@ -2707,26 +2890,48 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2707
2890
|
if (!provider) {
|
|
2708
2891
|
provider = this.selector.getBestProvider(this.network);
|
|
2709
2892
|
}
|
|
2710
|
-
if (!provider)
|
|
2893
|
+
if (!provider) {
|
|
2894
|
+
this.options.logger.warn(`Cannot report error: no provider available for ${this.network}`);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2711
2897
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2712
2898
|
const errorMsgLower = errorMsg.toLowerCase();
|
|
2713
|
-
const
|
|
2714
|
-
const
|
|
2715
|
-
const
|
|
2716
|
-
const
|
|
2717
|
-
const
|
|
2899
|
+
const enhancedErrorMsg = `Provider ${provider.id} (${provider.name}): ${errorMsg}`;
|
|
2900
|
+
const responseStatus = error instanceof Error && error?.response?.status || error instanceof Error && error?.status || error instanceof Error && error?.statusCode || null;
|
|
2901
|
+
const statusMatch = errorMsg.match(/\b(\d{3})\b/);
|
|
2902
|
+
const statusFromMsg = statusMatch ? parseInt(statusMatch[1], 10) : null;
|
|
2903
|
+
const httpStatus = responseStatus || statusFromMsg;
|
|
2904
|
+
const is429 = httpStatus === 429 || errorMsgLower.includes("429") || errorMsgLower.includes("rate limit") || errorMsgLower.includes("too many requests");
|
|
2905
|
+
const is503 = httpStatus === 503 || errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
|
|
2906
|
+
const is502 = httpStatus === 502 || errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
|
|
2907
|
+
const is404 = httpStatus === 404 || errorMsgLower.includes("404") || errorMsgLower.includes("not found");
|
|
2908
|
+
const is401 = httpStatus === 401 || errorMsgLower.includes("401") || errorMsgLower.includes("unauthorized") || errorMsgLower.includes("invalid api key") || errorMsgLower.includes("authentication failed");
|
|
2909
|
+
const isTimeout = error instanceof Error && error.name === "AbortError" || errorMsgLower.includes("timeout") || errorMsgLower.includes("timed out") || errorMsgLower.includes("abort");
|
|
2718
2910
|
if (isRateLimitError(error) || is429) {
|
|
2719
2911
|
this.rateLimiter.reportRateLimitError(provider.id);
|
|
2720
|
-
this.healthChecker.markDegraded(provider.id, this.network,
|
|
2721
|
-
|
|
2912
|
+
this.healthChecker.markDegraded(provider.id, this.network, enhancedErrorMsg);
|
|
2913
|
+
this.options.logger.warn(`${enhancedErrorMsg} - Rate limit detected, switching to next provider`);
|
|
2914
|
+
} else if (is404 || is401) {
|
|
2915
|
+
this.rateLimiter.reportError(provider.id);
|
|
2916
|
+
this.healthChecker.markOffline(provider.id, this.network, enhancedErrorMsg);
|
|
2917
|
+
this.options.logger.error(`${enhancedErrorMsg} - Permanent error (${is404 ? "404" : "401"}), provider marked offline`);
|
|
2918
|
+
} else if (is503 || is502 || isTimeout) {
|
|
2722
2919
|
this.rateLimiter.reportError(provider.id);
|
|
2723
|
-
this.healthChecker.markOffline(provider.id, this.network,
|
|
2920
|
+
this.healthChecker.markOffline(provider.id, this.network, enhancedErrorMsg);
|
|
2921
|
+
const errorType = is503 ? "503" : is502 ? "502" : "timeout";
|
|
2922
|
+
this.options.logger.warn(`${enhancedErrorMsg} - Server error (${errorType}), switching to next provider`);
|
|
2724
2923
|
} else {
|
|
2725
2924
|
this.rateLimiter.reportError(provider.id);
|
|
2726
|
-
this.healthChecker.markDegraded(provider.id, this.network,
|
|
2925
|
+
this.healthChecker.markDegraded(provider.id, this.network, enhancedErrorMsg);
|
|
2926
|
+
this.options.logger.warn(`${enhancedErrorMsg} - Unknown error, switching to next provider`);
|
|
2927
|
+
}
|
|
2928
|
+
const nextProvider = this.selector.handleProviderFailure(provider.id, this.network);
|
|
2929
|
+
if (nextProvider) {
|
|
2930
|
+
this.options.logger.info(`Failover: switched from ${provider.id} to ${nextProvider.id}`);
|
|
2931
|
+
} else {
|
|
2932
|
+
this.options.logger.warn(`Failover: no alternative provider available for ${this.network}`);
|
|
2727
2933
|
}
|
|
2728
2934
|
this.selector.clearCache(this.network);
|
|
2729
|
-
this.selector.handleProviderFailure(provider.id, this.network);
|
|
2730
2935
|
this.notifyListeners();
|
|
2731
2936
|
}
|
|
2732
2937
|
// ========================================================================
|
|
@@ -2993,7 +3198,7 @@ var NodeAdapter = class {
|
|
|
2993
3198
|
network,
|
|
2994
3199
|
createdAt: Date.now()
|
|
2995
3200
|
};
|
|
2996
|
-
this.logger.debug(`Created TonClient for ${network}`, { endpoint });
|
|
3201
|
+
this.logger.debug(`Created TonClient for ${network}`, { endpoint: redactUrl(endpoint) });
|
|
2997
3202
|
return client;
|
|
2998
3203
|
}
|
|
2999
3204
|
/**
|
|
@@ -3277,9 +3482,11 @@ var BrowserAdapter = class {
|
|
|
3277
3482
|
// ========================================================================
|
|
3278
3483
|
/**
|
|
3279
3484
|
* Make a JSON-RPC call to the TON API
|
|
3485
|
+
*
|
|
3486
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3280
3487
|
*/
|
|
3281
3488
|
async jsonRpc(method, params = {}, timeoutMs = 1e4) {
|
|
3282
|
-
const endpoint = await this.manager.
|
|
3489
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3283
3490
|
const controller = new AbortController();
|
|
3284
3491
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3285
3492
|
try {
|
|
@@ -3318,9 +3525,11 @@ var BrowserAdapter = class {
|
|
|
3318
3525
|
// ========================================================================
|
|
3319
3526
|
/**
|
|
3320
3527
|
* Get address state
|
|
3528
|
+
*
|
|
3529
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3321
3530
|
*/
|
|
3322
3531
|
async getAddressState(address, timeoutMs = 1e4) {
|
|
3323
|
-
const endpoint = await this.manager.
|
|
3532
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3324
3533
|
const baseV2 = toV2Base(endpoint);
|
|
3325
3534
|
const url = `${baseV2}/getAddressState?address=${encodeURIComponent(address)}`;
|
|
3326
3535
|
const controller = new AbortController();
|
|
@@ -3350,9 +3559,11 @@ var BrowserAdapter = class {
|
|
|
3350
3559
|
}
|
|
3351
3560
|
/**
|
|
3352
3561
|
* Get address balance
|
|
3562
|
+
*
|
|
3563
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3353
3564
|
*/
|
|
3354
3565
|
async getAddressBalance(address, timeoutMs = 1e4) {
|
|
3355
|
-
const endpoint = await this.manager.
|
|
3566
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3356
3567
|
const baseV2 = toV2Base(endpoint);
|
|
3357
3568
|
const url = `${baseV2}/getAddressBalance?address=${encodeURIComponent(address)}`;
|
|
3358
3569
|
const controller = new AbortController();
|
|
@@ -3382,9 +3593,11 @@ var BrowserAdapter = class {
|
|
|
3382
3593
|
}
|
|
3383
3594
|
/**
|
|
3384
3595
|
* Get address information
|
|
3596
|
+
*
|
|
3597
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3385
3598
|
*/
|
|
3386
3599
|
async getAddressInfo(address, timeoutMs = 1e4) {
|
|
3387
|
-
const endpoint = await this.manager.
|
|
3600
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3388
3601
|
const baseV2 = toV2Base(endpoint);
|
|
3389
3602
|
const url = `${baseV2}/getAddressInformation?address=${encodeURIComponent(address)}`;
|
|
3390
3603
|
const controller = new AbortController();
|
|
@@ -3412,9 +3625,11 @@ var BrowserAdapter = class {
|
|
|
3412
3625
|
}
|
|
3413
3626
|
/**
|
|
3414
3627
|
* Run get method
|
|
3628
|
+
*
|
|
3629
|
+
* Note: Uses rate limiting to prevent 429 errors.
|
|
3415
3630
|
*/
|
|
3416
3631
|
async runGetMethod(address, method, stack = [], timeoutMs = 15e3) {
|
|
3417
|
-
const endpoint = await this.manager.
|
|
3632
|
+
const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
3418
3633
|
const baseV2 = toV2Base(endpoint);
|
|
3419
3634
|
const url = `${baseV2}/runGetMethod`;
|
|
3420
3635
|
const controller = new AbortController();
|
|
@@ -3520,6 +3735,6 @@ async function createBrowserAdapterForNetwork(network, configPath, logger) {
|
|
|
3520
3735
|
return new BrowserAdapter(manager, logger);
|
|
3521
3736
|
}
|
|
3522
3737
|
|
|
3523
|
-
export { ApiVersionSchema, BaseProvider, BrowserAdapter, CHAINSTACK_RATE_LIMIT, ChainstackProvider, ConfigError, DEFAULT_CONTRACT_TIMEOUT_MS, DEFAULT_HEALTH_CHECK_TIMEOUT_MS, DEFAULT_PROVIDERS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_RATE_LIMIT, GenericProvider, GetBlockProvider, HealthChecker, NetworkSchema, NodeAdapter, ORBS_RATE_LIMIT, OnFinalityProvider, OrbsProvider, ProviderConfigSchema, ProviderError, ProviderManager, ProviderRegistry, ProviderSelector, ProviderTypeSchema, QUICKNODE_RATE_LIMIT, QuickNodeProvider, RateLimitError, RateLimiterManager, RpcConfigSchema, TatumProvider, TimeoutError, TokenBucketRateLimiter, TonCenterProvider, buildGetAddressBalanceUrl, buildGetAddressInfoUrl, buildGetAddressStateUrl, buildRestUrl, createBrowserAdapter, createBrowserAdapterForNetwork, createDefaultConfig, createDefaultRegistry, createEmptyConfig, createHealthChecker, createNodeAdapter, createProvider, createProviderManager, createRateLimiter, createRateLimiterManager, createRegistry, createRegistryFromData, createRegistryFromFile, createSelector, createTimeoutController, detectNetworkFromEndpoint, fetchWithTimeout, getBaseUrl, getDefaultProvidersForNetwork, getEnvVar, getProviderManager, getProvidersForNetwork, getRateLimitForType, getTonClient, getTonClientForNetwork, getTonClientWithRateLimit, isApiVersion, isChainstackUrl, isNetwork, isOrbsUrl, isProviderType, isQuickNodeUrl, isRateLimitError, isTimeoutError, isTonCenterUrl, isValidHttpUrl, isValidWsUrl, loadBuiltinConfig, loadConfig, loadConfigFromData, loadConfigFromUrl, mergeConfigs, mergeWithDefaults, normalizeV2Endpoint, parseProviderConfig, parseRpcConfig, resetNodeAdapter, resolveAllProviders, resolveEndpoints, resolveKeyPlaceholder, resolveProvider, sleep, toV2Base, toV3Base, withRetry, withTimeout, withTimeoutAndRetry, withTimeoutFn };
|
|
3738
|
+
export { ApiVersionSchema, BaseProvider, BrowserAdapter, CHAINSTACK_RATE_LIMIT, ChainstackProvider, ConfigError, DEFAULT_CONTRACT_TIMEOUT_MS, DEFAULT_HEALTH_CHECK_TIMEOUT_MS, DEFAULT_PROVIDERS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_RATE_LIMIT, GenericProvider, GetBlockProvider, HealthChecker, NetworkSchema, NodeAdapter, ORBS_RATE_LIMIT, OnFinalityProvider, OrbsProvider, ProviderConfigSchema, ProviderError, ProviderManager, ProviderRegistry, ProviderSelector, ProviderTypeSchema, QUICKNODE_RATE_LIMIT, QuickNodeProvider, RateLimitError, RateLimiterManager, RpcConfigSchema, TatumProvider, TimeoutError, TokenBucketRateLimiter, TonCenterProvider, buildGetAddressBalanceUrl, buildGetAddressInfoUrl, buildGetAddressStateUrl, buildRestUrl, createBrowserAdapter, createBrowserAdapterForNetwork, createDefaultConfig, createDefaultRegistry, createEmptyConfig, createHealthChecker, createNodeAdapter, createProvider, createProviderManager, createRateLimiter, createRateLimiterManager, createRegistry, createRegistryFromData, createRegistryFromFile, createSelector, createTimeoutController, detectNetworkFromEndpoint, fetchWithTimeout, getBaseUrl, getDefaultProvidersForNetwork, getEnvVar, getProviderManager, getProvidersForNetwork, getRateLimitForType, getTonClient, getTonClientForNetwork, getTonClientWithRateLimit, isApiVersion, isChainstackUrl, isNetwork, isOrbsUrl, isProviderType, isQuickNodeUrl, isRateLimitError, isTimeoutError, isTonCenterUrl, isValidHttpUrl, isValidWsUrl, loadBuiltinConfig, loadConfig, loadConfigFromData, loadConfigFromUrl, mergeConfigs, mergeWithDefaults, normalizeV2Endpoint, parseProviderConfig, parseRpcConfig, redactUrl, resetNodeAdapter, resolveAllProviders, resolveEndpoints, resolveKeyPlaceholder, resolveProvider, sleep, toV2Base, toV3Base, withRetry, withTimeout, withTimeoutAndRetry, withTimeoutFn };
|
|
3524
3739
|
//# sourceMappingURL=index.js.map
|
|
3525
3740
|
//# sourceMappingURL=index.js.map
|