ton-provider-system 0.5.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
@@ -3069,6 +3069,22 @@ var _ProviderManager = class _ProviderManager {
3069
3069
  if (!this.initialized || !this.network) return [];
3070
3070
  return this.selector.getAvailableProviders(this.network).filter((p) => p.servesGetTransactions !== false);
3071
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
+ }
3072
3088
  /**
3073
3089
  * Get provider health results for current network
3074
3090
  */
@@ -3457,30 +3473,106 @@ var NodeAdapter = class {
3457
3473
  }
3458
3474
  }
3459
3475
  /**
3460
- * 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.
3461
3493
  */
3462
3494
  async sendBoc(boc, timeoutMs = 3e4) {
3463
- 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) {
3464
3545
  const baseV2 = toV2Base(endpoint);
3465
3546
  const url = `${baseV2}/sendBoc`;
3466
- const bocBase64 = typeof boc === "string" ? boc : boc.toString("base64");
3467
- try {
3468
- const response = await fetchWithTimeout(
3469
- url,
3470
- {
3471
- method: "POST",
3472
- headers: { "Content-Type": "application/json" },
3473
- body: JSON.stringify({ boc: bocBase64 })
3474
- },
3475
- 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`
3476
3559
  );
3477
- const json = await response.json();
3478
- this.unwrapResponse(json);
3479
- this.manager.reportSuccess();
3480
- } catch (error) {
3481
- this.manager.reportError(error);
3482
- throw error;
3560
+ err.status = response.status;
3561
+ throw err;
3483
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);
3484
3576
  }
3485
3577
  /**
3486
3578
  * Check if contract is deployed