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