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 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 provider = this.selector.getBestProvider(this.network);
2893
- if (provider) {
2894
- this.rateLimiter.reportSuccess(provider.id);
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 (activeProviderId) {
2905
- provider = this.registry.getProvider(activeProviderId) || null;
2906
- }
2907
- if (!provider) {
2908
- provider = this.selector.getBestProvider(this.network);
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 loops across the network's
3259
- * providers: a provider that passes the `getMasterchainInfo` health probe but
3260
- * fails the v2 `getTransactions` call (e.g. Chainstack/Orbs testnet, which
3261
- * 403 on transaction reads) is no longer able to permanently break this path.
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. Pick the current best provider (the selector's scored top).
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
- * 3. On success → `reportSuccess()` and return.
3268
- * 4. On error → `reportError()` (marks the provider success:false, scoring 0
3269
- * within its cooldown, and clears the selection cache) so the next pick is
3270
- * a DIFFERENT, still-eligible provider, then retry.
3271
- * 5. When every candidate has been tried, re-throw the last error so the
3272
- * caller's retry/dead-letter logic still triggers — but only after a
3273
- * genuine failover across all providers.
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 maxAttempts = Math.max(1, this.manager.getProviders().length);
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 (let attempt = 0; attempt < maxAttempts; attempt++) {
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 = await this.manager.getEndpoint();
3299
- const apiKey = this.manager.getActiveProvider()?.apiKey;
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: no available provider for ${network}`);
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
- * Send BOC via REST API
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 endpoint = await this.manager.getEndpoint();
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 bocBase64 = typeof boc === "string" ? boc : boc.toString("base64");
3431
- try {
3432
- const response = await fetchWithTimeout(
3433
- url,
3434
- {
3435
- method: "POST",
3436
- headers: { "Content-Type": "application/json" },
3437
- body: JSON.stringify({ boc: bocBase64 })
3438
- },
3439
- timeoutMs
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
- const json = await response.json();
3442
- this.unwrapResponse(json);
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