ton-provider-system 0.4.0 → 0.6.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/dist/index.cjs +187 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -18
- package/dist/index.d.ts +109 -18
- package/dist/index.js +187 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/rpc-schema.json +5 -0
- package/rpc.json +13 -10
package/dist/index.cjs
CHANGED
|
@@ -72,6 +72,7 @@ var ProviderConfigSchema = zod.z.object({
|
|
|
72
72
|
enabled: zod.z.boolean().default(true),
|
|
73
73
|
isDynamic: zod.z.boolean().optional().default(false),
|
|
74
74
|
browserCompatible: zod.z.boolean().optional(),
|
|
75
|
+
servesGetTransactions: zod.z.boolean().optional(),
|
|
75
76
|
description: zod.z.string().optional()
|
|
76
77
|
});
|
|
77
78
|
var NetworkDefaultsSchema = zod.z.object({
|
|
@@ -220,7 +221,10 @@ function resolveProvider(id, config) {
|
|
|
220
221
|
rps: config.rps,
|
|
221
222
|
priority: config.priority,
|
|
222
223
|
isDynamic: config.isDynamic || false,
|
|
223
|
-
browserCompatible: config.browserCompatible !== void 0 ? config.browserCompatible : true
|
|
224
|
+
browserCompatible: config.browserCompatible !== void 0 ? config.browserCompatible : true,
|
|
225
|
+
// Default true: a provider is assumed to serve getTransactions unless the
|
|
226
|
+
// config explicitly opts out (e.g. Chainstack/Orbs testnet liteserver proxies).
|
|
227
|
+
servesGetTransactions: config.servesGetTransactions !== false
|
|
224
228
|
};
|
|
225
229
|
}
|
|
226
230
|
function resolveAllProviders(config) {
|
|
@@ -2554,8 +2558,10 @@ var ProviderSelector = class {
|
|
|
2554
2558
|
rps: 10,
|
|
2555
2559
|
priority: 0,
|
|
2556
2560
|
isDynamic: false,
|
|
2557
|
-
browserCompatible: true
|
|
2561
|
+
browserCompatible: true,
|
|
2558
2562
|
// Custom endpoints are assumed compatible
|
|
2563
|
+
servesGetTransactions: true
|
|
2564
|
+
// Custom endpoints are assumed fully capable
|
|
2559
2565
|
};
|
|
2560
2566
|
}
|
|
2561
2567
|
/**
|
|
@@ -2886,26 +2892,39 @@ var _ProviderManager = class _ProviderManager {
|
|
|
2886
2892
|
// ========================================================================
|
|
2887
2893
|
/**
|
|
2888
2894
|
* Report a successful request
|
|
2895
|
+
*
|
|
2896
|
+
* @param providerId - Optionally attribute the success to a SPECIFIC provider
|
|
2897
|
+
* (used by callers that drive their own candidate list, e.g.
|
|
2898
|
+
* NodeAdapter.getTransactions, where the provider used is not necessarily the
|
|
2899
|
+
* selector's current best). Defaults to the current best provider.
|
|
2889
2900
|
*/
|
|
2890
|
-
reportSuccess() {
|
|
2901
|
+
reportSuccess(providerId) {
|
|
2891
2902
|
if (!this.initialized || !this.network) return;
|
|
2892
|
-
const
|
|
2893
|
-
if (
|
|
2894
|
-
this.rateLimiter.reportSuccess(
|
|
2903
|
+
const id = providerId ?? this.selector.getBestProvider(this.network)?.id;
|
|
2904
|
+
if (id) {
|
|
2905
|
+
this.rateLimiter.reportSuccess(id);
|
|
2895
2906
|
}
|
|
2896
2907
|
}
|
|
2897
2908
|
/**
|
|
2898
2909
|
* Report an error (triggers provider switch if needed)
|
|
2910
|
+
*
|
|
2911
|
+
* @param providerId - Optionally attribute the error to a SPECIFIC provider
|
|
2912
|
+
* (used by callers that drive their own candidate list, e.g.
|
|
2913
|
+
* NodeAdapter.getTransactions). Defaults to the current active/best provider.
|
|
2899
2914
|
*/
|
|
2900
|
-
reportError(error) {
|
|
2915
|
+
reportError(error, providerId) {
|
|
2901
2916
|
if (!this.initialized || !this.network) return;
|
|
2902
|
-
const activeProviderId = this.selector.getActiveProviderId(this.network);
|
|
2903
2917
|
let provider = null;
|
|
2904
|
-
if (
|
|
2905
|
-
provider = this.registry.getProvider(
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2918
|
+
if (providerId) {
|
|
2919
|
+
provider = this.registry.getProvider(providerId) || null;
|
|
2920
|
+
} else {
|
|
2921
|
+
const activeProviderId = this.selector.getActiveProviderId(this.network);
|
|
2922
|
+
if (activeProviderId) {
|
|
2923
|
+
provider = this.registry.getProvider(activeProviderId) || null;
|
|
2924
|
+
}
|
|
2925
|
+
if (!provider) {
|
|
2926
|
+
provider = this.selector.getBestProvider(this.network);
|
|
2927
|
+
}
|
|
2909
2928
|
}
|
|
2910
2929
|
if (!provider) {
|
|
2911
2930
|
this.options.logger.warn(`Cannot report error: no provider available for ${this.network}`);
|
|
@@ -3035,6 +3054,37 @@ var _ProviderManager = class _ProviderManager {
|
|
|
3035
3054
|
}
|
|
3036
3055
|
return providers;
|
|
3037
3056
|
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Get transaction-capable providers for the current network, in selector
|
|
3059
|
+
* score order (best first).
|
|
3060
|
+
*
|
|
3061
|
+
* Read-only: enumerates the score-ordered available providers and excludes any
|
|
3062
|
+
* flagged `servesGetTransactions: false` (Chainstack/Orbs testnet liteserver
|
|
3063
|
+
* proxies, which 403 on v2 getTransactions). Used by
|
|
3064
|
+
* NodeAdapter.getTransactions to build its candidate set WITHOUT poisoning the
|
|
3065
|
+
* incapable providers' global health — so the get-method path keeps using them.
|
|
3066
|
+
* Returns [] when no capable provider is currently selectable.
|
|
3067
|
+
*/
|
|
3068
|
+
getTransactionCapableProviders() {
|
|
3069
|
+
if (!this.initialized || !this.network) return [];
|
|
3070
|
+
return this.selector.getAvailableProviders(this.network).filter((p) => p.servesGetTransactions !== false);
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* Get broadcast-capable providers for the current network, in selector score
|
|
3074
|
+
* order (best first).
|
|
3075
|
+
*
|
|
3076
|
+
* Unlike getTransactionCapableProviders, this applies NO capability filter:
|
|
3077
|
+
* `sendBoc` (forwarding an already-signed external message) works on EVERY
|
|
3078
|
+
* provider type — including the liteserver-proxy providers (Chainstack/Orbs)
|
|
3079
|
+
* that 403 only on the v2 `getTransactions` shape. The list is the plain
|
|
3080
|
+
* score-ordered available set, so a broadcast walks best→worst and a provider
|
|
3081
|
+
* that 429s/5xxs on sendBoc is skipped in favour of the next one.
|
|
3082
|
+
* Returns [] when no provider is currently selectable.
|
|
3083
|
+
*/
|
|
3084
|
+
getBroadcastCapableProviders() {
|
|
3085
|
+
if (!this.initialized || !this.network) return [];
|
|
3086
|
+
return this.selector.getAvailableProviders(this.network);
|
|
3087
|
+
}
|
|
3038
3088
|
/**
|
|
3039
3089
|
* Get provider health results for current network
|
|
3040
3090
|
*/
|
|
@@ -3253,24 +3303,27 @@ var NodeAdapter = class {
|
|
|
3253
3303
|
// Failover-aware high-level reads
|
|
3254
3304
|
// ========================================================================
|
|
3255
3305
|
/**
|
|
3256
|
-
* Get account transactions with provider failover.
|
|
3306
|
+
* Get account transactions with capability-aware provider failover.
|
|
3257
3307
|
*
|
|
3258
|
-
* Unlike `getClient().getTransactions(...)`, this
|
|
3259
|
-
*
|
|
3260
|
-
*
|
|
3261
|
-
*
|
|
3308
|
+
* Unlike `getClient().getTransactions(...)`, this builds a candidate set of the
|
|
3309
|
+
* network's *transaction-capable* providers (score-ordered, best first) and
|
|
3310
|
+
* loops over THAT set. Providers known not to serve the v2 `getTransactions`
|
|
3311
|
+
* shape — flagged `servesGetTransactions: false` in config (Chainstack/Orbs
|
|
3312
|
+
* testnet, which pass the `getMasterchainInfo` health probe but 403 on
|
|
3313
|
+
* transaction reads) — are excluded UP FRONT. They are never called and never
|
|
3314
|
+
* `reportError`-ed here, so their global health stays intact and the fast
|
|
3315
|
+
* get-method path keeps using them (a wasted 403 would otherwise evict them).
|
|
3262
3316
|
*
|
|
3263
|
-
* Semantics:
|
|
3264
|
-
* 1.
|
|
3265
|
-
* 2. Acquire a rate-limit token for THAT provider, bind a short-lived
|
|
3317
|
+
* Semantics (per capable candidate, in score order):
|
|
3318
|
+
* 1. Acquire a rate-limit token for THAT provider, bind a short-lived
|
|
3266
3319
|
* `TonClient` to its endpoint, and call `getTransactions`.
|
|
3267
|
-
*
|
|
3268
|
-
*
|
|
3269
|
-
*
|
|
3270
|
-
*
|
|
3271
|
-
*
|
|
3272
|
-
* caller's retry/dead-letter logic still triggers
|
|
3273
|
-
*
|
|
3320
|
+
* 2. On success → `reportSuccess(provider.id)` and return.
|
|
3321
|
+
* 3. On a GENUINE error → `reportError(error, provider.id)` (marks THAT
|
|
3322
|
+
* provider success:false + clears the selection cache) and try the next
|
|
3323
|
+
* capable candidate.
|
|
3324
|
+
* 4. When every capable candidate has been tried, re-throw the last error so
|
|
3325
|
+
* the caller's retry/dead-letter logic still triggers.
|
|
3326
|
+
* 5. If there is NO capable provider at all, throw a clear error.
|
|
3274
3327
|
*
|
|
3275
3328
|
* Returns the same `Transaction[]` `TonClient.getTransactions` returns, so a
|
|
3276
3329
|
* consumer can swap `client.getTransactions(addr, opts)` for
|
|
@@ -3283,28 +3336,27 @@ var NodeAdapter = class {
|
|
|
3283
3336
|
}
|
|
3284
3337
|
const addr = typeof address === "string" ? core.Address.parse(address) : address;
|
|
3285
3338
|
const reqOpts = { limit: opts.limit ?? 20, ...opts };
|
|
3286
|
-
const
|
|
3339
|
+
const candidates = this.manager.getTransactionCapableProviders();
|
|
3340
|
+
if (candidates.length === 0) {
|
|
3341
|
+
throw new Error(
|
|
3342
|
+
`getTransactions: no transaction-capable provider for ${network}`
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3287
3345
|
const rateLimiter = this.manager.getRateLimiter();
|
|
3288
|
-
const tried = /* @__PURE__ */ new Set();
|
|
3289
3346
|
let lastError;
|
|
3290
|
-
for (
|
|
3291
|
-
const provider = this.manager.getActiveProvider();
|
|
3292
|
-
if (!provider) break;
|
|
3293
|
-
if (tried.has(provider.id)) break;
|
|
3294
|
-
tried.add(provider.id);
|
|
3347
|
+
for (const provider of candidates) {
|
|
3295
3348
|
if (rateLimiter) {
|
|
3296
3349
|
await rateLimiter.acquire(provider.id, timeoutMs);
|
|
3297
3350
|
}
|
|
3298
|
-
const endpoint =
|
|
3299
|
-
const
|
|
3300
|
-
const client = new ton.TonClient({ endpoint, apiKey });
|
|
3351
|
+
const endpoint = normalizeV2Endpoint(provider.endpointV2, provider);
|
|
3352
|
+
const client = new ton.TonClient({ endpoint, apiKey: provider.apiKey });
|
|
3301
3353
|
try {
|
|
3302
3354
|
const result = await withTimeout(
|
|
3303
3355
|
client.getTransactions(addr, reqOpts),
|
|
3304
3356
|
timeoutMs,
|
|
3305
3357
|
`getTransactions(${provider.id})`
|
|
3306
3358
|
);
|
|
3307
|
-
this.manager.reportSuccess();
|
|
3359
|
+
this.manager.reportSuccess(provider.id);
|
|
3308
3360
|
return result;
|
|
3309
3361
|
} catch (error) {
|
|
3310
3362
|
lastError = error;
|
|
@@ -3312,10 +3364,10 @@ var NodeAdapter = class {
|
|
|
3312
3364
|
`getTransactions failed on ${provider.id}, failing over`,
|
|
3313
3365
|
{ error: error?.message || String(error) }
|
|
3314
3366
|
);
|
|
3315
|
-
this.manager.reportError(error);
|
|
3367
|
+
this.manager.reportError(error, provider.id);
|
|
3316
3368
|
}
|
|
3317
3369
|
}
|
|
3318
|
-
throw lastError || new Error(`getTransactions:
|
|
3370
|
+
throw lastError || new Error(`getTransactions: all transaction-capable providers failed for ${network}`);
|
|
3319
3371
|
}
|
|
3320
3372
|
// ========================================================================
|
|
3321
3373
|
// Direct REST API Methods
|
|
@@ -3421,30 +3473,106 @@ var NodeAdapter = class {
|
|
|
3421
3473
|
}
|
|
3422
3474
|
}
|
|
3423
3475
|
/**
|
|
3424
|
-
*
|
|
3476
|
+
* Broadcast an already-signed external message BOC, with provider failover.
|
|
3477
|
+
*
|
|
3478
|
+
* Unlike a single-endpoint `client.sendFile`/raw POST, this walks the network's
|
|
3479
|
+
* score-ordered providers (best first; see ProviderManager.getBroadcastCapableProviders)
|
|
3480
|
+
* and fails over to the next provider when one returns a TRANSIENT failure — a
|
|
3481
|
+
* 429 (rate limit), a 5xx (gateway/server error, e.g. the testnet chainstack
|
|
3482
|
+
* free plan 500ing on sendBoc), a timeout, or a network error. This closes the
|
|
3483
|
+
* broadcast half of the failover story: the getTransactions capability flag only
|
|
3484
|
+
* diverted reads, so a demoted-but-still-reachable provider could still 500 a
|
|
3485
|
+
* broadcast — now the broadcast itself moves on to a healthy provider.
|
|
3486
|
+
*
|
|
3487
|
+
* Safety: re-broadcasting is idempotent at the TON level — the external message
|
|
3488
|
+
* has a deterministic hash, so forwarding the SAME signed BOC to another provider
|
|
3489
|
+
* after a transient failure is safe (the network dedups; a duplicate is a no-op).
|
|
3490
|
+
* A 4xx that means "this BOC is invalid" (400/413/422) is therefore NOT failed
|
|
3491
|
+
* over: it is the payload, not the provider — surfaced immediately, without
|
|
3492
|
+
* poisoning the provider's health or spraying a bad BOC across the fleet.
|
|
3425
3493
|
*/
|
|
3426
3494
|
async sendBoc(boc, timeoutMs = 3e4) {
|
|
3427
|
-
const
|
|
3495
|
+
const network = this.manager.getNetwork();
|
|
3496
|
+
if (!network) {
|
|
3497
|
+
throw new Error("ProviderManager not initialized");
|
|
3498
|
+
}
|
|
3499
|
+
const bocBase64 = typeof boc === "string" ? boc : boc.toString("base64");
|
|
3500
|
+
const candidates = this.manager.getBroadcastCapableProviders();
|
|
3501
|
+
if (candidates.length === 0) {
|
|
3502
|
+
const endpoint = await this.manager.getEndpoint();
|
|
3503
|
+
try {
|
|
3504
|
+
await this.sendBocToEndpoint(endpoint, bocBase64, timeoutMs);
|
|
3505
|
+
this.manager.reportSuccess();
|
|
3506
|
+
} catch (error) {
|
|
3507
|
+
if (!this.isInvalidPayloadError(error)) {
|
|
3508
|
+
this.manager.reportError(error);
|
|
3509
|
+
}
|
|
3510
|
+
throw error;
|
|
3511
|
+
}
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
const rateLimiter = this.manager.getRateLimiter();
|
|
3515
|
+
let lastError;
|
|
3516
|
+
for (const provider of candidates) {
|
|
3517
|
+
if (rateLimiter) {
|
|
3518
|
+
await rateLimiter.acquire(provider.id, timeoutMs);
|
|
3519
|
+
}
|
|
3520
|
+
const endpoint = normalizeV2Endpoint(provider.endpointV2, provider);
|
|
3521
|
+
try {
|
|
3522
|
+
await this.sendBocToEndpoint(endpoint, bocBase64, timeoutMs);
|
|
3523
|
+
this.manager.reportSuccess(provider.id);
|
|
3524
|
+
return;
|
|
3525
|
+
} catch (error) {
|
|
3526
|
+
if (this.isInvalidPayloadError(error)) {
|
|
3527
|
+
throw error;
|
|
3528
|
+
}
|
|
3529
|
+
lastError = error;
|
|
3530
|
+
this.manager.reportError(error, provider.id);
|
|
3531
|
+
this.logger.warn(
|
|
3532
|
+
`sendBoc failed on ${provider.id}, failing over`,
|
|
3533
|
+
{ error: error?.message || String(error) }
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
throw lastError || new Error(`sendBoc: all providers failed for ${network}`);
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* POST a base64 BOC to one provider's REST `/sendBoc`. Throws on a non-2xx
|
|
3541
|
+
* response with the HTTP `status` attached (so the caller can classify
|
|
3542
|
+
* transient vs invalid-payload), and on an `ok:false` JSON body.
|
|
3543
|
+
*/
|
|
3544
|
+
async sendBocToEndpoint(endpoint, bocBase64, timeoutMs) {
|
|
3428
3545
|
const baseV2 = toV2Base(endpoint);
|
|
3429
3546
|
const url = `${baseV2}/sendBoc`;
|
|
3430
|
-
const
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
{
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3547
|
+
const response = await fetchWithTimeout(
|
|
3548
|
+
url,
|
|
3549
|
+
{
|
|
3550
|
+
method: "POST",
|
|
3551
|
+
headers: { "Content-Type": "application/json" },
|
|
3552
|
+
body: JSON.stringify({ boc: bocBase64 })
|
|
3553
|
+
},
|
|
3554
|
+
timeoutMs
|
|
3555
|
+
);
|
|
3556
|
+
if (!response.ok) {
|
|
3557
|
+
const err = new Error(
|
|
3558
|
+
`HTTP ${response.status} ${response.statusText} on sendBoc`
|
|
3440
3559
|
);
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
this.manager.reportSuccess();
|
|
3444
|
-
} catch (error) {
|
|
3445
|
-
this.manager.reportError(error);
|
|
3446
|
-
throw error;
|
|
3560
|
+
err.status = response.status;
|
|
3561
|
+
throw err;
|
|
3447
3562
|
}
|
|
3563
|
+
const json = await response.json();
|
|
3564
|
+
this.unwrapResponse(json);
|
|
3565
|
+
}
|
|
3566
|
+
/**
|
|
3567
|
+
* True only for a 4xx that means the BOC payload itself is invalid (400 Bad
|
|
3568
|
+
* Request, 413 Payload Too Large, 422 Unprocessable) — these must NOT fail over
|
|
3569
|
+
* (the next provider would reject the same BOC identically). Auth/availability
|
|
3570
|
+
* 4xx (401/403/404), 429, 5xx, timeouts and network errors are all transient
|
|
3571
|
+
* for an idempotent broadcast and DO fail over.
|
|
3572
|
+
*/
|
|
3573
|
+
isInvalidPayloadError(error) {
|
|
3574
|
+
const status = error?.status;
|
|
3575
|
+
return typeof status === "number" && (status === 400 || status === 413 || status === 422);
|
|
3448
3576
|
}
|
|
3449
3577
|
/**
|
|
3450
3578
|
* Check if contract is deployed
|