ton-provider-system 0.1.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 +421 -0
- package/dist/index.cjs +2696 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1686 -0
- package/dist/index.d.ts +1686 -0
- package/dist/index.js +2609 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
- package/rpc-schema.json +205 -0
- package/rpc.json +169 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2696 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var ton = require('@ton/ton');
|
|
5
|
+
|
|
6
|
+
// src/types.ts
|
|
7
|
+
var TimeoutError = class extends Error {
|
|
8
|
+
constructor(operation, timeoutMs, message) {
|
|
9
|
+
super(message || `Operation "${operation}" timed out after ${timeoutMs}ms`);
|
|
10
|
+
this.operation = operation;
|
|
11
|
+
this.timeoutMs = timeoutMs;
|
|
12
|
+
this.name = "TimeoutError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var ProviderError = class extends Error {
|
|
16
|
+
constructor(providerId, operation, message, cause) {
|
|
17
|
+
super(`[${providerId}] ${operation}: ${message}`);
|
|
18
|
+
this.providerId = providerId;
|
|
19
|
+
this.operation = operation;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
this.name = "ProviderError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var RateLimitError = class extends Error {
|
|
25
|
+
constructor(providerId, retryAfterMs) {
|
|
26
|
+
super(`Provider ${providerId} rate limited${retryAfterMs ? `, retry after ${retryAfterMs}ms` : ""}`);
|
|
27
|
+
this.providerId = providerId;
|
|
28
|
+
this.retryAfterMs = retryAfterMs;
|
|
29
|
+
this.name = "RateLimitError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var ConfigError = class extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "ConfigError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var NetworkSchema = zod.z.enum(["testnet", "mainnet"]);
|
|
39
|
+
var ProviderTypeSchema = zod.z.enum([
|
|
40
|
+
"chainstack",
|
|
41
|
+
"quicknode",
|
|
42
|
+
"toncenter",
|
|
43
|
+
"orbs",
|
|
44
|
+
"onfinality",
|
|
45
|
+
"ankr",
|
|
46
|
+
"getblock",
|
|
47
|
+
"tatum",
|
|
48
|
+
"tonhub",
|
|
49
|
+
"custom"
|
|
50
|
+
]);
|
|
51
|
+
var ApiVersionSchema = zod.z.enum(["v2", "v3", "v4"]);
|
|
52
|
+
var ProviderEndpointsSchema = zod.z.object({
|
|
53
|
+
v2: zod.z.string().url().optional(),
|
|
54
|
+
v3: zod.z.string().url().optional(),
|
|
55
|
+
v4: zod.z.string().url().optional(),
|
|
56
|
+
ws: zod.z.string().url().optional()
|
|
57
|
+
}).refine(
|
|
58
|
+
(data) => data.v2 || data.v3 || data.v4,
|
|
59
|
+
{ message: "At least one endpoint (v2, v3, or v4) must be provided" }
|
|
60
|
+
);
|
|
61
|
+
var ProviderConfigSchema = zod.z.object({
|
|
62
|
+
name: zod.z.string().min(1, "Provider name is required"),
|
|
63
|
+
type: ProviderTypeSchema,
|
|
64
|
+
network: NetworkSchema,
|
|
65
|
+
endpoints: ProviderEndpointsSchema,
|
|
66
|
+
keyEnvVar: zod.z.string().optional(),
|
|
67
|
+
apiKeyEnvVar: zod.z.string().optional(),
|
|
68
|
+
rps: zod.z.number().int().positive().default(1),
|
|
69
|
+
priority: zod.z.number().int().nonnegative().default(10),
|
|
70
|
+
enabled: zod.z.boolean().default(true),
|
|
71
|
+
isDynamic: zod.z.boolean().optional().default(false),
|
|
72
|
+
description: zod.z.string().optional()
|
|
73
|
+
});
|
|
74
|
+
var NetworkDefaultsSchema = zod.z.object({
|
|
75
|
+
testnet: zod.z.array(zod.z.string()).default([]),
|
|
76
|
+
mainnet: zod.z.array(zod.z.string()).default([])
|
|
77
|
+
});
|
|
78
|
+
var RpcConfigSchema = zod.z.object({
|
|
79
|
+
$schema: zod.z.string().optional(),
|
|
80
|
+
version: zod.z.string().default("1.0"),
|
|
81
|
+
providers: zod.z.record(zod.z.string(), ProviderConfigSchema),
|
|
82
|
+
defaults: NetworkDefaultsSchema
|
|
83
|
+
}).refine(
|
|
84
|
+
(data) => {
|
|
85
|
+
const providerIds = Object.keys(data.providers);
|
|
86
|
+
const allDefaults = [...data.defaults.testnet, ...data.defaults.mainnet];
|
|
87
|
+
const invalidIds = allDefaults.filter((id) => !providerIds.includes(id));
|
|
88
|
+
return invalidIds.length === 0;
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
message: "Default provider IDs must reference existing providers"
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
function parseRpcConfig(data) {
|
|
95
|
+
const result = RpcConfigSchema.safeParse(data);
|
|
96
|
+
if (!result.success) {
|
|
97
|
+
const errors = result.error.errors.map((e) => {
|
|
98
|
+
const path = e.path.join(".");
|
|
99
|
+
return ` - ${path}: ${e.message}`;
|
|
100
|
+
}).join("\n");
|
|
101
|
+
throw new Error(`Invalid rpc.json configuration:
|
|
102
|
+
${errors}`);
|
|
103
|
+
}
|
|
104
|
+
return result.data;
|
|
105
|
+
}
|
|
106
|
+
function parseProviderConfig(id, data) {
|
|
107
|
+
const result = ProviderConfigSchema.safeParse(data);
|
|
108
|
+
if (!result.success) {
|
|
109
|
+
const errors = result.error.errors.map((e) => {
|
|
110
|
+
const path = e.path.join(".");
|
|
111
|
+
return `${path}: ${e.message}`;
|
|
112
|
+
}).join(", ");
|
|
113
|
+
throw new Error(`Invalid provider "${id}": ${errors}`);
|
|
114
|
+
}
|
|
115
|
+
return result.data;
|
|
116
|
+
}
|
|
117
|
+
function createEmptyConfig() {
|
|
118
|
+
return {
|
|
119
|
+
version: "1.0",
|
|
120
|
+
providers: {},
|
|
121
|
+
defaults: {
|
|
122
|
+
testnet: [],
|
|
123
|
+
mainnet: []
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function mergeConfigs(base, override) {
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
...override,
|
|
131
|
+
providers: {
|
|
132
|
+
...base.providers,
|
|
133
|
+
...override.providers || {}
|
|
134
|
+
},
|
|
135
|
+
defaults: {
|
|
136
|
+
testnet: override.defaults?.testnet || base.defaults.testnet,
|
|
137
|
+
mainnet: override.defaults?.mainnet || base.defaults.mainnet
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function isNetwork(value) {
|
|
142
|
+
return value === "testnet" || value === "mainnet";
|
|
143
|
+
}
|
|
144
|
+
function isProviderType(value) {
|
|
145
|
+
const types = [
|
|
146
|
+
"chainstack",
|
|
147
|
+
"quicknode",
|
|
148
|
+
"toncenter",
|
|
149
|
+
"orbs",
|
|
150
|
+
"onfinality",
|
|
151
|
+
"ankr",
|
|
152
|
+
"getblock",
|
|
153
|
+
"tatum",
|
|
154
|
+
"tonhub",
|
|
155
|
+
"custom"
|
|
156
|
+
];
|
|
157
|
+
return typeof value === "string" && types.includes(value);
|
|
158
|
+
}
|
|
159
|
+
function isApiVersion(value) {
|
|
160
|
+
return value === "v2" || value === "v3" || value === "v4";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/config/parser.ts
|
|
164
|
+
var RPC_CONFIG_FILENAME = "rpc.json";
|
|
165
|
+
function getEnvVar(name) {
|
|
166
|
+
if (typeof process !== "undefined" && process.env) {
|
|
167
|
+
return process.env[name];
|
|
168
|
+
}
|
|
169
|
+
if (typeof window !== "undefined" && window.__ENV__) {
|
|
170
|
+
return window.__ENV__[name];
|
|
171
|
+
}
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
function resolveKeyPlaceholder(url, keyEnvVar) {
|
|
175
|
+
if (!keyEnvVar || !url.includes("{key}")) {
|
|
176
|
+
return url;
|
|
177
|
+
}
|
|
178
|
+
const key = getEnvVar(keyEnvVar);
|
|
179
|
+
if (!key) {
|
|
180
|
+
console.warn(`[ConfigParser] Environment variable ${keyEnvVar} not set for URL: ${url}`);
|
|
181
|
+
return url;
|
|
182
|
+
}
|
|
183
|
+
return url.replace("{key}", key);
|
|
184
|
+
}
|
|
185
|
+
function resolveEndpoints(endpoints, keyEnvVar) {
|
|
186
|
+
return {
|
|
187
|
+
v2: endpoints.v2 ? resolveKeyPlaceholder(endpoints.v2, keyEnvVar) : void 0,
|
|
188
|
+
v3: endpoints.v3 ? resolveKeyPlaceholder(endpoints.v3, keyEnvVar) : void 0,
|
|
189
|
+
v4: endpoints.v4 ? resolveKeyPlaceholder(endpoints.v4, keyEnvVar) : void 0,
|
|
190
|
+
ws: endpoints.ws ? resolveKeyPlaceholder(endpoints.ws, keyEnvVar) : void 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function resolveProvider(id, config) {
|
|
194
|
+
if (!config.enabled) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const resolved = resolveEndpoints(config.endpoints, config.keyEnvVar);
|
|
198
|
+
if (!resolved.v2 && !resolved.v3 && !resolved.v4) {
|
|
199
|
+
console.warn(`[ConfigParser] Provider ${id} has no valid endpoints after resolution`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const apiKey = config.apiKeyEnvVar ? getEnvVar(config.apiKeyEnvVar) : void 0;
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
name: config.name,
|
|
206
|
+
type: config.type,
|
|
207
|
+
network: config.network,
|
|
208
|
+
endpointV2: resolved.v2 || resolved.v3 || "",
|
|
209
|
+
// Fallback to v3 if v2 not available
|
|
210
|
+
endpointV3: resolved.v3,
|
|
211
|
+
endpointV4: resolved.v4,
|
|
212
|
+
endpointWs: resolved.ws,
|
|
213
|
+
apiKey,
|
|
214
|
+
rps: config.rps,
|
|
215
|
+
priority: config.priority,
|
|
216
|
+
isDynamic: config.isDynamic || false
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function resolveAllProviders(config) {
|
|
220
|
+
const resolved = [];
|
|
221
|
+
for (const [id, providerConfig] of Object.entries(config.providers)) {
|
|
222
|
+
const provider = resolveProvider(id, providerConfig);
|
|
223
|
+
if (provider) {
|
|
224
|
+
resolved.push(provider);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return resolved;
|
|
228
|
+
}
|
|
229
|
+
function getProvidersForNetwork(config, network) {
|
|
230
|
+
const all = resolveAllProviders(config);
|
|
231
|
+
return all.filter((p) => p.network === network);
|
|
232
|
+
}
|
|
233
|
+
function getDefaultProvidersForNetwork(config, network) {
|
|
234
|
+
const defaultOrder = config.defaults[network];
|
|
235
|
+
const networkProviders = getProvidersForNetwork(config, network);
|
|
236
|
+
const inOrder = [];
|
|
237
|
+
const remaining = [];
|
|
238
|
+
for (const provider of networkProviders) {
|
|
239
|
+
const defaultIndex = defaultOrder.indexOf(provider.id);
|
|
240
|
+
if (defaultIndex !== -1) {
|
|
241
|
+
inOrder[defaultIndex] = provider;
|
|
242
|
+
} else {
|
|
243
|
+
remaining.push(provider);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const orderedProviders = inOrder.filter(Boolean);
|
|
247
|
+
remaining.sort((a, b) => a.priority - b.priority);
|
|
248
|
+
return [...orderedProviders, ...remaining];
|
|
249
|
+
}
|
|
250
|
+
async function loadBuiltinConfig() {
|
|
251
|
+
const fs = await import('fs').then((m) => m.promises);
|
|
252
|
+
const path = await import('path');
|
|
253
|
+
const possiblePaths = [
|
|
254
|
+
// When running from project root (e.g., ts-node scripts/...)
|
|
255
|
+
path.resolve(process.cwd(), "provider_system", RPC_CONFIG_FILENAME),
|
|
256
|
+
// When running from provider_system folder
|
|
257
|
+
path.resolve(process.cwd(), RPC_CONFIG_FILENAME),
|
|
258
|
+
// Relative to this file (CommonJS style)
|
|
259
|
+
path.resolve(__dirname, "..", RPC_CONFIG_FILENAME)
|
|
260
|
+
];
|
|
261
|
+
for (const configPath of possiblePaths) {
|
|
262
|
+
try {
|
|
263
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
264
|
+
const data = JSON.parse(content);
|
|
265
|
+
return parseRpcConfig(data);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error.code !== "ENOENT") {
|
|
268
|
+
throw new Error(`Failed to load RPC config from ${configPath}: ${error.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.warn(`[ConfigParser] Config file ${RPC_CONFIG_FILENAME} not found, using defaults`);
|
|
273
|
+
return createDefaultConfig();
|
|
274
|
+
}
|
|
275
|
+
async function loadConfigFromUrl(url) {
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(url);
|
|
278
|
+
if (!response.ok) {
|
|
279
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
280
|
+
}
|
|
281
|
+
const data = await response.json();
|
|
282
|
+
return parseRpcConfig(data);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new Error(`Failed to load RPC config from ${url}: ${error.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function loadConfigFromData(data) {
|
|
288
|
+
return parseRpcConfig(data);
|
|
289
|
+
}
|
|
290
|
+
async function loadConfig() {
|
|
291
|
+
if (typeof window !== "undefined" && window.__RPC_CONFIG__) {
|
|
292
|
+
return parseRpcConfig(window.__RPC_CONFIG__);
|
|
293
|
+
}
|
|
294
|
+
if (typeof process !== "undefined" && typeof process.cwd === "function") {
|
|
295
|
+
return loadBuiltinConfig();
|
|
296
|
+
}
|
|
297
|
+
console.warn("[ConfigParser] No config source available, using defaults");
|
|
298
|
+
return createDefaultConfig();
|
|
299
|
+
}
|
|
300
|
+
var DEFAULT_PROVIDERS = {
|
|
301
|
+
toncenter_testnet: {
|
|
302
|
+
name: "TON Center Testnet",
|
|
303
|
+
type: "toncenter",
|
|
304
|
+
network: "testnet",
|
|
305
|
+
endpoints: {
|
|
306
|
+
v2: "https://testnet.toncenter.com/api/v2"
|
|
307
|
+
},
|
|
308
|
+
rps: 1,
|
|
309
|
+
// Without API key
|
|
310
|
+
priority: 100,
|
|
311
|
+
enabled: true,
|
|
312
|
+
description: "Official TON Center public endpoint"
|
|
313
|
+
},
|
|
314
|
+
orbs_testnet: {
|
|
315
|
+
name: "Orbs TON Access Testnet",
|
|
316
|
+
type: "orbs",
|
|
317
|
+
network: "testnet",
|
|
318
|
+
endpoints: {
|
|
319
|
+
v2: "https://ton-testnet.orbs.network/api/v2"
|
|
320
|
+
},
|
|
321
|
+
rps: 10,
|
|
322
|
+
priority: 50,
|
|
323
|
+
enabled: true,
|
|
324
|
+
isDynamic: true,
|
|
325
|
+
description: "Decentralized gateway - no API key needed"
|
|
326
|
+
},
|
|
327
|
+
toncenter_mainnet: {
|
|
328
|
+
name: "TON Center Mainnet",
|
|
329
|
+
type: "toncenter",
|
|
330
|
+
network: "mainnet",
|
|
331
|
+
endpoints: {
|
|
332
|
+
v2: "https://toncenter.com/api/v2"
|
|
333
|
+
},
|
|
334
|
+
rps: 1,
|
|
335
|
+
// Without API key
|
|
336
|
+
priority: 100,
|
|
337
|
+
enabled: true,
|
|
338
|
+
description: "Official TON Center public endpoint"
|
|
339
|
+
},
|
|
340
|
+
orbs_mainnet: {
|
|
341
|
+
name: "Orbs TON Access Mainnet",
|
|
342
|
+
type: "orbs",
|
|
343
|
+
network: "mainnet",
|
|
344
|
+
endpoints: {
|
|
345
|
+
v2: "https://ton-mainnet.orbs.network/api/v2"
|
|
346
|
+
},
|
|
347
|
+
rps: 10,
|
|
348
|
+
priority: 50,
|
|
349
|
+
enabled: true,
|
|
350
|
+
isDynamic: true,
|
|
351
|
+
description: "Decentralized gateway - no API key needed"
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
function createDefaultConfig() {
|
|
355
|
+
return {
|
|
356
|
+
version: "1.0",
|
|
357
|
+
providers: { ...DEFAULT_PROVIDERS },
|
|
358
|
+
defaults: {
|
|
359
|
+
testnet: ["orbs_testnet", "toncenter_testnet"],
|
|
360
|
+
mainnet: ["orbs_mainnet", "toncenter_mainnet"]
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function mergeWithDefaults(config) {
|
|
365
|
+
const defaults = createDefaultConfig();
|
|
366
|
+
return {
|
|
367
|
+
...config,
|
|
368
|
+
providers: {
|
|
369
|
+
...defaults.providers,
|
|
370
|
+
...config.providers
|
|
371
|
+
},
|
|
372
|
+
defaults: {
|
|
373
|
+
testnet: config.defaults.testnet.length > 0 ? config.defaults.testnet : defaults.defaults.testnet,
|
|
374
|
+
mainnet: config.defaults.mainnet.length > 0 ? config.defaults.mainnet : defaults.defaults.mainnet
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/core/registry.ts
|
|
380
|
+
var consoleLogger = {
|
|
381
|
+
debug: (msg, data) => console.debug(`[ProviderRegistry] ${msg}`, data || ""),
|
|
382
|
+
info: (msg, data) => console.log(`[ProviderRegistry] ${msg}`, data || ""),
|
|
383
|
+
warn: (msg, data) => console.warn(`[ProviderRegistry] ${msg}`, data || ""),
|
|
384
|
+
error: (msg, data) => console.error(`[ProviderRegistry] ${msg}`, data || "")
|
|
385
|
+
};
|
|
386
|
+
var ProviderRegistry = class {
|
|
387
|
+
constructor(config, logger) {
|
|
388
|
+
this.providers = /* @__PURE__ */ new Map();
|
|
389
|
+
this.config = config || createDefaultConfig();
|
|
390
|
+
this.logger = logger || consoleLogger;
|
|
391
|
+
this.loadProviders();
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Load and resolve all providers from config
|
|
395
|
+
*/
|
|
396
|
+
loadProviders() {
|
|
397
|
+
this.providers.clear();
|
|
398
|
+
const resolved = resolveAllProviders(this.config);
|
|
399
|
+
for (const provider of resolved) {
|
|
400
|
+
this.providers.set(provider.id, provider);
|
|
401
|
+
}
|
|
402
|
+
this.logger.info(`Loaded ${this.providers.size} providers`);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Get a provider by ID
|
|
406
|
+
*/
|
|
407
|
+
getProvider(id) {
|
|
408
|
+
return this.providers.get(id);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Get all providers
|
|
412
|
+
*/
|
|
413
|
+
getAllProviders() {
|
|
414
|
+
return Array.from(this.providers.values());
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Get providers for a specific network
|
|
418
|
+
*/
|
|
419
|
+
getProvidersForNetwork(network) {
|
|
420
|
+
return Array.from(this.providers.values()).filter(
|
|
421
|
+
(p) => p.network === network
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Get providers in default order for a network
|
|
426
|
+
*/
|
|
427
|
+
getDefaultOrderForNetwork(network) {
|
|
428
|
+
return getDefaultProvidersForNetwork(this.config, network);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get providers by type
|
|
432
|
+
*/
|
|
433
|
+
getProvidersByType(type) {
|
|
434
|
+
return Array.from(this.providers.values()).filter(
|
|
435
|
+
(p) => p.type === type
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Get providers that have v2 API endpoints
|
|
440
|
+
*/
|
|
441
|
+
getV2Providers() {
|
|
442
|
+
return Array.from(this.providers.values()).filter(
|
|
443
|
+
(p) => p.endpointV2 && p.endpointV2.length > 0
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get v2 providers for a specific network
|
|
448
|
+
*/
|
|
449
|
+
getV2ProvidersForNetwork(network) {
|
|
450
|
+
return this.getProvidersForNetwork(network).filter(
|
|
451
|
+
(p) => p.endpointV2 && p.endpointV2.length > 0
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Check if a provider exists
|
|
456
|
+
*/
|
|
457
|
+
hasProvider(id) {
|
|
458
|
+
return this.providers.has(id);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get provider count
|
|
462
|
+
*/
|
|
463
|
+
get size() {
|
|
464
|
+
return this.providers.size;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Get the underlying config
|
|
468
|
+
*/
|
|
469
|
+
getConfig() {
|
|
470
|
+
return this.config;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Update config and reload providers
|
|
474
|
+
*/
|
|
475
|
+
updateConfig(config) {
|
|
476
|
+
this.config = config;
|
|
477
|
+
this.loadProviders();
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Add or update a provider at runtime
|
|
481
|
+
*/
|
|
482
|
+
setProvider(id, provider) {
|
|
483
|
+
this.providers.set(id, provider);
|
|
484
|
+
this.logger.debug(`Provider ${id} added/updated`);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Remove a provider
|
|
488
|
+
*/
|
|
489
|
+
removeProvider(id) {
|
|
490
|
+
const removed = this.providers.delete(id);
|
|
491
|
+
if (removed) {
|
|
492
|
+
this.logger.debug(`Provider ${id} removed`);
|
|
493
|
+
}
|
|
494
|
+
return removed;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get provider IDs
|
|
498
|
+
*/
|
|
499
|
+
getProviderIds() {
|
|
500
|
+
return Array.from(this.providers.keys());
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get network default provider IDs
|
|
504
|
+
*/
|
|
505
|
+
getDefaultProviderIds(network) {
|
|
506
|
+
return this.config.defaults[network];
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Find provider by endpoint URL (useful for error reporting)
|
|
510
|
+
*/
|
|
511
|
+
findProviderByEndpoint(endpoint) {
|
|
512
|
+
const normalizedEndpoint = endpoint.toLowerCase().replace(/\/jsonrpc$/i, "");
|
|
513
|
+
for (const provider of this.providers.values()) {
|
|
514
|
+
const v2Normalized = provider.endpointV2?.toLowerCase().replace(/\/jsonrpc$/i, "");
|
|
515
|
+
const v3Normalized = provider.endpointV3?.toLowerCase().replace(/\/jsonrpc$/i, "");
|
|
516
|
+
if (v2Normalized && normalizedEndpoint.includes(v2Normalized.split("/api/")[0])) {
|
|
517
|
+
return provider;
|
|
518
|
+
}
|
|
519
|
+
if (v3Normalized && normalizedEndpoint.includes(v3Normalized.split("/api/")[0])) {
|
|
520
|
+
return provider;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return void 0;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
async function createRegistry(logger) {
|
|
527
|
+
const config = await loadConfig();
|
|
528
|
+
const mergedConfig = mergeWithDefaults(config);
|
|
529
|
+
return new ProviderRegistry(mergedConfig, logger);
|
|
530
|
+
}
|
|
531
|
+
async function createRegistryFromFile(_filePath, logger) {
|
|
532
|
+
return createRegistry(logger);
|
|
533
|
+
}
|
|
534
|
+
function createDefaultRegistry(logger) {
|
|
535
|
+
const config = createDefaultConfig();
|
|
536
|
+
return new ProviderRegistry(config, logger);
|
|
537
|
+
}
|
|
538
|
+
function createRegistryFromData(data, logger) {
|
|
539
|
+
const mergedConfig = mergeWithDefaults(data);
|
|
540
|
+
return new ProviderRegistry(mergedConfig, logger);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/utils/endpoint.ts
|
|
544
|
+
function normalizeV2Endpoint(endpoint) {
|
|
545
|
+
let normalized = endpoint.trim();
|
|
546
|
+
if (normalized.endsWith("/")) {
|
|
547
|
+
normalized = normalized.slice(0, -1);
|
|
548
|
+
}
|
|
549
|
+
if (normalized.toLowerCase().endsWith("/jsonrpc")) {
|
|
550
|
+
return normalized;
|
|
551
|
+
}
|
|
552
|
+
if (normalized.endsWith("/api/v2")) {
|
|
553
|
+
return normalized + "/jsonRPC";
|
|
554
|
+
}
|
|
555
|
+
if (normalized.endsWith("/api/v3")) {
|
|
556
|
+
return normalized.replace("/api/v3", "/api/v2/jsonRPC");
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const url = new URL(normalized);
|
|
560
|
+
if (!url.pathname || url.pathname === "/") {
|
|
561
|
+
return normalized + "/jsonRPC";
|
|
562
|
+
}
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
return normalized;
|
|
566
|
+
}
|
|
567
|
+
function toV2Base(endpoint) {
|
|
568
|
+
let normalized = endpoint.trim();
|
|
569
|
+
if (normalized.endsWith("/")) {
|
|
570
|
+
normalized = normalized.slice(0, -1);
|
|
571
|
+
}
|
|
572
|
+
if (normalized.toLowerCase().endsWith("/jsonrpc")) {
|
|
573
|
+
normalized = normalized.slice(0, -8);
|
|
574
|
+
}
|
|
575
|
+
normalized = normalized.replace(/\/api\/v3\b/, "/api/v2");
|
|
576
|
+
if (!normalized.endsWith("/api/v2")) {
|
|
577
|
+
if (normalized.includes("/api/v2")) {
|
|
578
|
+
const idx = normalized.indexOf("/api/v2");
|
|
579
|
+
normalized = normalized.slice(0, idx + 7);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return normalized;
|
|
583
|
+
}
|
|
584
|
+
function toV3Base(endpoint) {
|
|
585
|
+
let normalized = toV2Base(endpoint);
|
|
586
|
+
return normalized.replace("/api/v2", "/api/v3");
|
|
587
|
+
}
|
|
588
|
+
function getBaseUrl(endpoint) {
|
|
589
|
+
try {
|
|
590
|
+
const url = new URL(endpoint);
|
|
591
|
+
return `${url.protocol}//${url.host}`;
|
|
592
|
+
} catch {
|
|
593
|
+
return endpoint;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function isChainstackUrl(url) {
|
|
597
|
+
try {
|
|
598
|
+
const parsed = new URL(url.trim());
|
|
599
|
+
return parsed.hostname.includes("chainstack.com");
|
|
600
|
+
} catch {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function isQuickNodeUrl(url) {
|
|
605
|
+
try {
|
|
606
|
+
const parsed = new URL(url.trim());
|
|
607
|
+
return parsed.hostname.includes("quiknode.pro");
|
|
608
|
+
} catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function isTonCenterUrl(url) {
|
|
613
|
+
try {
|
|
614
|
+
const parsed = new URL(url.trim());
|
|
615
|
+
return parsed.hostname.includes("toncenter.com");
|
|
616
|
+
} catch {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function isOrbsUrl(url) {
|
|
621
|
+
try {
|
|
622
|
+
const parsed = new URL(url.trim());
|
|
623
|
+
return parsed.hostname.includes("orbs.network") || parsed.hostname.includes("ton-access");
|
|
624
|
+
} catch {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function buildRestUrl(baseEndpoint, method) {
|
|
629
|
+
const base = toV2Base(baseEndpoint);
|
|
630
|
+
return `${base}/${method}`;
|
|
631
|
+
}
|
|
632
|
+
function buildGetAddressStateUrl(baseEndpoint, address) {
|
|
633
|
+
const base = toV2Base(baseEndpoint);
|
|
634
|
+
return `${base}/getAddressState?address=${encodeURIComponent(address)}`;
|
|
635
|
+
}
|
|
636
|
+
function buildGetAddressBalanceUrl(baseEndpoint, address) {
|
|
637
|
+
const base = toV2Base(baseEndpoint);
|
|
638
|
+
return `${base}/getAddressBalance?address=${encodeURIComponent(address)}`;
|
|
639
|
+
}
|
|
640
|
+
function buildGetAddressInfoUrl(baseEndpoint, address) {
|
|
641
|
+
const base = toV2Base(baseEndpoint);
|
|
642
|
+
return `${base}/getAddressInformation?address=${encodeURIComponent(address)}`;
|
|
643
|
+
}
|
|
644
|
+
function detectNetworkFromEndpoint(endpoint) {
|
|
645
|
+
const lower = endpoint.toLowerCase();
|
|
646
|
+
if (lower.includes("testnet") || lower.includes("test") || lower.includes("sandbox")) {
|
|
647
|
+
return "testnet";
|
|
648
|
+
}
|
|
649
|
+
if (lower.includes("mainnet") || lower.includes("main") || // TonCenter mainnet doesn't have 'mainnet' in URL
|
|
650
|
+
lower.includes("toncenter.com") && !lower.includes("testnet")) {
|
|
651
|
+
return "mainnet";
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
function isValidHttpUrl(str) {
|
|
656
|
+
try {
|
|
657
|
+
const url = new URL(str);
|
|
658
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function isValidWsUrl(str) {
|
|
664
|
+
try {
|
|
665
|
+
const url = new URL(str);
|
|
666
|
+
return url.protocol === "ws:" || url.protocol === "wss:";
|
|
667
|
+
} catch {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/core/healthChecker.ts
|
|
673
|
+
var consoleLogger2 = {
|
|
674
|
+
debug: (msg, data) => console.debug(`[HealthChecker] ${msg}`, data || ""),
|
|
675
|
+
info: (msg, data) => console.log(`[HealthChecker] ${msg}`, data || ""),
|
|
676
|
+
warn: (msg, data) => console.warn(`[HealthChecker] ${msg}`, data || ""),
|
|
677
|
+
error: (msg, data) => console.error(`[HealthChecker] ${msg}`, data || "")
|
|
678
|
+
};
|
|
679
|
+
var DEFAULT_CONFIG = {
|
|
680
|
+
timeoutMs: 1e4,
|
|
681
|
+
maxBlocksBehind: 10,
|
|
682
|
+
degradedLatencyMs: 3e3
|
|
683
|
+
};
|
|
684
|
+
var HealthChecker = class {
|
|
685
|
+
constructor(config, logger) {
|
|
686
|
+
this.results = /* @__PURE__ */ new Map();
|
|
687
|
+
this.highestSeqno = /* @__PURE__ */ new Map();
|
|
688
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
689
|
+
this.logger = logger || consoleLogger2;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Test a single provider's health
|
|
693
|
+
*/
|
|
694
|
+
async testProvider(provider) {
|
|
695
|
+
const startTime = performance.now();
|
|
696
|
+
const key = this.getResultKey(provider.id, provider.network);
|
|
697
|
+
const testingResult = {
|
|
698
|
+
id: provider.id,
|
|
699
|
+
network: provider.network,
|
|
700
|
+
success: false,
|
|
701
|
+
status: "testing",
|
|
702
|
+
latencyMs: null,
|
|
703
|
+
seqno: null,
|
|
704
|
+
blocksBehind: 0,
|
|
705
|
+
lastTested: null
|
|
706
|
+
};
|
|
707
|
+
this.results.set(key, testingResult);
|
|
708
|
+
try {
|
|
709
|
+
const endpoint = await this.getEndpoint(provider);
|
|
710
|
+
if (!endpoint) {
|
|
711
|
+
throw new Error("No valid endpoint available");
|
|
712
|
+
}
|
|
713
|
+
const normalizedEndpoint = normalizeV2Endpoint(endpoint);
|
|
714
|
+
const info = await this.callGetMasterchainInfo(normalizedEndpoint);
|
|
715
|
+
const endTime = performance.now();
|
|
716
|
+
const latencyMs = Math.round(endTime - startTime);
|
|
717
|
+
const seqno = info.last?.seqno || 0;
|
|
718
|
+
const currentHighest = this.highestSeqno.get(provider.network) || 0;
|
|
719
|
+
if (seqno > currentHighest) {
|
|
720
|
+
this.highestSeqno.set(provider.network, seqno);
|
|
721
|
+
}
|
|
722
|
+
const blocksBehind = Math.max(0, (this.highestSeqno.get(provider.network) || seqno) - seqno);
|
|
723
|
+
let status = "available";
|
|
724
|
+
if (blocksBehind > this.config.maxBlocksBehind) {
|
|
725
|
+
status = "stale";
|
|
726
|
+
} else if (latencyMs > this.config.degradedLatencyMs) {
|
|
727
|
+
status = "degraded";
|
|
728
|
+
}
|
|
729
|
+
const result = {
|
|
730
|
+
id: provider.id,
|
|
731
|
+
network: provider.network,
|
|
732
|
+
success: true,
|
|
733
|
+
status,
|
|
734
|
+
latencyMs,
|
|
735
|
+
seqno,
|
|
736
|
+
blocksBehind,
|
|
737
|
+
lastTested: /* @__PURE__ */ new Date(),
|
|
738
|
+
cachedEndpoint: normalizedEndpoint
|
|
739
|
+
};
|
|
740
|
+
this.results.set(key, result);
|
|
741
|
+
this.logger.debug(
|
|
742
|
+
`Provider ${provider.id} health check: ${status} (${latencyMs}ms, seqno=${seqno}, behind=${blocksBehind})`
|
|
743
|
+
);
|
|
744
|
+
return result;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
const endTime = performance.now();
|
|
747
|
+
const latencyMs = Math.round(endTime - startTime);
|
|
748
|
+
const errorMsg = error.message || String(error) || "Unknown error";
|
|
749
|
+
const is429 = errorMsg.includes("429") || errorMsg.toLowerCase().includes("rate limit");
|
|
750
|
+
const is404 = errorMsg.includes("404") || errorMsg.toLowerCase().includes("not found");
|
|
751
|
+
const isTimeout = error.name === "AbortError" || errorMsg.includes("timeout");
|
|
752
|
+
let status = "offline";
|
|
753
|
+
if (is429) {
|
|
754
|
+
status = "degraded";
|
|
755
|
+
} else if (is404) {
|
|
756
|
+
status = "offline";
|
|
757
|
+
} else if (isTimeout) {
|
|
758
|
+
status = "offline";
|
|
759
|
+
}
|
|
760
|
+
const result = {
|
|
761
|
+
id: provider.id,
|
|
762
|
+
network: provider.network,
|
|
763
|
+
success: false,
|
|
764
|
+
status,
|
|
765
|
+
latencyMs: isTimeout ? null : latencyMs,
|
|
766
|
+
seqno: null,
|
|
767
|
+
blocksBehind: 0,
|
|
768
|
+
lastTested: /* @__PURE__ */ new Date(),
|
|
769
|
+
error: errorMsg
|
|
770
|
+
};
|
|
771
|
+
this.results.set(key, result);
|
|
772
|
+
this.logger.warn(`Provider ${provider.id} health check failed: ${result.error}`);
|
|
773
|
+
return result;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Test multiple providers in parallel with staggered batches
|
|
778
|
+
*/
|
|
779
|
+
async testProviders(providers, batchSize = 2, batchDelayMs = 300) {
|
|
780
|
+
const results = [];
|
|
781
|
+
for (let i = 0; i < providers.length; i += batchSize) {
|
|
782
|
+
const batch = providers.slice(i, i + batchSize);
|
|
783
|
+
const batchResults = await Promise.all(
|
|
784
|
+
batch.map((p) => this.testProvider(p))
|
|
785
|
+
);
|
|
786
|
+
results.push(...batchResults);
|
|
787
|
+
if (i + batchSize < providers.length && batchDelayMs > 0) {
|
|
788
|
+
await this.sleep(batchDelayMs);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return results;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Get the last health result for a provider
|
|
795
|
+
*/
|
|
796
|
+
getResult(providerId, network) {
|
|
797
|
+
const key = this.getResultKey(providerId, network);
|
|
798
|
+
return this.results.get(key);
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Get all results for a network
|
|
802
|
+
*/
|
|
803
|
+
getResultsForNetwork(network) {
|
|
804
|
+
const results = [];
|
|
805
|
+
for (const [key, result] of this.results) {
|
|
806
|
+
if (result.network === network) {
|
|
807
|
+
results.push(result);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return results;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Get available providers for a network (status = available or degraded)
|
|
814
|
+
*/
|
|
815
|
+
getAvailableProviders(network) {
|
|
816
|
+
return this.getResultsForNetwork(network).filter(
|
|
817
|
+
(r) => r.status === "available" || r.status === "degraded"
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Get the best provider for a network (lowest latency among available)
|
|
822
|
+
*/
|
|
823
|
+
getBestProvider(network) {
|
|
824
|
+
const available = this.getAvailableProviders(network).filter((r) => r.latencyMs !== null).sort((a, b) => (a.latencyMs ?? Infinity) - (b.latencyMs ?? Infinity));
|
|
825
|
+
return available[0];
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Get highest known seqno for a network
|
|
829
|
+
*/
|
|
830
|
+
getHighestSeqno(network) {
|
|
831
|
+
return this.highestSeqno.get(network) || 0;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Clear all results
|
|
835
|
+
*/
|
|
836
|
+
clearResults() {
|
|
837
|
+
this.results.clear();
|
|
838
|
+
this.highestSeqno.clear();
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Mark a provider as degraded (e.g., on 429 error)
|
|
842
|
+
*/
|
|
843
|
+
markDegraded(providerId, network, error) {
|
|
844
|
+
const key = this.getResultKey(providerId, network);
|
|
845
|
+
const existing = this.results.get(key);
|
|
846
|
+
if (existing) {
|
|
847
|
+
this.results.set(key, {
|
|
848
|
+
...existing,
|
|
849
|
+
status: "degraded",
|
|
850
|
+
error: error || "Marked as degraded",
|
|
851
|
+
lastTested: /* @__PURE__ */ new Date()
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Mark a provider as offline
|
|
857
|
+
*/
|
|
858
|
+
markOffline(providerId, network, error) {
|
|
859
|
+
const key = this.getResultKey(providerId, network);
|
|
860
|
+
const existing = this.results.get(key);
|
|
861
|
+
if (existing) {
|
|
862
|
+
this.results.set(key, {
|
|
863
|
+
...existing,
|
|
864
|
+
status: "offline",
|
|
865
|
+
error: error || "Marked as offline",
|
|
866
|
+
lastTested: /* @__PURE__ */ new Date()
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// ========================================================================
|
|
871
|
+
// Private Methods
|
|
872
|
+
// ========================================================================
|
|
873
|
+
getResultKey(providerId, network) {
|
|
874
|
+
return `${providerId}-${network}`;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Get endpoint URL for a provider (handles dynamic providers like Orbs)
|
|
878
|
+
*/
|
|
879
|
+
async getEndpoint(provider) {
|
|
880
|
+
if (provider.isDynamic && provider.type === "orbs") {
|
|
881
|
+
try {
|
|
882
|
+
const { getHttpEndpoint } = await import('@orbs-network/ton-access');
|
|
883
|
+
const endpoint = await getHttpEndpoint({ network: provider.network });
|
|
884
|
+
return endpoint;
|
|
885
|
+
} catch (error) {
|
|
886
|
+
this.logger.warn(`Failed to get Orbs endpoint: ${error.message}`);
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return provider.endpointV2 || provider.endpointV3 || null;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Call getMasterchainInfo API
|
|
894
|
+
*/
|
|
895
|
+
async callGetMasterchainInfo(endpoint) {
|
|
896
|
+
const controller = new AbortController();
|
|
897
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
898
|
+
try {
|
|
899
|
+
const response = await fetch(endpoint, {
|
|
900
|
+
method: "POST",
|
|
901
|
+
headers: { "Content-Type": "application/json" },
|
|
902
|
+
body: JSON.stringify({
|
|
903
|
+
id: "1",
|
|
904
|
+
jsonrpc: "2.0",
|
|
905
|
+
method: "getMasterchainInfo",
|
|
906
|
+
params: {}
|
|
907
|
+
}),
|
|
908
|
+
signal: controller.signal
|
|
909
|
+
});
|
|
910
|
+
clearTimeout(timeoutId);
|
|
911
|
+
if (!response.ok) {
|
|
912
|
+
throw new Error(`HTTP ${response.status}`);
|
|
913
|
+
}
|
|
914
|
+
const data = await response.json();
|
|
915
|
+
if (data && typeof data === "object" && "ok" in data) {
|
|
916
|
+
if (!data.ok) {
|
|
917
|
+
throw new Error(data.error || "API returned ok=false");
|
|
918
|
+
}
|
|
919
|
+
return data.result || data;
|
|
920
|
+
}
|
|
921
|
+
if (data.result) {
|
|
922
|
+
return data.result;
|
|
923
|
+
}
|
|
924
|
+
return data;
|
|
925
|
+
} catch (error) {
|
|
926
|
+
clearTimeout(timeoutId);
|
|
927
|
+
throw error;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
sleep(ms) {
|
|
931
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
function createHealthChecker(config, logger) {
|
|
935
|
+
return new HealthChecker(config, logger);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/utils/timeout.ts
|
|
939
|
+
var DEFAULT_PROVIDER_TIMEOUT_MS = 3e4;
|
|
940
|
+
var DEFAULT_CONTRACT_TIMEOUT_MS = 45e3;
|
|
941
|
+
var DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 1e4;
|
|
942
|
+
async function withTimeout(promise, timeoutMs, operationName) {
|
|
943
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
944
|
+
const timeoutId = setTimeout(() => {
|
|
945
|
+
reject(new TimeoutError(operationName, timeoutMs));
|
|
946
|
+
}, timeoutMs);
|
|
947
|
+
promise.finally(() => clearTimeout(timeoutId));
|
|
948
|
+
});
|
|
949
|
+
return Promise.race([promise, timeoutPromise]);
|
|
950
|
+
}
|
|
951
|
+
async function withTimeoutFn(fn, timeoutMs, operationName) {
|
|
952
|
+
return withTimeout(fn(), timeoutMs, operationName);
|
|
953
|
+
}
|
|
954
|
+
function createTimeoutController(timeoutMs) {
|
|
955
|
+
const controller = new AbortController();
|
|
956
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
957
|
+
return {
|
|
958
|
+
controller,
|
|
959
|
+
clear: () => clearTimeout(timeoutId)
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
963
|
+
const { controller, clear } = createTimeoutController(timeoutMs);
|
|
964
|
+
try {
|
|
965
|
+
const response = await fetch(url, {
|
|
966
|
+
...options,
|
|
967
|
+
signal: controller.signal
|
|
968
|
+
});
|
|
969
|
+
return response;
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (error.name === "AbortError") {
|
|
972
|
+
throw new TimeoutError(url, timeoutMs, `Fetch to ${url} timed out after ${timeoutMs}ms`);
|
|
973
|
+
}
|
|
974
|
+
throw error;
|
|
975
|
+
} finally {
|
|
976
|
+
clear();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
var DEFAULT_RETRY_OPTIONS = {
|
|
980
|
+
maxRetries: 3,
|
|
981
|
+
baseDelayMs: 1e3,
|
|
982
|
+
maxDelayMs: 1e4,
|
|
983
|
+
backoffMultiplier: 2
|
|
984
|
+
};
|
|
985
|
+
async function withRetry(fn, options) {
|
|
986
|
+
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
|
|
987
|
+
let lastError = null;
|
|
988
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
989
|
+
try {
|
|
990
|
+
return await fn();
|
|
991
|
+
} catch (error) {
|
|
992
|
+
lastError = error;
|
|
993
|
+
if (opts.isRetryable && !opts.isRetryable(error)) {
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
if (attempt < opts.maxRetries) {
|
|
997
|
+
const delay = Math.min(
|
|
998
|
+
opts.baseDelayMs * Math.pow(opts.backoffMultiplier, attempt),
|
|
999
|
+
opts.maxDelayMs
|
|
1000
|
+
);
|
|
1001
|
+
await sleep(delay);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
throw lastError || new Error("Retry failed");
|
|
1006
|
+
}
|
|
1007
|
+
async function withTimeoutAndRetry(fn, timeoutMs, operationName, retryOptions) {
|
|
1008
|
+
return withRetry(
|
|
1009
|
+
() => withTimeout(fn(), timeoutMs, operationName),
|
|
1010
|
+
{
|
|
1011
|
+
...retryOptions,
|
|
1012
|
+
isRetryable: (error) => {
|
|
1013
|
+
if (error instanceof TimeoutError) {
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
if (retryOptions?.isRetryable) {
|
|
1017
|
+
return retryOptions.isRetryable(error);
|
|
1018
|
+
}
|
|
1019
|
+
const message = error.message?.toLowerCase() || "";
|
|
1020
|
+
return message.includes("network") || message.includes("fetch") || message.includes("econnrefused") || message.includes("etimedout");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
function sleep(ms) {
|
|
1026
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1027
|
+
}
|
|
1028
|
+
function isTimeoutError(error) {
|
|
1029
|
+
return error instanceof TimeoutError;
|
|
1030
|
+
}
|
|
1031
|
+
function isRateLimitError(error) {
|
|
1032
|
+
if (!error) return false;
|
|
1033
|
+
const message = error.message?.toLowerCase() || "";
|
|
1034
|
+
const status = error.status || error.response?.status;
|
|
1035
|
+
return status === 429 || message.includes("rate limit") || message.includes("429");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// src/core/rateLimiter.ts
|
|
1039
|
+
var consoleLogger3 = {
|
|
1040
|
+
debug: (msg, data) => console.debug(`[RateLimiter] ${msg}`, data || ""),
|
|
1041
|
+
info: (msg, data) => console.log(`[RateLimiter] ${msg}`, data || ""),
|
|
1042
|
+
warn: (msg, data) => console.warn(`[RateLimiter] ${msg}`, data || ""),
|
|
1043
|
+
error: (msg, data) => console.error(`[RateLimiter] ${msg}`, data || "")
|
|
1044
|
+
};
|
|
1045
|
+
var DEFAULT_RATE_LIMIT = {
|
|
1046
|
+
rps: 1,
|
|
1047
|
+
burstSize: 3,
|
|
1048
|
+
minDelayMs: 1e3,
|
|
1049
|
+
backoffMultiplier: 2,
|
|
1050
|
+
maxBackoffMs: 3e4
|
|
1051
|
+
};
|
|
1052
|
+
var CHAINSTACK_RATE_LIMIT = {
|
|
1053
|
+
rps: 25,
|
|
1054
|
+
burstSize: 30,
|
|
1055
|
+
minDelayMs: 40,
|
|
1056
|
+
backoffMultiplier: 2,
|
|
1057
|
+
maxBackoffMs: 1e4
|
|
1058
|
+
};
|
|
1059
|
+
var QUICKNODE_RATE_LIMIT = {
|
|
1060
|
+
rps: 10,
|
|
1061
|
+
burstSize: 15,
|
|
1062
|
+
minDelayMs: 100,
|
|
1063
|
+
backoffMultiplier: 2,
|
|
1064
|
+
maxBackoffMs: 1e4
|
|
1065
|
+
};
|
|
1066
|
+
var ORBS_RATE_LIMIT = {
|
|
1067
|
+
rps: 10,
|
|
1068
|
+
burstSize: 20,
|
|
1069
|
+
minDelayMs: 100,
|
|
1070
|
+
backoffMultiplier: 2,
|
|
1071
|
+
maxBackoffMs: 1e4
|
|
1072
|
+
};
|
|
1073
|
+
function getRateLimitForType(type) {
|
|
1074
|
+
switch (type.toLowerCase()) {
|
|
1075
|
+
case "chainstack":
|
|
1076
|
+
return CHAINSTACK_RATE_LIMIT;
|
|
1077
|
+
case "quicknode":
|
|
1078
|
+
return QUICKNODE_RATE_LIMIT;
|
|
1079
|
+
case "orbs":
|
|
1080
|
+
return ORBS_RATE_LIMIT;
|
|
1081
|
+
default:
|
|
1082
|
+
return DEFAULT_RATE_LIMIT;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
var TokenBucketRateLimiter = class {
|
|
1086
|
+
constructor(config, logger) {
|
|
1087
|
+
this.currentBackoff = 0;
|
|
1088
|
+
this.consecutiveErrors = 0;
|
|
1089
|
+
this.requestQueue = [];
|
|
1090
|
+
this.processing = false;
|
|
1091
|
+
this.config = { ...DEFAULT_RATE_LIMIT, ...config };
|
|
1092
|
+
this.tokens = this.config.burstSize;
|
|
1093
|
+
this.lastRefill = Date.now();
|
|
1094
|
+
this.logger = logger || consoleLogger3;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Get current state
|
|
1098
|
+
*/
|
|
1099
|
+
getState() {
|
|
1100
|
+
this.refill();
|
|
1101
|
+
return {
|
|
1102
|
+
tokens: this.tokens,
|
|
1103
|
+
lastRefill: this.lastRefill,
|
|
1104
|
+
currentBackoff: this.currentBackoff,
|
|
1105
|
+
consecutiveErrors: this.consecutiveErrors,
|
|
1106
|
+
processing: this.processing,
|
|
1107
|
+
queueLength: this.requestQueue.length
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Acquire a token (wait if necessary)
|
|
1112
|
+
*
|
|
1113
|
+
* @param timeoutMs - Maximum time to wait for a token (default: 60s)
|
|
1114
|
+
* @returns true if token acquired, false if timeout
|
|
1115
|
+
*/
|
|
1116
|
+
async acquire(timeoutMs = 6e4) {
|
|
1117
|
+
const startTime = Date.now();
|
|
1118
|
+
if (this.processing) {
|
|
1119
|
+
const acquired = await new Promise((resolve) => {
|
|
1120
|
+
const checkTimeout = () => {
|
|
1121
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
1122
|
+
const idx = this.requestQueue.indexOf(resolveCallback);
|
|
1123
|
+
if (idx >= 0) {
|
|
1124
|
+
this.requestQueue.splice(idx, 1);
|
|
1125
|
+
}
|
|
1126
|
+
resolve(false);
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
const resolveCallback = () => resolve(true);
|
|
1130
|
+
this.requestQueue.push(resolveCallback);
|
|
1131
|
+
const timeoutInterval = setInterval(checkTimeout, 1e3);
|
|
1132
|
+
const cleanup = () => clearInterval(timeoutInterval);
|
|
1133
|
+
Promise.resolve().then(() => {
|
|
1134
|
+
if (this.requestQueue.includes(resolveCallback)) ; else {
|
|
1135
|
+
cleanup();
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
if (!acquired) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
this.processing = true;
|
|
1144
|
+
try {
|
|
1145
|
+
this.refill();
|
|
1146
|
+
if (this.currentBackoff > 0) {
|
|
1147
|
+
this.logger.debug(`Applying backoff: ${this.currentBackoff}ms`);
|
|
1148
|
+
await sleep(this.currentBackoff);
|
|
1149
|
+
}
|
|
1150
|
+
while (this.tokens <= 0) {
|
|
1151
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
await sleep(Math.min(100, this.config.minDelayMs));
|
|
1155
|
+
this.refill();
|
|
1156
|
+
}
|
|
1157
|
+
this.tokens--;
|
|
1158
|
+
const timeSinceLastRefill = Date.now() - this.lastRefill;
|
|
1159
|
+
if (timeSinceLastRefill < this.config.minDelayMs) {
|
|
1160
|
+
await sleep(this.config.minDelayMs - timeSinceLastRefill);
|
|
1161
|
+
}
|
|
1162
|
+
this.lastRefill = Date.now();
|
|
1163
|
+
return true;
|
|
1164
|
+
} finally {
|
|
1165
|
+
this.processing = false;
|
|
1166
|
+
if (this.requestQueue.length > 0) {
|
|
1167
|
+
const next = this.requestQueue.shift();
|
|
1168
|
+
next();
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Release a token (call on request completion)
|
|
1174
|
+
*/
|
|
1175
|
+
release() {
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Report a successful request (resets backoff)
|
|
1179
|
+
*/
|
|
1180
|
+
reportSuccess() {
|
|
1181
|
+
this.currentBackoff = 0;
|
|
1182
|
+
this.consecutiveErrors = 0;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Report a rate limit error (applies backoff)
|
|
1186
|
+
*/
|
|
1187
|
+
reportRateLimitError() {
|
|
1188
|
+
this.consecutiveErrors++;
|
|
1189
|
+
if (this.currentBackoff === 0) {
|
|
1190
|
+
this.currentBackoff = this.config.minDelayMs * this.config.backoffMultiplier;
|
|
1191
|
+
} else {
|
|
1192
|
+
this.currentBackoff = Math.min(
|
|
1193
|
+
this.currentBackoff * this.config.backoffMultiplier,
|
|
1194
|
+
this.config.maxBackoffMs
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
this.logger.warn(`Rate limit hit, backoff: ${this.currentBackoff}ms, errors: ${this.consecutiveErrors}`);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Report a general error
|
|
1201
|
+
*/
|
|
1202
|
+
reportError() {
|
|
1203
|
+
this.consecutiveErrors++;
|
|
1204
|
+
if (this.consecutiveErrors >= 3) {
|
|
1205
|
+
this.currentBackoff = Math.min(
|
|
1206
|
+
this.config.minDelayMs * this.consecutiveErrors,
|
|
1207
|
+
this.config.maxBackoffMs / 2
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Reset rate limiter state
|
|
1213
|
+
*/
|
|
1214
|
+
reset() {
|
|
1215
|
+
this.tokens = this.config.burstSize;
|
|
1216
|
+
this.lastRefill = Date.now();
|
|
1217
|
+
this.currentBackoff = 0;
|
|
1218
|
+
this.consecutiveErrors = 0;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Update configuration
|
|
1222
|
+
*/
|
|
1223
|
+
updateConfig(config) {
|
|
1224
|
+
this.config = { ...this.config, ...config };
|
|
1225
|
+
if (this.tokens > this.config.burstSize) {
|
|
1226
|
+
this.tokens = this.config.burstSize;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Refill tokens based on time elapsed
|
|
1231
|
+
*/
|
|
1232
|
+
refill() {
|
|
1233
|
+
const now = Date.now();
|
|
1234
|
+
const elapsed = now - this.lastRefill;
|
|
1235
|
+
const tokensToAdd = Math.floor(elapsed / 1e3 * this.config.rps);
|
|
1236
|
+
if (tokensToAdd > 0) {
|
|
1237
|
+
this.tokens = Math.min(this.config.burstSize, this.tokens + tokensToAdd);
|
|
1238
|
+
this.lastRefill = now;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
var RateLimiterManager = class {
|
|
1243
|
+
constructor(logger) {
|
|
1244
|
+
this.limiters = /* @__PURE__ */ new Map();
|
|
1245
|
+
this.configs = /* @__PURE__ */ new Map();
|
|
1246
|
+
this.logger = logger || consoleLogger3;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Get or create rate limiter for a provider
|
|
1250
|
+
*/
|
|
1251
|
+
getLimiter(providerId, config) {
|
|
1252
|
+
let limiter = this.limiters.get(providerId);
|
|
1253
|
+
if (!limiter) {
|
|
1254
|
+
const savedConfig = this.configs.get(providerId);
|
|
1255
|
+
limiter = new TokenBucketRateLimiter(
|
|
1256
|
+
{ ...savedConfig, ...config },
|
|
1257
|
+
this.logger
|
|
1258
|
+
);
|
|
1259
|
+
this.limiters.set(providerId, limiter);
|
|
1260
|
+
}
|
|
1261
|
+
return limiter;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Set rate limit config for a provider
|
|
1265
|
+
*/
|
|
1266
|
+
setConfig(providerId, config) {
|
|
1267
|
+
this.configs.set(providerId, config);
|
|
1268
|
+
const limiter = this.limiters.get(providerId);
|
|
1269
|
+
if (limiter) {
|
|
1270
|
+
limiter.updateConfig(config);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Get rate limit state for a provider
|
|
1275
|
+
*/
|
|
1276
|
+
getState(providerId) {
|
|
1277
|
+
const limiter = this.limiters.get(providerId);
|
|
1278
|
+
return limiter ? limiter.getState() : null;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Acquire token for a provider
|
|
1282
|
+
*/
|
|
1283
|
+
async acquire(providerId, timeoutMs) {
|
|
1284
|
+
const limiter = this.getLimiter(providerId);
|
|
1285
|
+
return limiter.acquire(timeoutMs);
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Report success for a provider
|
|
1289
|
+
*/
|
|
1290
|
+
reportSuccess(providerId) {
|
|
1291
|
+
const limiter = this.limiters.get(providerId);
|
|
1292
|
+
if (limiter) {
|
|
1293
|
+
limiter.reportSuccess();
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Report rate limit error for a provider
|
|
1298
|
+
*/
|
|
1299
|
+
reportRateLimitError(providerId) {
|
|
1300
|
+
const limiter = this.getLimiter(providerId);
|
|
1301
|
+
limiter.reportRateLimitError();
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Report general error for a provider
|
|
1305
|
+
*/
|
|
1306
|
+
reportError(providerId) {
|
|
1307
|
+
const limiter = this.getLimiter(providerId);
|
|
1308
|
+
limiter.reportError();
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Reset a provider's rate limiter
|
|
1312
|
+
*/
|
|
1313
|
+
reset(providerId) {
|
|
1314
|
+
const limiter = this.limiters.get(providerId);
|
|
1315
|
+
if (limiter) {
|
|
1316
|
+
limiter.reset();
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Reset all rate limiters
|
|
1321
|
+
*/
|
|
1322
|
+
resetAll() {
|
|
1323
|
+
for (const limiter of this.limiters.values()) {
|
|
1324
|
+
limiter.reset();
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Remove a provider's rate limiter
|
|
1329
|
+
*/
|
|
1330
|
+
remove(providerId) {
|
|
1331
|
+
this.limiters.delete(providerId);
|
|
1332
|
+
this.configs.delete(providerId);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Clear all limiters
|
|
1336
|
+
*/
|
|
1337
|
+
clear() {
|
|
1338
|
+
this.limiters.clear();
|
|
1339
|
+
this.configs.clear();
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
function createRateLimiter(rps, logger) {
|
|
1343
|
+
return new TokenBucketRateLimiter(
|
|
1344
|
+
{
|
|
1345
|
+
rps,
|
|
1346
|
+
burstSize: Math.max(3, Math.ceil(rps * 1.5)),
|
|
1347
|
+
minDelayMs: Math.ceil(1e3 / rps),
|
|
1348
|
+
backoffMultiplier: 2,
|
|
1349
|
+
maxBackoffMs: 3e4
|
|
1350
|
+
},
|
|
1351
|
+
logger
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
function createRateLimiterManager(logger) {
|
|
1355
|
+
return new RateLimiterManager(logger);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/core/selector.ts
|
|
1359
|
+
var consoleLogger4 = {
|
|
1360
|
+
debug: (msg, data) => console.debug(`[ProviderSelector] ${msg}`, data || ""),
|
|
1361
|
+
info: (msg, data) => console.log(`[ProviderSelector] ${msg}`, data || ""),
|
|
1362
|
+
warn: (msg, data) => console.warn(`[ProviderSelector] ${msg}`, data || ""),
|
|
1363
|
+
error: (msg, data) => console.error(`[ProviderSelector] ${msg}`, data || "")
|
|
1364
|
+
};
|
|
1365
|
+
var DEFAULT_CONFIG2 = {
|
|
1366
|
+
preferredLatencyMs: 1e3,
|
|
1367
|
+
latencyWeight: 0.4,
|
|
1368
|
+
priorityWeight: 0.3,
|
|
1369
|
+
freshnessWeight: 0.3,
|
|
1370
|
+
minStatus: ["available", "degraded"]
|
|
1371
|
+
};
|
|
1372
|
+
var ProviderSelector = class {
|
|
1373
|
+
constructor(registry, healthChecker, config, logger) {
|
|
1374
|
+
// Selection state
|
|
1375
|
+
this.selectedProviderId = null;
|
|
1376
|
+
this.autoSelect = true;
|
|
1377
|
+
this.customEndpoint = null;
|
|
1378
|
+
this.bestProviderByNetwork = /* @__PURE__ */ new Map();
|
|
1379
|
+
this.registry = registry;
|
|
1380
|
+
this.healthChecker = healthChecker;
|
|
1381
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
1382
|
+
this.logger = logger || consoleLogger4;
|
|
1383
|
+
}
|
|
1384
|
+
// ========================================================================
|
|
1385
|
+
// Selection Methods
|
|
1386
|
+
// ========================================================================
|
|
1387
|
+
/**
|
|
1388
|
+
* Get the best provider for a network
|
|
1389
|
+
*/
|
|
1390
|
+
getBestProvider(network) {
|
|
1391
|
+
if (this.customEndpoint) {
|
|
1392
|
+
return this.createCustomProvider(network);
|
|
1393
|
+
}
|
|
1394
|
+
if (!this.autoSelect && this.selectedProviderId) {
|
|
1395
|
+
const provider = this.registry.getProvider(this.selectedProviderId);
|
|
1396
|
+
if (provider && provider.network === network) {
|
|
1397
|
+
return provider;
|
|
1398
|
+
}
|
|
1399
|
+
this.logger.warn(
|
|
1400
|
+
`Selected provider ${this.selectedProviderId} not found or wrong network, using auto-select`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
const cachedBestId = this.bestProviderByNetwork.get(network);
|
|
1404
|
+
if (cachedBestId) {
|
|
1405
|
+
const cached = this.registry.getProvider(cachedBestId);
|
|
1406
|
+
const health = this.healthChecker.getResult(cachedBestId, network);
|
|
1407
|
+
if (cached && health && this.config.minStatus.includes(health.status)) {
|
|
1408
|
+
return cached;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return this.findBestProvider(network);
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Find the best provider for a network (recalculates)
|
|
1415
|
+
*/
|
|
1416
|
+
findBestProvider(network) {
|
|
1417
|
+
const providers = this.registry.getProvidersForNetwork(network);
|
|
1418
|
+
if (providers.length === 0) {
|
|
1419
|
+
this.logger.warn(`No providers available for ${network}`);
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
const scored = providers.map((provider) => ({
|
|
1423
|
+
provider,
|
|
1424
|
+
score: this.scoreProvider(provider, network)
|
|
1425
|
+
})).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
|
1426
|
+
if (scored.length === 0) {
|
|
1427
|
+
const defaults = this.registry.getDefaultOrderForNetwork(network);
|
|
1428
|
+
if (defaults.length > 0) {
|
|
1429
|
+
this.logger.warn(`No healthy providers for ${network}, using first default`);
|
|
1430
|
+
return defaults[0];
|
|
1431
|
+
}
|
|
1432
|
+
return providers[0];
|
|
1433
|
+
}
|
|
1434
|
+
const best = scored[0].provider;
|
|
1435
|
+
this.bestProviderByNetwork.set(network, best.id);
|
|
1436
|
+
this.logger.debug(
|
|
1437
|
+
`Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)})`
|
|
1438
|
+
);
|
|
1439
|
+
return best;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Get all available providers for a network, sorted by score
|
|
1443
|
+
*/
|
|
1444
|
+
getAvailableProviders(network) {
|
|
1445
|
+
const providers = this.registry.getProvidersForNetwork(network);
|
|
1446
|
+
return providers.map((provider) => ({
|
|
1447
|
+
provider,
|
|
1448
|
+
score: this.scoreProvider(provider, network)
|
|
1449
|
+
})).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).map((item) => item.provider);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Get the next best provider (for failover)
|
|
1453
|
+
*/
|
|
1454
|
+
getNextProvider(network, excludeIds) {
|
|
1455
|
+
const providers = this.registry.getProvidersForNetwork(network);
|
|
1456
|
+
const available = providers.filter((p) => !excludeIds.includes(p.id)).map((provider) => ({
|
|
1457
|
+
provider,
|
|
1458
|
+
score: this.scoreProvider(provider, network)
|
|
1459
|
+
})).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
|
1460
|
+
if (available.length === 0) {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
return available[0].provider;
|
|
1464
|
+
}
|
|
1465
|
+
// ========================================================================
|
|
1466
|
+
// Scoring
|
|
1467
|
+
// ========================================================================
|
|
1468
|
+
/**
|
|
1469
|
+
* Calculate a score for a provider (higher is better)
|
|
1470
|
+
*/
|
|
1471
|
+
scoreProvider(provider, network) {
|
|
1472
|
+
const health = this.healthChecker.getResult(provider.id, network);
|
|
1473
|
+
if (!health || health.status === "untested") {
|
|
1474
|
+
return 0.1 * (1 / (provider.priority + 1));
|
|
1475
|
+
}
|
|
1476
|
+
if (health.success === false) {
|
|
1477
|
+
return 0;
|
|
1478
|
+
}
|
|
1479
|
+
if (health.status === "offline") {
|
|
1480
|
+
return 0;
|
|
1481
|
+
}
|
|
1482
|
+
if (!this.config.minStatus.includes(health.status)) {
|
|
1483
|
+
return 0;
|
|
1484
|
+
}
|
|
1485
|
+
const statusScore = this.getStatusScore(health.status);
|
|
1486
|
+
const latencyScore = this.getLatencyScore(health.latencyMs);
|
|
1487
|
+
const priorityScore = this.getPriorityScore(provider.priority);
|
|
1488
|
+
const freshnessScore = this.getFreshnessScore(health.blocksBehind);
|
|
1489
|
+
const score = statusScore * 0.2 + // Base status score
|
|
1490
|
+
latencyScore * this.config.latencyWeight + priorityScore * this.config.priorityWeight + freshnessScore * this.config.freshnessWeight;
|
|
1491
|
+
return score;
|
|
1492
|
+
}
|
|
1493
|
+
getStatusScore(status) {
|
|
1494
|
+
switch (status) {
|
|
1495
|
+
case "available":
|
|
1496
|
+
return 1;
|
|
1497
|
+
case "degraded":
|
|
1498
|
+
return 0.5;
|
|
1499
|
+
case "stale":
|
|
1500
|
+
return 0.3;
|
|
1501
|
+
default:
|
|
1502
|
+
return 0;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
getLatencyScore(latencyMs) {
|
|
1506
|
+
if (latencyMs === null) {
|
|
1507
|
+
return 0.5;
|
|
1508
|
+
}
|
|
1509
|
+
const ratio = latencyMs / this.config.preferredLatencyMs;
|
|
1510
|
+
return Math.max(0, 1 - Math.log(ratio + 1) / Math.log(11));
|
|
1511
|
+
}
|
|
1512
|
+
getPriorityScore(priority) {
|
|
1513
|
+
return Math.max(0, 1 - priority / 100);
|
|
1514
|
+
}
|
|
1515
|
+
getFreshnessScore(blocksBehind) {
|
|
1516
|
+
return Math.max(0, 1 - blocksBehind / 10);
|
|
1517
|
+
}
|
|
1518
|
+
// ========================================================================
|
|
1519
|
+
// Selection Control
|
|
1520
|
+
// ========================================================================
|
|
1521
|
+
/**
|
|
1522
|
+
* Set manual provider selection
|
|
1523
|
+
*/
|
|
1524
|
+
setSelectedProvider(providerId) {
|
|
1525
|
+
this.selectedProviderId = providerId;
|
|
1526
|
+
if (providerId !== null) {
|
|
1527
|
+
this.autoSelect = false;
|
|
1528
|
+
}
|
|
1529
|
+
this.logger.info(`Selected provider: ${providerId || "(auto)"}`);
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Get currently selected provider ID
|
|
1533
|
+
*/
|
|
1534
|
+
getSelectedProviderId() {
|
|
1535
|
+
return this.selectedProviderId;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Enable/disable auto-selection
|
|
1539
|
+
*/
|
|
1540
|
+
setAutoSelect(enabled) {
|
|
1541
|
+
this.autoSelect = enabled;
|
|
1542
|
+
if (enabled) {
|
|
1543
|
+
this.selectedProviderId = null;
|
|
1544
|
+
}
|
|
1545
|
+
this.logger.info(`Auto-select: ${enabled}`);
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Check if auto-selection is enabled
|
|
1549
|
+
*/
|
|
1550
|
+
isAutoSelectEnabled() {
|
|
1551
|
+
return this.autoSelect;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Set custom endpoint override
|
|
1555
|
+
*/
|
|
1556
|
+
setCustomEndpoint(endpoint) {
|
|
1557
|
+
this.customEndpoint = endpoint?.trim() || null;
|
|
1558
|
+
this.logger.info(`Custom endpoint: ${this.customEndpoint || "(none)"}`);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Get custom endpoint
|
|
1562
|
+
*/
|
|
1563
|
+
getCustomEndpoint() {
|
|
1564
|
+
return this.customEndpoint;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Check if using custom endpoint
|
|
1568
|
+
*/
|
|
1569
|
+
isUsingCustomEndpoint() {
|
|
1570
|
+
return this.customEndpoint !== null && this.customEndpoint.length > 0;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Clear cached best providers (forces recalculation)
|
|
1574
|
+
*/
|
|
1575
|
+
clearCache() {
|
|
1576
|
+
this.bestProviderByNetwork.clear();
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Update best provider after health check
|
|
1580
|
+
*/
|
|
1581
|
+
updateBestProvider(network) {
|
|
1582
|
+
this.findBestProvider(network);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Handle provider failure (switch to next best)
|
|
1586
|
+
*/
|
|
1587
|
+
handleProviderFailure(providerId, network) {
|
|
1588
|
+
if (this.bestProviderByNetwork.get(network) === providerId) {
|
|
1589
|
+
this.bestProviderByNetwork.delete(network);
|
|
1590
|
+
}
|
|
1591
|
+
return this.getNextProvider(network, [providerId]);
|
|
1592
|
+
}
|
|
1593
|
+
// ========================================================================
|
|
1594
|
+
// Info
|
|
1595
|
+
// ========================================================================
|
|
1596
|
+
/**
|
|
1597
|
+
* Get active provider info
|
|
1598
|
+
*/
|
|
1599
|
+
getActiveProviderInfo(network) {
|
|
1600
|
+
if (this.customEndpoint) {
|
|
1601
|
+
return { id: "custom", name: "Custom Endpoint", isCustom: true };
|
|
1602
|
+
}
|
|
1603
|
+
const provider = this.getBestProvider(network);
|
|
1604
|
+
if (provider) {
|
|
1605
|
+
return { id: provider.id, name: provider.name, isCustom: false };
|
|
1606
|
+
}
|
|
1607
|
+
return null;
|
|
1608
|
+
}
|
|
1609
|
+
// ========================================================================
|
|
1610
|
+
// Private Helpers
|
|
1611
|
+
// ========================================================================
|
|
1612
|
+
/**
|
|
1613
|
+
* Create a pseudo-provider for custom endpoint
|
|
1614
|
+
*/
|
|
1615
|
+
createCustomProvider(network) {
|
|
1616
|
+
return {
|
|
1617
|
+
id: "custom",
|
|
1618
|
+
name: "Custom Endpoint",
|
|
1619
|
+
type: "custom",
|
|
1620
|
+
network,
|
|
1621
|
+
endpointV2: this.customEndpoint,
|
|
1622
|
+
rps: 10,
|
|
1623
|
+
priority: 0,
|
|
1624
|
+
isDynamic: false
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
function createSelector(registry, healthChecker, config, logger) {
|
|
1629
|
+
return new ProviderSelector(registry, healthChecker, config, logger);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/core/manager.ts
|
|
1633
|
+
var consoleLogger5 = {
|
|
1634
|
+
debug: (msg, data) => console.debug(`[ProviderManager] ${msg}`, data || ""),
|
|
1635
|
+
info: (msg, data) => console.log(`[ProviderManager] ${msg}`, data || ""),
|
|
1636
|
+
warn: (msg, data) => console.warn(`[ProviderManager] ${msg}`, data || ""),
|
|
1637
|
+
error: (msg, data) => console.error(`[ProviderManager] ${msg}`, data || "")
|
|
1638
|
+
};
|
|
1639
|
+
var DEFAULT_OPTIONS = {
|
|
1640
|
+
configPath: "",
|
|
1641
|
+
// Unused - config is loaded from provider_system/rpc.json
|
|
1642
|
+
adapter: "node",
|
|
1643
|
+
autoInit: true,
|
|
1644
|
+
requestTimeoutMs: 1e4,
|
|
1645
|
+
healthCheckIntervalMs: 0,
|
|
1646
|
+
// Disabled by default
|
|
1647
|
+
maxBlocksBehind: 10,
|
|
1648
|
+
logger: consoleLogger5
|
|
1649
|
+
};
|
|
1650
|
+
var _ProviderManager = class _ProviderManager {
|
|
1651
|
+
constructor(options) {
|
|
1652
|
+
// Components
|
|
1653
|
+
this.registry = null;
|
|
1654
|
+
this.healthChecker = null;
|
|
1655
|
+
this.rateLimiter = null;
|
|
1656
|
+
this.selector = null;
|
|
1657
|
+
this.network = null;
|
|
1658
|
+
this.initialized = false;
|
|
1659
|
+
this.isTesting = false;
|
|
1660
|
+
this.healthCheckInterval = null;
|
|
1661
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1662
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
1663
|
+
}
|
|
1664
|
+
// ========================================================================
|
|
1665
|
+
// Singleton Pattern
|
|
1666
|
+
// ========================================================================
|
|
1667
|
+
/**
|
|
1668
|
+
* Get singleton instance (recommended for Node.js)
|
|
1669
|
+
*/
|
|
1670
|
+
static getInstance(options) {
|
|
1671
|
+
if (!_ProviderManager.instance) {
|
|
1672
|
+
_ProviderManager.instance = new _ProviderManager(options);
|
|
1673
|
+
}
|
|
1674
|
+
return _ProviderManager.instance;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Reset singleton instance (for testing)
|
|
1678
|
+
*/
|
|
1679
|
+
static resetInstance() {
|
|
1680
|
+
if (_ProviderManager.instance) {
|
|
1681
|
+
_ProviderManager.instance.destroy();
|
|
1682
|
+
_ProviderManager.instance = null;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
// ========================================================================
|
|
1686
|
+
// Initialization
|
|
1687
|
+
// ========================================================================
|
|
1688
|
+
/**
|
|
1689
|
+
* Initialize the provider manager
|
|
1690
|
+
*
|
|
1691
|
+
* @param network - Network to initialize for
|
|
1692
|
+
* @param testProviders - Whether to test providers immediately (default: true)
|
|
1693
|
+
*/
|
|
1694
|
+
async init(network, testProviders = true) {
|
|
1695
|
+
if (this.initialized && this.network === network) {
|
|
1696
|
+
this.options.logger.debug("Already initialized for this network");
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
this.options.logger.info(`Initializing for ${network}...`);
|
|
1700
|
+
this.network = network;
|
|
1701
|
+
const config = await loadConfig();
|
|
1702
|
+
const mergedConfig = mergeWithDefaults(config);
|
|
1703
|
+
this.registry = new ProviderRegistry(mergedConfig, this.options.logger);
|
|
1704
|
+
this.healthChecker = createHealthChecker(
|
|
1705
|
+
{
|
|
1706
|
+
timeoutMs: this.options.requestTimeoutMs,
|
|
1707
|
+
maxBlocksBehind: this.options.maxBlocksBehind
|
|
1708
|
+
},
|
|
1709
|
+
this.options.logger
|
|
1710
|
+
);
|
|
1711
|
+
this.rateLimiter = createRateLimiterManager(this.options.logger);
|
|
1712
|
+
this.selector = createSelector(
|
|
1713
|
+
this.registry,
|
|
1714
|
+
this.healthChecker,
|
|
1715
|
+
void 0,
|
|
1716
|
+
this.options.logger
|
|
1717
|
+
);
|
|
1718
|
+
for (const provider of this.registry.getAllProviders()) {
|
|
1719
|
+
const config2 = getRateLimitForType(provider.type);
|
|
1720
|
+
this.rateLimiter.setConfig(provider.id, {
|
|
1721
|
+
...config2,
|
|
1722
|
+
rps: provider.rps,
|
|
1723
|
+
minDelayMs: Math.ceil(1e3 / provider.rps)
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
this.initialized = true;
|
|
1727
|
+
this.notifyListeners();
|
|
1728
|
+
if (testProviders) {
|
|
1729
|
+
await this.testAllProviders();
|
|
1730
|
+
}
|
|
1731
|
+
if (this.options.healthCheckIntervalMs > 0) {
|
|
1732
|
+
this.startHealthCheckInterval();
|
|
1733
|
+
}
|
|
1734
|
+
this.options.logger.info("Initialization complete");
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Check if manager is initialized
|
|
1738
|
+
*/
|
|
1739
|
+
isInitialized() {
|
|
1740
|
+
return this.initialized;
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Destroy the manager (cleanup)
|
|
1744
|
+
*/
|
|
1745
|
+
destroy() {
|
|
1746
|
+
this.stopHealthCheckInterval();
|
|
1747
|
+
this.listeners.clear();
|
|
1748
|
+
this.registry = null;
|
|
1749
|
+
this.healthChecker = null;
|
|
1750
|
+
this.rateLimiter = null;
|
|
1751
|
+
this.selector = null;
|
|
1752
|
+
this.initialized = false;
|
|
1753
|
+
this.network = null;
|
|
1754
|
+
}
|
|
1755
|
+
// ========================================================================
|
|
1756
|
+
// Provider Testing
|
|
1757
|
+
// ========================================================================
|
|
1758
|
+
/**
|
|
1759
|
+
* Test all providers for current network
|
|
1760
|
+
*/
|
|
1761
|
+
async testAllProviders() {
|
|
1762
|
+
this.ensureInitialized();
|
|
1763
|
+
if (this.isTesting) {
|
|
1764
|
+
this.options.logger.debug("Already testing providers");
|
|
1765
|
+
return [];
|
|
1766
|
+
}
|
|
1767
|
+
this.isTesting = true;
|
|
1768
|
+
this.notifyListeners();
|
|
1769
|
+
this.options.logger.info(`Testing all providers for ${this.network}...`);
|
|
1770
|
+
try {
|
|
1771
|
+
const providers = this.registry.getProvidersForNetwork(this.network);
|
|
1772
|
+
const results = await this.healthChecker.testProviders(providers);
|
|
1773
|
+
this.selector.updateBestProvider(this.network);
|
|
1774
|
+
const available = results.filter((r) => r.success);
|
|
1775
|
+
this.options.logger.info(
|
|
1776
|
+
`Provider testing complete: ${available.length}/${results.length} available`
|
|
1777
|
+
);
|
|
1778
|
+
return results;
|
|
1779
|
+
} finally {
|
|
1780
|
+
this.isTesting = false;
|
|
1781
|
+
this.notifyListeners();
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Test a specific provider
|
|
1786
|
+
*/
|
|
1787
|
+
async testProvider(providerId) {
|
|
1788
|
+
this.ensureInitialized();
|
|
1789
|
+
const provider = this.registry.getProvider(providerId);
|
|
1790
|
+
if (!provider) {
|
|
1791
|
+
this.options.logger.warn(`Provider ${providerId} not found`);
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
return this.healthChecker.testProvider(provider);
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Check if testing is in progress
|
|
1798
|
+
*/
|
|
1799
|
+
isTestingProviders() {
|
|
1800
|
+
return this.isTesting;
|
|
1801
|
+
}
|
|
1802
|
+
// ========================================================================
|
|
1803
|
+
// Endpoint Access
|
|
1804
|
+
// ========================================================================
|
|
1805
|
+
/**
|
|
1806
|
+
* Get endpoint URL for current network
|
|
1807
|
+
*
|
|
1808
|
+
* Handles: custom endpoint > manual selection > auto-selection > fallback
|
|
1809
|
+
*/
|
|
1810
|
+
async getEndpoint() {
|
|
1811
|
+
this.ensureInitialized();
|
|
1812
|
+
const provider = this.selector.getBestProvider(this.network);
|
|
1813
|
+
if (!provider) {
|
|
1814
|
+
this.options.logger.warn("No providers available, using fallback");
|
|
1815
|
+
return this.getFallbackEndpoint();
|
|
1816
|
+
}
|
|
1817
|
+
if (provider.isDynamic && provider.type === "orbs") {
|
|
1818
|
+
try {
|
|
1819
|
+
const { getHttpEndpoint } = await import('@orbs-network/ton-access');
|
|
1820
|
+
const endpoint = await getHttpEndpoint({ network: this.network });
|
|
1821
|
+
return normalizeV2Endpoint(endpoint);
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
this.options.logger.warn(`Failed to get Orbs endpoint: ${error.message}`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return normalizeV2Endpoint(provider.endpointV2);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Get endpoint with rate limiting
|
|
1830
|
+
*
|
|
1831
|
+
* Waits for rate limit token before returning endpoint.
|
|
1832
|
+
*/
|
|
1833
|
+
async getEndpointWithRateLimit(timeoutMs) {
|
|
1834
|
+
this.ensureInitialized();
|
|
1835
|
+
const provider = this.selector.getBestProvider(this.network);
|
|
1836
|
+
if (!provider) {
|
|
1837
|
+
return this.getFallbackEndpoint();
|
|
1838
|
+
}
|
|
1839
|
+
const acquired = await this.rateLimiter.acquire(provider.id, timeoutMs);
|
|
1840
|
+
if (!acquired) {
|
|
1841
|
+
this.options.logger.warn(`Rate limit timeout for ${provider.id}`);
|
|
1842
|
+
const next = this.selector.getNextProvider(this.network, [provider.id]);
|
|
1843
|
+
if (next) {
|
|
1844
|
+
return normalizeV2Endpoint(next.endpointV2);
|
|
1845
|
+
}
|
|
1846
|
+
return this.getFallbackEndpoint();
|
|
1847
|
+
}
|
|
1848
|
+
return normalizeV2Endpoint(provider.endpointV2);
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Get current active provider
|
|
1852
|
+
*/
|
|
1853
|
+
getActiveProvider() {
|
|
1854
|
+
if (!this.initialized || !this.network) {
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
return this.selector.getBestProvider(this.network);
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Get active provider info
|
|
1861
|
+
*/
|
|
1862
|
+
getActiveProviderInfo() {
|
|
1863
|
+
if (!this.initialized || !this.network) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
return this.selector.getActiveProviderInfo(this.network);
|
|
1867
|
+
}
|
|
1868
|
+
// ========================================================================
|
|
1869
|
+
// Error Reporting
|
|
1870
|
+
// ========================================================================
|
|
1871
|
+
/**
|
|
1872
|
+
* Report a successful request
|
|
1873
|
+
*/
|
|
1874
|
+
reportSuccess() {
|
|
1875
|
+
if (!this.initialized || !this.network) return;
|
|
1876
|
+
const provider = this.selector.getBestProvider(this.network);
|
|
1877
|
+
if (provider) {
|
|
1878
|
+
this.rateLimiter.reportSuccess(provider.id);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Report an error (triggers provider switch if needed)
|
|
1883
|
+
*/
|
|
1884
|
+
reportError(error) {
|
|
1885
|
+
if (!this.initialized || !this.network) return;
|
|
1886
|
+
const provider = this.selector.getBestProvider(this.network);
|
|
1887
|
+
if (!provider) return;
|
|
1888
|
+
const errorMsg = error instanceof Error ? error.message : error;
|
|
1889
|
+
if (isRateLimitError(error)) {
|
|
1890
|
+
this.rateLimiter.reportRateLimitError(provider.id);
|
|
1891
|
+
this.healthChecker.markDegraded(provider.id, this.network, errorMsg);
|
|
1892
|
+
} else {
|
|
1893
|
+
this.rateLimiter.reportError(provider.id);
|
|
1894
|
+
}
|
|
1895
|
+
this.selector.handleProviderFailure(provider.id, this.network);
|
|
1896
|
+
this.notifyListeners();
|
|
1897
|
+
}
|
|
1898
|
+
// ========================================================================
|
|
1899
|
+
// Selection Control
|
|
1900
|
+
// ========================================================================
|
|
1901
|
+
/**
|
|
1902
|
+
* Set manual provider selection
|
|
1903
|
+
*/
|
|
1904
|
+
setSelectedProvider(providerId) {
|
|
1905
|
+
this.ensureInitialized();
|
|
1906
|
+
this.selector.setSelectedProvider(providerId);
|
|
1907
|
+
this.notifyListeners();
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Get selected provider ID
|
|
1911
|
+
*/
|
|
1912
|
+
getSelectedProviderId() {
|
|
1913
|
+
if (!this.initialized) return null;
|
|
1914
|
+
return this.selector.getSelectedProviderId();
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Set auto-select mode
|
|
1918
|
+
*/
|
|
1919
|
+
setAutoSelect(enabled) {
|
|
1920
|
+
this.ensureInitialized();
|
|
1921
|
+
this.selector.setAutoSelect(enabled);
|
|
1922
|
+
this.notifyListeners();
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Check if auto-select is enabled
|
|
1926
|
+
*/
|
|
1927
|
+
isAutoSelectEnabled() {
|
|
1928
|
+
if (!this.initialized) return true;
|
|
1929
|
+
return this.selector.isAutoSelectEnabled();
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Set custom endpoint override
|
|
1933
|
+
*/
|
|
1934
|
+
setCustomEndpoint(endpoint) {
|
|
1935
|
+
this.ensureInitialized();
|
|
1936
|
+
this.selector.setCustomEndpoint(endpoint);
|
|
1937
|
+
this.notifyListeners();
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Get custom endpoint
|
|
1941
|
+
*/
|
|
1942
|
+
getCustomEndpoint() {
|
|
1943
|
+
if (!this.initialized) return null;
|
|
1944
|
+
return this.selector.getCustomEndpoint();
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Check if using custom endpoint
|
|
1948
|
+
*/
|
|
1949
|
+
isUsingCustomEndpoint() {
|
|
1950
|
+
if (!this.initialized) return false;
|
|
1951
|
+
return this.selector.isUsingCustomEndpoint();
|
|
1952
|
+
}
|
|
1953
|
+
// ========================================================================
|
|
1954
|
+
// State Access
|
|
1955
|
+
// ========================================================================
|
|
1956
|
+
/**
|
|
1957
|
+
* Get current network
|
|
1958
|
+
*/
|
|
1959
|
+
getNetwork() {
|
|
1960
|
+
return this.network;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Get all providers for current network
|
|
1964
|
+
*/
|
|
1965
|
+
getProviders() {
|
|
1966
|
+
if (!this.initialized || !this.network) return [];
|
|
1967
|
+
return this.registry.getProvidersForNetwork(this.network);
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Get provider health results for current network
|
|
1971
|
+
*/
|
|
1972
|
+
getProviderHealthResults() {
|
|
1973
|
+
if (!this.initialized || !this.network) return [];
|
|
1974
|
+
return this.healthChecker.getResultsForNetwork(this.network);
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Get registry (for advanced usage)
|
|
1978
|
+
*/
|
|
1979
|
+
getRegistry() {
|
|
1980
|
+
return this.registry;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Get health checker (for advanced usage)
|
|
1984
|
+
*/
|
|
1985
|
+
getHealthChecker() {
|
|
1986
|
+
return this.healthChecker;
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Get rate limiter manager (for advanced usage)
|
|
1990
|
+
*/
|
|
1991
|
+
getRateLimiter() {
|
|
1992
|
+
return this.rateLimiter;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Get current state (for UI)
|
|
1996
|
+
*/
|
|
1997
|
+
getState() {
|
|
1998
|
+
const providers = /* @__PURE__ */ new Map();
|
|
1999
|
+
if (this.initialized && this.network && this.registry) {
|
|
2000
|
+
for (const provider of this.registry.getProvidersForNetwork(this.network)) {
|
|
2001
|
+
const health = this.healthChecker?.getResult(provider.id, this.network);
|
|
2002
|
+
const rateLimit = this.rateLimiter?.getState(provider.id);
|
|
2003
|
+
providers.set(provider.id, {
|
|
2004
|
+
id: provider.id,
|
|
2005
|
+
health: health || {
|
|
2006
|
+
id: provider.id,
|
|
2007
|
+
network: this.network,
|
|
2008
|
+
success: false,
|
|
2009
|
+
status: "untested",
|
|
2010
|
+
latencyMs: null,
|
|
2011
|
+
seqno: null,
|
|
2012
|
+
blocksBehind: 0,
|
|
2013
|
+
lastTested: null
|
|
2014
|
+
},
|
|
2015
|
+
rateLimit: rateLimit || {
|
|
2016
|
+
tokens: 0,
|
|
2017
|
+
lastRefill: 0,
|
|
2018
|
+
currentBackoff: 0,
|
|
2019
|
+
consecutiveErrors: 0,
|
|
2020
|
+
processing: false,
|
|
2021
|
+
queueLength: 0
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
network: this.network,
|
|
2028
|
+
initialized: this.initialized,
|
|
2029
|
+
isTesting: this.isTesting,
|
|
2030
|
+
providers,
|
|
2031
|
+
bestProviderByNetwork: new Map(
|
|
2032
|
+
this.network && this.selector ? [[this.network, this.selector.getBestProvider(this.network)?.id || ""]] : []
|
|
2033
|
+
),
|
|
2034
|
+
selectedProviderId: this.selector?.getSelectedProviderId() || null,
|
|
2035
|
+
autoSelect: this.selector?.isAutoSelectEnabled() ?? true,
|
|
2036
|
+
customEndpoint: this.selector?.getCustomEndpoint() || null
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
// ========================================================================
|
|
2040
|
+
// State Listeners
|
|
2041
|
+
// ========================================================================
|
|
2042
|
+
/**
|
|
2043
|
+
* Subscribe to state changes
|
|
2044
|
+
*/
|
|
2045
|
+
subscribe(listener) {
|
|
2046
|
+
this.listeners.add(listener);
|
|
2047
|
+
return () => {
|
|
2048
|
+
this.listeners.delete(listener);
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Notify all listeners
|
|
2053
|
+
*/
|
|
2054
|
+
notifyListeners() {
|
|
2055
|
+
const state = this.getState();
|
|
2056
|
+
this.listeners.forEach((listener) => listener(state));
|
|
2057
|
+
}
|
|
2058
|
+
// ========================================================================
|
|
2059
|
+
// Private Helpers
|
|
2060
|
+
// ========================================================================
|
|
2061
|
+
ensureInitialized() {
|
|
2062
|
+
if (!this.initialized) {
|
|
2063
|
+
throw new Error("ProviderManager not initialized. Call init() first.");
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
getFallbackEndpoint() {
|
|
2067
|
+
if (this.network === "mainnet") {
|
|
2068
|
+
return "https://toncenter.com/api/v2/jsonRPC";
|
|
2069
|
+
}
|
|
2070
|
+
return "https://testnet.toncenter.com/api/v2/jsonRPC";
|
|
2071
|
+
}
|
|
2072
|
+
startHealthCheckInterval() {
|
|
2073
|
+
this.stopHealthCheckInterval();
|
|
2074
|
+
this.healthCheckInterval = setInterval(() => {
|
|
2075
|
+
this.testAllProviders().catch((error) => {
|
|
2076
|
+
this.options.logger.error(`Health check interval failed: ${error.message}`);
|
|
2077
|
+
});
|
|
2078
|
+
}, this.options.healthCheckIntervalMs);
|
|
2079
|
+
this.options.logger.debug(
|
|
2080
|
+
`Started health check interval: ${this.options.healthCheckIntervalMs}ms`
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
stopHealthCheckInterval() {
|
|
2084
|
+
if (this.healthCheckInterval) {
|
|
2085
|
+
clearInterval(this.healthCheckInterval);
|
|
2086
|
+
this.healthCheckInterval = null;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
// Singleton instance
|
|
2091
|
+
_ProviderManager.instance = null;
|
|
2092
|
+
var ProviderManager = _ProviderManager;
|
|
2093
|
+
function createProviderManager(options) {
|
|
2094
|
+
return new ProviderManager(options);
|
|
2095
|
+
}
|
|
2096
|
+
function getProviderManager(options) {
|
|
2097
|
+
return ProviderManager.getInstance(options);
|
|
2098
|
+
}
|
|
2099
|
+
var consoleLogger6 = {
|
|
2100
|
+
debug: (msg, data) => console.debug(`[NodeAdapter] ${msg}`, data || ""),
|
|
2101
|
+
info: (msg, data) => console.log(`[NodeAdapter] ${msg}`, data || ""),
|
|
2102
|
+
warn: (msg, data) => console.warn(`[NodeAdapter] ${msg}`, data || ""),
|
|
2103
|
+
error: (msg, data) => console.error(`[NodeAdapter] ${msg}`, data || "")
|
|
2104
|
+
};
|
|
2105
|
+
var cachedClient = null;
|
|
2106
|
+
var NodeAdapter = class {
|
|
2107
|
+
constructor(manager, logger) {
|
|
2108
|
+
this.manager = manager;
|
|
2109
|
+
this.logger = logger || consoleLogger6;
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Get TonClient instance
|
|
2113
|
+
*
|
|
2114
|
+
* Creates a new client if endpoint changed, otherwise returns cached.
|
|
2115
|
+
*/
|
|
2116
|
+
async getClient() {
|
|
2117
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2118
|
+
const network = this.manager.getNetwork();
|
|
2119
|
+
if (!network) {
|
|
2120
|
+
throw new Error("ProviderManager not initialized");
|
|
2121
|
+
}
|
|
2122
|
+
if (cachedClient && cachedClient.endpoint === endpoint && cachedClient.network === network) {
|
|
2123
|
+
return cachedClient.client;
|
|
2124
|
+
}
|
|
2125
|
+
const provider = this.manager.getActiveProvider();
|
|
2126
|
+
const apiKey = provider?.apiKey;
|
|
2127
|
+
const client = new ton.TonClient({
|
|
2128
|
+
endpoint,
|
|
2129
|
+
apiKey
|
|
2130
|
+
});
|
|
2131
|
+
cachedClient = {
|
|
2132
|
+
client,
|
|
2133
|
+
endpoint,
|
|
2134
|
+
network,
|
|
2135
|
+
createdAt: Date.now()
|
|
2136
|
+
};
|
|
2137
|
+
this.logger.debug(`Created TonClient for ${network}`, { endpoint });
|
|
2138
|
+
return client;
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Reset client cache (forces new client creation)
|
|
2142
|
+
*/
|
|
2143
|
+
resetClient() {
|
|
2144
|
+
cachedClient = null;
|
|
2145
|
+
this.logger.debug("Client cache cleared");
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Get cached client info (for debugging)
|
|
2149
|
+
*/
|
|
2150
|
+
getClientInfo() {
|
|
2151
|
+
if (!cachedClient) return null;
|
|
2152
|
+
return {
|
|
2153
|
+
endpoint: cachedClient.endpoint,
|
|
2154
|
+
network: cachedClient.network,
|
|
2155
|
+
age: Date.now() - cachedClient.createdAt
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
// ========================================================================
|
|
2159
|
+
// Direct REST API Methods
|
|
2160
|
+
// ========================================================================
|
|
2161
|
+
/**
|
|
2162
|
+
* Get address state via REST API
|
|
2163
|
+
*/
|
|
2164
|
+
async getAddressState(address, timeoutMs = 1e4) {
|
|
2165
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2166
|
+
const baseV2 = toV2Base(endpoint);
|
|
2167
|
+
const addrStr = typeof address === "string" ? address : address.toString();
|
|
2168
|
+
const url = `${baseV2}/getAddressState?address=${encodeURIComponent(addrStr)}`;
|
|
2169
|
+
try {
|
|
2170
|
+
const response = await fetchWithTimeout(
|
|
2171
|
+
url,
|
|
2172
|
+
{ headers: { accept: "application/json" } },
|
|
2173
|
+
timeoutMs
|
|
2174
|
+
);
|
|
2175
|
+
const json = await response.json();
|
|
2176
|
+
const data = this.unwrapResponse(json);
|
|
2177
|
+
if (typeof data === "string") {
|
|
2178
|
+
this.manager.reportSuccess();
|
|
2179
|
+
return data;
|
|
2180
|
+
}
|
|
2181
|
+
if (data && typeof data === "object" && typeof data.state === "string") {
|
|
2182
|
+
this.manager.reportSuccess();
|
|
2183
|
+
return data.state;
|
|
2184
|
+
}
|
|
2185
|
+
throw new Error("Unexpected response format");
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
this.manager.reportError(error);
|
|
2188
|
+
throw error;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Get address balance via REST API
|
|
2193
|
+
*/
|
|
2194
|
+
async getAddressBalance(address, timeoutMs = 1e4) {
|
|
2195
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2196
|
+
const baseV2 = toV2Base(endpoint);
|
|
2197
|
+
const addrStr = typeof address === "string" ? address : address.toString();
|
|
2198
|
+
const url = `${baseV2}/getAddressBalance?address=${encodeURIComponent(addrStr)}`;
|
|
2199
|
+
try {
|
|
2200
|
+
const response = await fetchWithTimeout(
|
|
2201
|
+
url,
|
|
2202
|
+
{ headers: { accept: "application/json" } },
|
|
2203
|
+
timeoutMs
|
|
2204
|
+
);
|
|
2205
|
+
const json = await response.json();
|
|
2206
|
+
const data = this.unwrapResponse(json);
|
|
2207
|
+
if (typeof data === "string" || typeof data === "number") {
|
|
2208
|
+
this.manager.reportSuccess();
|
|
2209
|
+
return BigInt(data);
|
|
2210
|
+
}
|
|
2211
|
+
if (data && typeof data === "object" && data.balance !== void 0) {
|
|
2212
|
+
this.manager.reportSuccess();
|
|
2213
|
+
return BigInt(String(data.balance));
|
|
2214
|
+
}
|
|
2215
|
+
throw new Error("Unexpected response format");
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
this.manager.reportError(error);
|
|
2218
|
+
throw error;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Run a get method via REST API
|
|
2223
|
+
*/
|
|
2224
|
+
async runGetMethod(address, method, stack = [], timeoutMs = 15e3) {
|
|
2225
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2226
|
+
const baseV2 = toV2Base(endpoint);
|
|
2227
|
+
const addrStr = typeof address === "string" ? address : address.toString();
|
|
2228
|
+
const url = `${baseV2}/runGetMethod`;
|
|
2229
|
+
try {
|
|
2230
|
+
const response = await fetchWithTimeout(
|
|
2231
|
+
url,
|
|
2232
|
+
{
|
|
2233
|
+
method: "POST",
|
|
2234
|
+
headers: {
|
|
2235
|
+
"Content-Type": "application/json",
|
|
2236
|
+
accept: "application/json"
|
|
2237
|
+
},
|
|
2238
|
+
body: JSON.stringify({
|
|
2239
|
+
address: addrStr,
|
|
2240
|
+
method,
|
|
2241
|
+
stack
|
|
2242
|
+
})
|
|
2243
|
+
},
|
|
2244
|
+
timeoutMs
|
|
2245
|
+
);
|
|
2246
|
+
const json = await response.json();
|
|
2247
|
+
const data = this.unwrapResponse(json);
|
|
2248
|
+
if (data.exit_code === void 0) {
|
|
2249
|
+
throw new Error("Missing exit_code in response");
|
|
2250
|
+
}
|
|
2251
|
+
this.manager.reportSuccess();
|
|
2252
|
+
return {
|
|
2253
|
+
exit_code: data.exit_code,
|
|
2254
|
+
stack: data.stack || []
|
|
2255
|
+
};
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
this.manager.reportError(error);
|
|
2258
|
+
throw error;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* Send BOC via REST API
|
|
2263
|
+
*/
|
|
2264
|
+
async sendBoc(boc, timeoutMs = 3e4) {
|
|
2265
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2266
|
+
const baseV2 = toV2Base(endpoint);
|
|
2267
|
+
const url = `${baseV2}/sendBoc`;
|
|
2268
|
+
const bocBase64 = typeof boc === "string" ? boc : boc.toString("base64");
|
|
2269
|
+
try {
|
|
2270
|
+
const response = await fetchWithTimeout(
|
|
2271
|
+
url,
|
|
2272
|
+
{
|
|
2273
|
+
method: "POST",
|
|
2274
|
+
headers: { "Content-Type": "application/json" },
|
|
2275
|
+
body: JSON.stringify({ boc: bocBase64 })
|
|
2276
|
+
},
|
|
2277
|
+
timeoutMs
|
|
2278
|
+
);
|
|
2279
|
+
const json = await response.json();
|
|
2280
|
+
this.unwrapResponse(json);
|
|
2281
|
+
this.manager.reportSuccess();
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
this.manager.reportError(error);
|
|
2284
|
+
throw error;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Check if contract is deployed
|
|
2289
|
+
*/
|
|
2290
|
+
async isContractDeployed(address, timeoutMs = 1e4) {
|
|
2291
|
+
try {
|
|
2292
|
+
const state = await this.getAddressState(address, timeoutMs);
|
|
2293
|
+
return state === "active";
|
|
2294
|
+
} catch {
|
|
2295
|
+
return false;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
// ========================================================================
|
|
2299
|
+
// Private Helpers
|
|
2300
|
+
// ========================================================================
|
|
2301
|
+
/**
|
|
2302
|
+
* Unwrap TON API response
|
|
2303
|
+
*/
|
|
2304
|
+
unwrapResponse(json) {
|
|
2305
|
+
if (json && typeof json === "object" && "ok" in json) {
|
|
2306
|
+
const resp = json;
|
|
2307
|
+
if (!resp.ok) {
|
|
2308
|
+
throw new Error(resp.error || "API returned ok=false");
|
|
2309
|
+
}
|
|
2310
|
+
return resp.result ?? json;
|
|
2311
|
+
}
|
|
2312
|
+
if (json && typeof json === "object" && "result" in json) {
|
|
2313
|
+
return json.result;
|
|
2314
|
+
}
|
|
2315
|
+
return json;
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
function createNodeAdapter(manager, logger) {
|
|
2319
|
+
return new NodeAdapter(manager, logger);
|
|
2320
|
+
}
|
|
2321
|
+
async function getTonClient(manager) {
|
|
2322
|
+
const adapter = new NodeAdapter(manager);
|
|
2323
|
+
return adapter.getClient();
|
|
2324
|
+
}
|
|
2325
|
+
async function getTonClientForNetwork(network, configPath) {
|
|
2326
|
+
const manager = ProviderManager.getInstance({ configPath });
|
|
2327
|
+
if (!manager.isInitialized() || manager.getNetwork() !== network) {
|
|
2328
|
+
await manager.init(network);
|
|
2329
|
+
}
|
|
2330
|
+
return getTonClient(manager);
|
|
2331
|
+
}
|
|
2332
|
+
function resetNodeAdapter() {
|
|
2333
|
+
cachedClient = null;
|
|
2334
|
+
ProviderManager.resetInstance();
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/adapters/browser.ts
|
|
2338
|
+
var consoleLogger7 = {
|
|
2339
|
+
debug: (msg, data) => console.debug(`[BrowserAdapter] ${msg}`, data || ""),
|
|
2340
|
+
info: (msg, data) => console.log(`[BrowserAdapter] ${msg}`, data || ""),
|
|
2341
|
+
warn: (msg, data) => console.warn(`[BrowserAdapter] ${msg}`, data || ""),
|
|
2342
|
+
error: (msg, data) => console.error(`[BrowserAdapter] ${msg}`, data || "")
|
|
2343
|
+
};
|
|
2344
|
+
var BrowserAdapter = class {
|
|
2345
|
+
constructor(manager, logger) {
|
|
2346
|
+
this.manager = manager;
|
|
2347
|
+
this.logger = logger || consoleLogger7;
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Get current endpoint URL
|
|
2351
|
+
*/
|
|
2352
|
+
async getEndpoint() {
|
|
2353
|
+
return this.manager.getEndpoint();
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Get endpoint with rate limiting
|
|
2357
|
+
*/
|
|
2358
|
+
async getEndpointWithRateLimit(timeoutMs) {
|
|
2359
|
+
return this.manager.getEndpointWithRateLimit(timeoutMs);
|
|
2360
|
+
}
|
|
2361
|
+
// ========================================================================
|
|
2362
|
+
// JSON-RPC Methods
|
|
2363
|
+
// ========================================================================
|
|
2364
|
+
/**
|
|
2365
|
+
* Make a JSON-RPC call to the TON API
|
|
2366
|
+
*/
|
|
2367
|
+
async jsonRpc(method, params = {}, timeoutMs = 1e4) {
|
|
2368
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2369
|
+
const controller = new AbortController();
|
|
2370
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2371
|
+
try {
|
|
2372
|
+
const response = await fetch(endpoint, {
|
|
2373
|
+
method: "POST",
|
|
2374
|
+
headers: { "Content-Type": "application/json" },
|
|
2375
|
+
body: JSON.stringify({
|
|
2376
|
+
id: "1",
|
|
2377
|
+
jsonrpc: "2.0",
|
|
2378
|
+
method,
|
|
2379
|
+
params
|
|
2380
|
+
}),
|
|
2381
|
+
signal: controller.signal
|
|
2382
|
+
});
|
|
2383
|
+
clearTimeout(timeoutId);
|
|
2384
|
+
if (!response.ok) {
|
|
2385
|
+
throw new Error(`HTTP ${response.status}`);
|
|
2386
|
+
}
|
|
2387
|
+
const json = await response.json();
|
|
2388
|
+
const data = this.unwrapResponse(json);
|
|
2389
|
+
this.manager.reportSuccess();
|
|
2390
|
+
return data;
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
clearTimeout(timeoutId);
|
|
2393
|
+
if (error.name === "AbortError") {
|
|
2394
|
+
const timeoutError = new Error(`Request timed out after ${timeoutMs}ms`);
|
|
2395
|
+
this.manager.reportError(timeoutError);
|
|
2396
|
+
throw timeoutError;
|
|
2397
|
+
}
|
|
2398
|
+
this.manager.reportError(error);
|
|
2399
|
+
throw error;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
// ========================================================================
|
|
2403
|
+
// REST API Methods
|
|
2404
|
+
// ========================================================================
|
|
2405
|
+
/**
|
|
2406
|
+
* Get address state
|
|
2407
|
+
*/
|
|
2408
|
+
async getAddressState(address, timeoutMs = 1e4) {
|
|
2409
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2410
|
+
const baseV2 = toV2Base(endpoint);
|
|
2411
|
+
const url = `${baseV2}/getAddressState?address=${encodeURIComponent(address)}`;
|
|
2412
|
+
const controller = new AbortController();
|
|
2413
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2414
|
+
try {
|
|
2415
|
+
const response = await fetch(url, {
|
|
2416
|
+
headers: { accept: "application/json" },
|
|
2417
|
+
signal: controller.signal
|
|
2418
|
+
});
|
|
2419
|
+
clearTimeout(timeoutId);
|
|
2420
|
+
const json = await response.json();
|
|
2421
|
+
const data = this.unwrapResponse(json);
|
|
2422
|
+
if (typeof data === "string") {
|
|
2423
|
+
this.manager.reportSuccess();
|
|
2424
|
+
return data;
|
|
2425
|
+
}
|
|
2426
|
+
if (data && typeof data === "object" && typeof data.state === "string") {
|
|
2427
|
+
this.manager.reportSuccess();
|
|
2428
|
+
return data.state;
|
|
2429
|
+
}
|
|
2430
|
+
throw new Error("Unexpected response format");
|
|
2431
|
+
} catch (error) {
|
|
2432
|
+
clearTimeout(timeoutId);
|
|
2433
|
+
this.manager.reportError(error);
|
|
2434
|
+
throw error;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Get address balance
|
|
2439
|
+
*/
|
|
2440
|
+
async getAddressBalance(address, timeoutMs = 1e4) {
|
|
2441
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2442
|
+
const baseV2 = toV2Base(endpoint);
|
|
2443
|
+
const url = `${baseV2}/getAddressBalance?address=${encodeURIComponent(address)}`;
|
|
2444
|
+
const controller = new AbortController();
|
|
2445
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2446
|
+
try {
|
|
2447
|
+
const response = await fetch(url, {
|
|
2448
|
+
headers: { accept: "application/json" },
|
|
2449
|
+
signal: controller.signal
|
|
2450
|
+
});
|
|
2451
|
+
clearTimeout(timeoutId);
|
|
2452
|
+
const json = await response.json();
|
|
2453
|
+
const data = this.unwrapResponse(json);
|
|
2454
|
+
if (typeof data === "string" || typeof data === "number") {
|
|
2455
|
+
this.manager.reportSuccess();
|
|
2456
|
+
return BigInt(data);
|
|
2457
|
+
}
|
|
2458
|
+
if (data && typeof data === "object" && data.balance !== void 0) {
|
|
2459
|
+
this.manager.reportSuccess();
|
|
2460
|
+
return BigInt(String(data.balance));
|
|
2461
|
+
}
|
|
2462
|
+
throw new Error("Unexpected response format");
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
clearTimeout(timeoutId);
|
|
2465
|
+
this.manager.reportError(error);
|
|
2466
|
+
throw error;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Get address information
|
|
2471
|
+
*/
|
|
2472
|
+
async getAddressInfo(address, timeoutMs = 1e4) {
|
|
2473
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2474
|
+
const baseV2 = toV2Base(endpoint);
|
|
2475
|
+
const url = `${baseV2}/getAddressInformation?address=${encodeURIComponent(address)}`;
|
|
2476
|
+
const controller = new AbortController();
|
|
2477
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2478
|
+
try {
|
|
2479
|
+
const response = await fetch(url, {
|
|
2480
|
+
headers: { accept: "application/json" },
|
|
2481
|
+
signal: controller.signal
|
|
2482
|
+
});
|
|
2483
|
+
clearTimeout(timeoutId);
|
|
2484
|
+
const json = await response.json();
|
|
2485
|
+
const data = this.unwrapResponse(json);
|
|
2486
|
+
this.manager.reportSuccess();
|
|
2487
|
+
return {
|
|
2488
|
+
state: data.state,
|
|
2489
|
+
balance: BigInt(String(data.balance || 0)),
|
|
2490
|
+
lastTransactionLt: data.last_transaction_id?.lt,
|
|
2491
|
+
lastTransactionHash: data.last_transaction_id?.hash
|
|
2492
|
+
};
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
clearTimeout(timeoutId);
|
|
2495
|
+
this.manager.reportError(error);
|
|
2496
|
+
throw error;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Run get method
|
|
2501
|
+
*/
|
|
2502
|
+
async runGetMethod(address, method, stack = [], timeoutMs = 15e3) {
|
|
2503
|
+
const endpoint = await this.manager.getEndpoint();
|
|
2504
|
+
const baseV2 = toV2Base(endpoint);
|
|
2505
|
+
const url = `${baseV2}/runGetMethod`;
|
|
2506
|
+
const controller = new AbortController();
|
|
2507
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2508
|
+
try {
|
|
2509
|
+
const response = await fetch(url, {
|
|
2510
|
+
method: "POST",
|
|
2511
|
+
headers: {
|
|
2512
|
+
"Content-Type": "application/json",
|
|
2513
|
+
accept: "application/json"
|
|
2514
|
+
},
|
|
2515
|
+
body: JSON.stringify({
|
|
2516
|
+
address,
|
|
2517
|
+
method,
|
|
2518
|
+
stack
|
|
2519
|
+
}),
|
|
2520
|
+
signal: controller.signal
|
|
2521
|
+
});
|
|
2522
|
+
clearTimeout(timeoutId);
|
|
2523
|
+
const json = await response.json();
|
|
2524
|
+
const data = this.unwrapResponse(json);
|
|
2525
|
+
if (data.exit_code === void 0) {
|
|
2526
|
+
throw new Error("Missing exit_code in response");
|
|
2527
|
+
}
|
|
2528
|
+
this.manager.reportSuccess();
|
|
2529
|
+
return {
|
|
2530
|
+
exit_code: data.exit_code,
|
|
2531
|
+
stack: data.stack || []
|
|
2532
|
+
};
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
clearTimeout(timeoutId);
|
|
2535
|
+
this.manager.reportError(error);
|
|
2536
|
+
throw error;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Get masterchain info
|
|
2541
|
+
*/
|
|
2542
|
+
async getMasterchainInfo(timeoutMs = 1e4) {
|
|
2543
|
+
const data = await this.jsonRpc("getMasterchainInfo", {}, timeoutMs);
|
|
2544
|
+
return {
|
|
2545
|
+
seqno: data.last?.seqno || 0,
|
|
2546
|
+
stateRootHash: data.state_root_hash || ""
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
// ========================================================================
|
|
2550
|
+
// Provider Management
|
|
2551
|
+
// ========================================================================
|
|
2552
|
+
/**
|
|
2553
|
+
* Get provider manager
|
|
2554
|
+
*/
|
|
2555
|
+
getManager() {
|
|
2556
|
+
return this.manager;
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Get active provider info
|
|
2560
|
+
*/
|
|
2561
|
+
getActiveProviderInfo() {
|
|
2562
|
+
return this.manager.getActiveProviderInfo();
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Get provider health results
|
|
2566
|
+
*/
|
|
2567
|
+
getProviderHealthResults() {
|
|
2568
|
+
return this.manager.getProviderHealthResults();
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Test all providers
|
|
2572
|
+
*/
|
|
2573
|
+
async testAllProviders() {
|
|
2574
|
+
return this.manager.testAllProviders();
|
|
2575
|
+
}
|
|
2576
|
+
// ========================================================================
|
|
2577
|
+
// Private Helpers
|
|
2578
|
+
// ========================================================================
|
|
2579
|
+
/**
|
|
2580
|
+
* Unwrap TON API response
|
|
2581
|
+
*/
|
|
2582
|
+
unwrapResponse(json) {
|
|
2583
|
+
if (json && typeof json === "object" && "ok" in json) {
|
|
2584
|
+
const resp = json;
|
|
2585
|
+
if (!resp.ok) {
|
|
2586
|
+
throw new Error(resp.error || "API returned ok=false");
|
|
2587
|
+
}
|
|
2588
|
+
return resp.result ?? json;
|
|
2589
|
+
}
|
|
2590
|
+
if (json && typeof json === "object" && "result" in json) {
|
|
2591
|
+
return json.result;
|
|
2592
|
+
}
|
|
2593
|
+
return json;
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
function createBrowserAdapter(manager, logger) {
|
|
2597
|
+
return new BrowserAdapter(manager, logger);
|
|
2598
|
+
}
|
|
2599
|
+
async function createBrowserAdapterForNetwork(network, configPath, logger) {
|
|
2600
|
+
const manager = new ProviderManager({
|
|
2601
|
+
configPath,
|
|
2602
|
+
adapter: "browser",
|
|
2603
|
+
logger
|
|
2604
|
+
});
|
|
2605
|
+
await manager.init(network);
|
|
2606
|
+
return new BrowserAdapter(manager, logger);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
exports.ApiVersionSchema = ApiVersionSchema;
|
|
2610
|
+
exports.BrowserAdapter = BrowserAdapter;
|
|
2611
|
+
exports.CHAINSTACK_RATE_LIMIT = CHAINSTACK_RATE_LIMIT;
|
|
2612
|
+
exports.ConfigError = ConfigError;
|
|
2613
|
+
exports.DEFAULT_CONTRACT_TIMEOUT_MS = DEFAULT_CONTRACT_TIMEOUT_MS;
|
|
2614
|
+
exports.DEFAULT_HEALTH_CHECK_TIMEOUT_MS = DEFAULT_HEALTH_CHECK_TIMEOUT_MS;
|
|
2615
|
+
exports.DEFAULT_PROVIDERS = DEFAULT_PROVIDERS;
|
|
2616
|
+
exports.DEFAULT_PROVIDER_TIMEOUT_MS = DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
2617
|
+
exports.DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT;
|
|
2618
|
+
exports.HealthChecker = HealthChecker;
|
|
2619
|
+
exports.NetworkSchema = NetworkSchema;
|
|
2620
|
+
exports.NodeAdapter = NodeAdapter;
|
|
2621
|
+
exports.ORBS_RATE_LIMIT = ORBS_RATE_LIMIT;
|
|
2622
|
+
exports.ProviderConfigSchema = ProviderConfigSchema;
|
|
2623
|
+
exports.ProviderError = ProviderError;
|
|
2624
|
+
exports.ProviderManager = ProviderManager;
|
|
2625
|
+
exports.ProviderRegistry = ProviderRegistry;
|
|
2626
|
+
exports.ProviderSelector = ProviderSelector;
|
|
2627
|
+
exports.ProviderTypeSchema = ProviderTypeSchema;
|
|
2628
|
+
exports.QUICKNODE_RATE_LIMIT = QUICKNODE_RATE_LIMIT;
|
|
2629
|
+
exports.RateLimitError = RateLimitError;
|
|
2630
|
+
exports.RateLimiterManager = RateLimiterManager;
|
|
2631
|
+
exports.RpcConfigSchema = RpcConfigSchema;
|
|
2632
|
+
exports.TimeoutError = TimeoutError;
|
|
2633
|
+
exports.TokenBucketRateLimiter = TokenBucketRateLimiter;
|
|
2634
|
+
exports.buildGetAddressBalanceUrl = buildGetAddressBalanceUrl;
|
|
2635
|
+
exports.buildGetAddressInfoUrl = buildGetAddressInfoUrl;
|
|
2636
|
+
exports.buildGetAddressStateUrl = buildGetAddressStateUrl;
|
|
2637
|
+
exports.buildRestUrl = buildRestUrl;
|
|
2638
|
+
exports.createBrowserAdapter = createBrowserAdapter;
|
|
2639
|
+
exports.createBrowserAdapterForNetwork = createBrowserAdapterForNetwork;
|
|
2640
|
+
exports.createDefaultConfig = createDefaultConfig;
|
|
2641
|
+
exports.createDefaultRegistry = createDefaultRegistry;
|
|
2642
|
+
exports.createEmptyConfig = createEmptyConfig;
|
|
2643
|
+
exports.createHealthChecker = createHealthChecker;
|
|
2644
|
+
exports.createNodeAdapter = createNodeAdapter;
|
|
2645
|
+
exports.createProviderManager = createProviderManager;
|
|
2646
|
+
exports.createRateLimiter = createRateLimiter;
|
|
2647
|
+
exports.createRateLimiterManager = createRateLimiterManager;
|
|
2648
|
+
exports.createRegistry = createRegistry;
|
|
2649
|
+
exports.createRegistryFromData = createRegistryFromData;
|
|
2650
|
+
exports.createRegistryFromFile = createRegistryFromFile;
|
|
2651
|
+
exports.createSelector = createSelector;
|
|
2652
|
+
exports.createTimeoutController = createTimeoutController;
|
|
2653
|
+
exports.detectNetworkFromEndpoint = detectNetworkFromEndpoint;
|
|
2654
|
+
exports.fetchWithTimeout = fetchWithTimeout;
|
|
2655
|
+
exports.getBaseUrl = getBaseUrl;
|
|
2656
|
+
exports.getDefaultProvidersForNetwork = getDefaultProvidersForNetwork;
|
|
2657
|
+
exports.getEnvVar = getEnvVar;
|
|
2658
|
+
exports.getProviderManager = getProviderManager;
|
|
2659
|
+
exports.getProvidersForNetwork = getProvidersForNetwork;
|
|
2660
|
+
exports.getRateLimitForType = getRateLimitForType;
|
|
2661
|
+
exports.getTonClient = getTonClient;
|
|
2662
|
+
exports.getTonClientForNetwork = getTonClientForNetwork;
|
|
2663
|
+
exports.isApiVersion = isApiVersion;
|
|
2664
|
+
exports.isChainstackUrl = isChainstackUrl;
|
|
2665
|
+
exports.isNetwork = isNetwork;
|
|
2666
|
+
exports.isOrbsUrl = isOrbsUrl;
|
|
2667
|
+
exports.isProviderType = isProviderType;
|
|
2668
|
+
exports.isQuickNodeUrl = isQuickNodeUrl;
|
|
2669
|
+
exports.isRateLimitError = isRateLimitError;
|
|
2670
|
+
exports.isTimeoutError = isTimeoutError;
|
|
2671
|
+
exports.isTonCenterUrl = isTonCenterUrl;
|
|
2672
|
+
exports.isValidHttpUrl = isValidHttpUrl;
|
|
2673
|
+
exports.isValidWsUrl = isValidWsUrl;
|
|
2674
|
+
exports.loadBuiltinConfig = loadBuiltinConfig;
|
|
2675
|
+
exports.loadConfig = loadConfig;
|
|
2676
|
+
exports.loadConfigFromData = loadConfigFromData;
|
|
2677
|
+
exports.loadConfigFromUrl = loadConfigFromUrl;
|
|
2678
|
+
exports.mergeConfigs = mergeConfigs;
|
|
2679
|
+
exports.mergeWithDefaults = mergeWithDefaults;
|
|
2680
|
+
exports.normalizeV2Endpoint = normalizeV2Endpoint;
|
|
2681
|
+
exports.parseProviderConfig = parseProviderConfig;
|
|
2682
|
+
exports.parseRpcConfig = parseRpcConfig;
|
|
2683
|
+
exports.resetNodeAdapter = resetNodeAdapter;
|
|
2684
|
+
exports.resolveAllProviders = resolveAllProviders;
|
|
2685
|
+
exports.resolveEndpoints = resolveEndpoints;
|
|
2686
|
+
exports.resolveKeyPlaceholder = resolveKeyPlaceholder;
|
|
2687
|
+
exports.resolveProvider = resolveProvider;
|
|
2688
|
+
exports.sleep = sleep;
|
|
2689
|
+
exports.toV2Base = toV2Base;
|
|
2690
|
+
exports.toV3Base = toV3Base;
|
|
2691
|
+
exports.withRetry = withRetry;
|
|
2692
|
+
exports.withTimeout = withTimeout;
|
|
2693
|
+
exports.withTimeoutAndRetry = withTimeoutAndRetry;
|
|
2694
|
+
exports.withTimeoutFn = withTimeoutFn;
|
|
2695
|
+
//# sourceMappingURL=index.cjs.map
|
|
2696
|
+
//# sourceMappingURL=index.cjs.map
|