ton-provider-system 0.5.0 → 0.7.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
@@ -329,30 +329,28 @@ var DEFAULT_PROVIDERS = {
329
329
  },
330
330
  rps: 1,
331
331
  // Without API key
332
- // Preferred on testnet: Toncenter serves the full v2 surface incl.
333
- // getTransactions (curl-proven 200). Orbs only proxies liteserver
334
- // get-methods/state and 403s on getTransactions, so it must NOT be the
335
- // primary testnet provider (it stays a fallback for get-method reads).
336
- // Selection is score-based and priority is the lever (lower = better),
337
- // so this gives Toncenter the testnet edge over Orbs (priority 90).
338
- priority: 10,
332
+ // Primary testnet provider: Toncenter serves the full v2 surface incl.
333
+ // getTransactions (curl-proven 200). Selection is score-based and priority
334
+ // is the lever (lower = better), so priority 1 keeps it ahead of Tatum.
335
+ priority: 1,
339
336
  enabled: true,
340
337
  description: "Official TON Center public endpoint"
341
338
  },
342
- orbs_testnet: {
343
- name: "Orbs TON Access Testnet",
344
- type: "orbs",
339
+ tatum_testnet: {
340
+ name: "Tatum Testnet",
341
+ type: "tatum",
345
342
  network: "testnet",
346
343
  endpoints: {
347
- v2: "https://ton-testnet.orbs.network/api/v2"
344
+ v2: "https://ton-testnet.gateway.tatum.io"
348
345
  },
349
- rps: 10,
350
- // Demoted below toncenter_testnet (priority 10): Orbs is the decentralised
351
- // fallback for non-transaction (get-method/state) testnet reads only.
352
- priority: 90,
346
+ apiKeyEnvVar: "TATUM_API_KEY_TESTNET",
347
+ rps: 3,
348
+ // Secondary testnet provider (priority 2): also serves the full workload
349
+ // (getTransactions/sendBoc 200), so it backs up Toncenter on failover.
350
+ priority: 2,
353
351
  enabled: true,
354
- isDynamic: true,
355
- description: "Decentralized gateway - no API key needed"
352
+ browserCompatible: false,
353
+ description: "Tatum testnet gateway - requires TATUM_API_KEY_TESTNET"
356
354
  },
357
355
  toncenter_mainnet: {
358
356
  name: "TON Center Mainnet",
@@ -386,12 +384,12 @@ function createDefaultConfig() {
386
384
  version: "1.0",
387
385
  providers: { ...DEFAULT_PROVIDERS },
388
386
  defaults: {
389
- // Testnet: Toncenter first (transactions-capable), Orbs as fallback.
390
- // This default order only governs the no-healthy-providers fallback
391
- // path; the primary, score-based selection is driven by priority
392
- // (see toncenter_testnet/orbs_testnet above). Mainnet order is left
387
+ // Testnet: only the transaction-capable providers Toncenter first,
388
+ // Tatum as failover. This default order governs the no-healthy-providers
389
+ // fallback path; the primary, score-based selection is driven by priority
390
+ // (see toncenter_testnet/tatum_testnet above). Mainnet order is left
393
391
  // unchanged (Orbs stays primary there — no regression).
394
- testnet: ["toncenter_testnet", "orbs_testnet"],
392
+ testnet: ["toncenter_testnet", "tatum_testnet"],
395
393
  mainnet: ["orbs_mainnet", "toncenter_mainnet"]
396
394
  }
397
395
  };
@@ -3069,6 +3067,22 @@ var _ProviderManager = class _ProviderManager {
3069
3067
  if (!this.initialized || !this.network) return [];
3070
3068
  return this.selector.getAvailableProviders(this.network).filter((p) => p.servesGetTransactions !== false);
3071
3069
  }
3070
+ /**
3071
+ * Get broadcast-capable providers for the current network, in selector score
3072
+ * order (best first).
3073
+ *
3074
+ * Unlike getTransactionCapableProviders, this applies NO capability filter:
3075
+ * `sendBoc` (forwarding an already-signed external message) works on EVERY
3076
+ * provider type — including the liteserver-proxy providers (Chainstack/Orbs)
3077
+ * that 403 only on the v2 `getTransactions` shape. The list is the plain
3078
+ * score-ordered available set, so a broadcast walks best→worst and a provider
3079
+ * that 429s/5xxs on sendBoc is skipped in favour of the next one.
3080
+ * Returns [] when no provider is currently selectable.
3081
+ */
3082
+ getBroadcastCapableProviders() {
3083
+ if (!this.initialized || !this.network) return [];
3084
+ return this.selector.getAvailableProviders(this.network);
3085
+ }
3072
3086
  /**
3073
3087
  * Get provider health results for current network
3074
3088
  */
@@ -3457,30 +3471,106 @@ var NodeAdapter = class {
3457
3471
  }
3458
3472
  }
3459
3473
  /**
3460
- * Send BOC via REST API
3474
+ * Broadcast an already-signed external message BOC, with provider failover.
3475
+ *
3476
+ * Unlike a single-endpoint `client.sendFile`/raw POST, this walks the network's
3477
+ * score-ordered providers (best first; see ProviderManager.getBroadcastCapableProviders)
3478
+ * and fails over to the next provider when one returns a TRANSIENT failure — a
3479
+ * 429 (rate limit), a 5xx (gateway/server error, e.g. the testnet chainstack
3480
+ * free plan 500ing on sendBoc), a timeout, or a network error. This closes the
3481
+ * broadcast half of the failover story: the getTransactions capability flag only
3482
+ * diverted reads, so a demoted-but-still-reachable provider could still 500 a
3483
+ * broadcast — now the broadcast itself moves on to a healthy provider.
3484
+ *
3485
+ * Safety: re-broadcasting is idempotent at the TON level — the external message
3486
+ * has a deterministic hash, so forwarding the SAME signed BOC to another provider
3487
+ * after a transient failure is safe (the network dedups; a duplicate is a no-op).
3488
+ * A 4xx that means "this BOC is invalid" (400/413/422) is therefore NOT failed
3489
+ * over: it is the payload, not the provider — surfaced immediately, without
3490
+ * poisoning the provider's health or spraying a bad BOC across the fleet.
3461
3491
  */
3462
3492
  async sendBoc(boc, timeoutMs = 3e4) {
3463
- const endpoint = await this.manager.getEndpoint();
3493
+ const network = this.manager.getNetwork();
3494
+ if (!network) {
3495
+ throw new Error("ProviderManager not initialized");
3496
+ }
3497
+ const bocBase64 = typeof boc === "string" ? boc : boc.toString("base64");
3498
+ const candidates = this.manager.getBroadcastCapableProviders();
3499
+ if (candidates.length === 0) {
3500
+ const endpoint = await this.manager.getEndpoint();
3501
+ try {
3502
+ await this.sendBocToEndpoint(endpoint, bocBase64, timeoutMs);
3503
+ this.manager.reportSuccess();
3504
+ } catch (error) {
3505
+ if (!this.isInvalidPayloadError(error)) {
3506
+ this.manager.reportError(error);
3507
+ }
3508
+ throw error;
3509
+ }
3510
+ return;
3511
+ }
3512
+ const rateLimiter = this.manager.getRateLimiter();
3513
+ let lastError;
3514
+ for (const provider of candidates) {
3515
+ if (rateLimiter) {
3516
+ await rateLimiter.acquire(provider.id, timeoutMs);
3517
+ }
3518
+ const endpoint = normalizeV2Endpoint(provider.endpointV2, provider);
3519
+ try {
3520
+ await this.sendBocToEndpoint(endpoint, bocBase64, timeoutMs);
3521
+ this.manager.reportSuccess(provider.id);
3522
+ return;
3523
+ } catch (error) {
3524
+ if (this.isInvalidPayloadError(error)) {
3525
+ throw error;
3526
+ }
3527
+ lastError = error;
3528
+ this.manager.reportError(error, provider.id);
3529
+ this.logger.warn(
3530
+ `sendBoc failed on ${provider.id}, failing over`,
3531
+ { error: error?.message || String(error) }
3532
+ );
3533
+ }
3534
+ }
3535
+ throw lastError || new Error(`sendBoc: all providers failed for ${network}`);
3536
+ }
3537
+ /**
3538
+ * POST a base64 BOC to one provider's REST `/sendBoc`. Throws on a non-2xx
3539
+ * response with the HTTP `status` attached (so the caller can classify
3540
+ * transient vs invalid-payload), and on an `ok:false` JSON body.
3541
+ */
3542
+ async sendBocToEndpoint(endpoint, bocBase64, timeoutMs) {
3464
3543
  const baseV2 = toV2Base(endpoint);
3465
3544
  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
3545
+ const response = await fetchWithTimeout(
3546
+ url,
3547
+ {
3548
+ method: "POST",
3549
+ headers: { "Content-Type": "application/json" },
3550
+ body: JSON.stringify({ boc: bocBase64 })
3551
+ },
3552
+ timeoutMs
3553
+ );
3554
+ if (!response.ok) {
3555
+ const err = new Error(
3556
+ `HTTP ${response.status} ${response.statusText} on sendBoc`
3476
3557
  );
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;
3558
+ err.status = response.status;
3559
+ throw err;
3483
3560
  }
3561
+ const json = await response.json();
3562
+ this.unwrapResponse(json);
3563
+ }
3564
+ /**
3565
+ * True only for a 4xx that means the BOC payload itself is invalid (400 Bad
3566
+ * Request, 413 Payload Too Large, 422 Unprocessable) — these must NOT fail over
3567
+ * (the next provider would reject the same BOC identically). Auth/availability
3568
+ * 4xx (401/403/404), 429, 5xx, timeouts and network errors are all transient
3569
+ * for an idempotent broadcast and DO fail over.
3570
+ */
3571
+ isInvalidPayloadError(error) {
3572
+ const status = error?.status;
3573
+ return typeof status === "number" && (status === 400 || status === 413 || status === 422);
3484
3574
  }
3485
3575
  /**
3486
3576
  * Check if contract is deployed