ton-provider-system 0.1.13 → 0.2.1

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.js CHANGED
@@ -554,183 +554,461 @@ function createRegistryFromData(data, logger) {
554
554
  return new ProviderRegistry(mergedConfig, logger);
555
555
  }
556
556
 
557
- // src/utils/endpoint.ts
558
- function normalizeV2Endpoint(endpoint) {
559
- let normalized = endpoint.trim();
560
- if (normalized.endsWith("/")) {
561
- normalized = normalized.slice(0, -1);
557
+ // src/providers/base.ts
558
+ var BaseProvider = class {
559
+ constructor(provider) {
560
+ this.provider = provider;
562
561
  }
563
- if (normalized.toLowerCase().endsWith("/jsonrpc")) {
564
- return normalized;
562
+ /**
563
+ * Get the provider instance
564
+ */
565
+ getProvider() {
566
+ return this.provider;
565
567
  }
566
- if (normalized.includes("gateway.tatum.io")) {
567
- try {
568
- const url = new URL(normalized);
569
- if (!url.pathname || url.pathname === "/") {
570
- return normalized + "/jsonRPC";
568
+ /**
569
+ * Build a JSON-RPC request body.
570
+ * Most providers use standard JSON-RPC 2.0, but some may need modifications.
571
+ */
572
+ buildRequest(method, params = {}) {
573
+ return {
574
+ id: "1",
575
+ jsonrpc: "2.0",
576
+ method,
577
+ params
578
+ };
579
+ }
580
+ /**
581
+ * Parse response from provider.
582
+ * Handles provider-specific response formats.
583
+ */
584
+ parseResponse(data) {
585
+ if (data && typeof data === "object") {
586
+ const dataObj = data;
587
+ if ("ok" in dataObj) {
588
+ if (!dataObj.ok) {
589
+ const error = dataObj.error;
590
+ throw new Error(error || "API returned ok=false");
591
+ }
592
+ const result = dataObj.result;
593
+ return result || dataObj;
571
594
  }
572
- if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
573
- return normalized + "/jsonRPC";
595
+ if ("result" in dataObj) {
596
+ return dataObj.result;
574
597
  }
575
- } catch {
598
+ if ("last" in dataObj || "@type" in dataObj) {
599
+ return dataObj;
600
+ }
601
+ if ("error" in dataObj) {
602
+ const errorObj = dataObj.error;
603
+ const errorMsg = typeof errorObj === "string" ? errorObj : errorObj?.message || errorObj?.code || String(errorObj);
604
+ throw new Error(`API error: ${errorMsg}`);
605
+ }
606
+ }
607
+ return data;
608
+ }
609
+ /**
610
+ * Parse masterchain info from response.
611
+ * Validates the response structure and extracts seqno.
612
+ */
613
+ parseMasterchainInfo(data) {
614
+ const info = this.parseResponse(data);
615
+ if (!info || typeof info !== "object") {
616
+ throw new Error("Invalid response structure");
617
+ }
618
+ const infoObj = info;
619
+ const seqno = infoObj.last?.seqno;
620
+ if (seqno === void 0 || seqno === null || seqno <= 0 || !Number.isInteger(seqno)) {
621
+ throw new Error(`Invalid seqno: ${seqno} (must be positive integer)`);
622
+ }
623
+ return info;
624
+ }
625
+ /**
626
+ * Validate provider configuration.
627
+ * Checks if required API keys are present, etc.
628
+ */
629
+ validateConfig() {
630
+ return { valid: true };
631
+ }
632
+ /**
633
+ * Get the normalized endpoint for this provider.
634
+ */
635
+ getNormalizedEndpoint() {
636
+ return this.normalizeEndpoint(this.provider.endpointV2);
637
+ }
638
+ /**
639
+ * Check if this provider requires an API key.
640
+ */
641
+ requiresApiKey() {
642
+ return false;
643
+ }
644
+ /**
645
+ * Check if API key is present (if required).
646
+ */
647
+ hasApiKey() {
648
+ return !!this.provider.apiKey;
649
+ }
650
+ };
651
+
652
+ // src/providers/chainstack.ts
653
+ var ChainstackProvider = class extends BaseProvider {
654
+ constructor(provider) {
655
+ super(provider);
656
+ }
657
+ normalizeEndpoint(endpoint) {
658
+ let normalized = endpoint.trim();
659
+ if (normalized.endsWith("/")) {
660
+ normalized = normalized.slice(0, -1);
661
+ }
662
+ if (normalized.toLowerCase().endsWith("/jsonrpc")) {
663
+ return normalized;
664
+ }
665
+ if (normalized.endsWith("/api/v2")) {
576
666
  return normalized + "/jsonRPC";
577
667
  }
668
+ if (normalized.endsWith("/api/v3")) {
669
+ return normalized.replace("/api/v3", "/api/v2/jsonRPC");
670
+ }
671
+ return normalized;
578
672
  }
579
- if (normalized.includes("onfinality.io")) {
580
- try {
581
- const url = new URL(normalized);
582
- const baseUrl = normalized.split("?")[0];
583
- if (!url.pathname || url.pathname === "/") {
584
- const apikey = url.searchParams.get("apikey");
585
- if (apikey && apikey !== "{key}" && apikey.length > 0) {
586
- return baseUrl.replace(/\/?$/, "/rpc");
673
+ buildHeaders() {
674
+ return {
675
+ "Content-Type": "application/json"
676
+ };
677
+ }
678
+ validateConfig() {
679
+ if (this.provider.endpointV2.includes("{key}")) {
680
+ return {
681
+ valid: false,
682
+ error: "Chainstack API key not resolved in endpoint URL"
683
+ };
684
+ }
685
+ return { valid: true };
686
+ }
687
+ };
688
+
689
+ // src/providers/tatum.ts
690
+ var TatumProvider = class extends BaseProvider {
691
+ constructor(provider) {
692
+ super(provider);
693
+ }
694
+ normalizeEndpoint(endpoint) {
695
+ let normalized = endpoint.trim();
696
+ if (normalized.endsWith("/")) {
697
+ normalized = normalized.slice(0, -1);
698
+ }
699
+ if (normalized.toLowerCase().endsWith("/jsonrpc")) {
700
+ return normalized;
701
+ }
702
+ if (normalized.includes("gateway.tatum.io")) {
703
+ try {
704
+ const url = new URL(normalized);
705
+ if (!url.pathname || url.pathname === "/") {
706
+ return normalized + "/jsonRPC";
587
707
  }
588
- return baseUrl.replace(/\/?$/, "/public");
708
+ if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
709
+ return normalized + "/jsonRPC";
710
+ }
711
+ } catch {
712
+ return normalized + "/jsonRPC";
589
713
  }
590
- if (url.pathname === "/rpc" || url.pathname === "/public") {
714
+ }
715
+ return normalized;
716
+ }
717
+ buildHeaders() {
718
+ const headers = {
719
+ "Content-Type": "application/json"
720
+ };
721
+ if (this.provider.apiKey) {
722
+ headers["x-api-key"] = this.provider.apiKey;
723
+ }
724
+ return headers;
725
+ }
726
+ requiresApiKey() {
727
+ return true;
728
+ }
729
+ validateConfig() {
730
+ if (!this.provider.apiKey) {
731
+ return {
732
+ valid: false,
733
+ error: "Tatum provider requires API key (set TATUM_API_KEY_TESTNET or TATUM_API_KEY_MAINNET)"
734
+ };
735
+ }
736
+ return { valid: true };
737
+ }
738
+ };
739
+
740
+ // src/providers/onfinality.ts
741
+ var OnFinalityProvider = class extends BaseProvider {
742
+ constructor(provider) {
743
+ super(provider);
744
+ }
745
+ normalizeEndpoint(endpoint) {
746
+ let normalized = endpoint.trim();
747
+ if (normalized.endsWith("/")) {
748
+ normalized = normalized.slice(0, -1);
749
+ }
750
+ if (normalized.includes("onfinality.io")) {
751
+ try {
752
+ const url = new URL(normalized);
753
+ const baseUrl = normalized.split("?")[0];
754
+ if (!url.pathname || url.pathname === "/") {
755
+ if (this.provider.apiKey) {
756
+ return baseUrl.replace(/\/?$/, "/rpc");
757
+ }
758
+ return baseUrl.replace(/\/?$/, "/public");
759
+ }
760
+ if (url.pathname === "/rpc" || url.pathname === "/public") {
761
+ return baseUrl;
762
+ }
591
763
  return baseUrl;
764
+ } catch {
765
+ if (normalized.includes("{key}")) {
766
+ return normalized.split("?")[0].replace(/\/?$/, "/public");
767
+ }
768
+ if (!normalized.includes("/rpc") && !normalized.includes("/public")) {
769
+ return normalized.split("?")[0] + "/public";
770
+ }
771
+ return normalized.split("?")[0];
592
772
  }
593
- return baseUrl;
594
- } catch {
595
- if (normalized.includes("{key}")) {
596
- return normalized.split("?")[0].replace(/\/?$/, "/public");
597
- }
598
- if (!normalized.includes("/rpc") && !normalized.includes("/public")) {
599
- return normalized.split("?")[0] + "/public";
600
- }
601
- return normalized.split("?")[0];
602
773
  }
774
+ return normalized;
603
775
  }
604
- if (normalized.endsWith("/api/v2")) {
605
- return normalized + "/jsonRPC";
776
+ buildHeaders() {
777
+ const headers = {
778
+ "Content-Type": "application/json"
779
+ };
780
+ if (this.provider.apiKey) {
781
+ headers["apikey"] = this.provider.apiKey;
782
+ }
783
+ return headers;
606
784
  }
607
- if (normalized.endsWith("/api/v3")) {
608
- return normalized.replace("/api/v3", "/api/v2/jsonRPC");
785
+ parseResponse(data) {
786
+ if (typeof data === "string") {
787
+ if (data.includes("Backend error") || data.includes("backend error")) {
788
+ throw new Error(`OnFinality backend error: ${data}`);
789
+ }
790
+ }
791
+ return super.parseResponse(data);
609
792
  }
610
- if (normalized.includes("quiknode.pro") || normalized.includes("getblock.io")) {
611
- try {
612
- const url = new URL(normalized);
613
- if (!url.pathname || url.pathname === "/") {
793
+ };
794
+
795
+ // src/providers/quicknode.ts
796
+ var QuickNodeProvider = class extends BaseProvider {
797
+ constructor(provider) {
798
+ super(provider);
799
+ }
800
+ normalizeEndpoint(endpoint) {
801
+ let normalized = endpoint.trim();
802
+ if (normalized.endsWith("/")) {
803
+ normalized = normalized.slice(0, -1);
804
+ }
805
+ if (normalized.toLowerCase().endsWith("/jsonrpc")) {
806
+ return normalized;
807
+ }
808
+ if (normalized.includes("quiknode.pro")) {
809
+ try {
810
+ const url = new URL(normalized);
811
+ if (!url.pathname || url.pathname === "/") {
812
+ return normalized + "/jsonRPC";
813
+ }
814
+ if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
815
+ return normalized + "/jsonRPC";
816
+ }
817
+ } catch {
614
818
  return normalized + "/jsonRPC";
615
819
  }
616
- if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
820
+ }
821
+ return normalized;
822
+ }
823
+ buildHeaders() {
824
+ return {
825
+ "Content-Type": "application/json"
826
+ };
827
+ }
828
+ validateConfig() {
829
+ if (this.provider.endpointV2.includes("{key}")) {
830
+ return {
831
+ valid: false,
832
+ error: "QuickNode API key not resolved in endpoint URL"
833
+ };
834
+ }
835
+ return { valid: true };
836
+ }
837
+ };
838
+
839
+ // src/providers/getblock.ts
840
+ var GetBlockProvider = class extends BaseProvider {
841
+ constructor(provider) {
842
+ super(provider);
843
+ }
844
+ normalizeEndpoint(endpoint) {
845
+ let normalized = endpoint.trim();
846
+ if (normalized.endsWith("/")) {
847
+ normalized = normalized.slice(0, -1);
848
+ }
849
+ if (normalized.toLowerCase().endsWith("/jsonrpc")) {
850
+ return normalized;
851
+ }
852
+ if (normalized.includes("getblock.io")) {
853
+ try {
854
+ const url = new URL(normalized);
855
+ if (!url.pathname || url.pathname === "/") {
856
+ return normalized + "/jsonRPC";
857
+ }
858
+ if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
859
+ return normalized + "/jsonRPC";
860
+ }
861
+ } catch {
617
862
  return normalized + "/jsonRPC";
618
863
  }
619
- } catch {
620
- return normalized + "/jsonRPC";
621
864
  }
865
+ return normalized;
622
866
  }
623
- try {
624
- const url = new URL(normalized);
625
- if (!url.pathname || url.pathname === "/") {
626
- return normalized + "/jsonRPC";
867
+ buildHeaders() {
868
+ const headers = {
869
+ "Content-Type": "application/json"
870
+ };
871
+ if (this.provider.apiKey) {
872
+ headers["x-api-key"] = this.provider.apiKey;
627
873
  }
628
- } catch {
629
- }
630
- return normalized;
631
- }
632
- function toV2Base(endpoint) {
633
- let normalized = endpoint.trim();
634
- if (normalized.endsWith("/")) {
635
- normalized = normalized.slice(0, -1);
874
+ return headers;
636
875
  }
637
- if (normalized.toLowerCase().endsWith("/jsonrpc")) {
638
- normalized = normalized.slice(0, -8);
876
+ requiresApiKey() {
877
+ return true;
639
878
  }
640
- normalized = normalized.replace(/\/api\/v3\b/, "/api/v2");
641
- if (!normalized.endsWith("/api/v2")) {
642
- if (normalized.includes("/api/v2")) {
643
- const idx = normalized.indexOf("/api/v2");
644
- normalized = normalized.slice(0, idx + 7);
879
+ validateConfig() {
880
+ if (!this.provider.apiKey) {
881
+ return {
882
+ valid: false,
883
+ error: "GetBlock provider requires API key"
884
+ };
645
885
  }
886
+ return { valid: true };
646
887
  }
647
- return normalized;
648
- }
649
- function toV3Base(endpoint) {
650
- const normalized = toV2Base(endpoint);
651
- return normalized.replace("/api/v2", "/api/v3");
652
- }
653
- function getBaseUrl(endpoint) {
654
- try {
655
- const url = new URL(endpoint);
656
- return `${url.protocol}//${url.host}`;
657
- } catch {
658
- return endpoint;
888
+ };
889
+
890
+ // src/providers/toncenter.ts
891
+ var TonCenterProvider = class extends BaseProvider {
892
+ constructor(provider) {
893
+ super(provider);
659
894
  }
660
- }
661
- function isChainstackUrl(url) {
662
- try {
663
- const parsed = new URL(url.trim());
664
- return parsed.hostname.includes("chainstack.com");
665
- } catch {
666
- return false;
895
+ normalizeEndpoint(endpoint) {
896
+ let normalized = endpoint.trim();
897
+ if (normalized.endsWith("/")) {
898
+ normalized = normalized.slice(0, -1);
899
+ }
900
+ if (normalized.toLowerCase().endsWith("/jsonrpc")) {
901
+ return normalized;
902
+ }
903
+ if (normalized.endsWith("/api/v2")) {
904
+ return normalized + "/jsonRPC";
905
+ }
906
+ if (normalized.endsWith("/api/v3")) {
907
+ return normalized.replace("/api/v3", "/api/v2/jsonRPC");
908
+ }
909
+ return normalized;
667
910
  }
668
- }
669
- function isQuickNodeUrl(url) {
670
- try {
671
- const parsed = new URL(url.trim());
672
- return parsed.hostname.includes("quiknode.pro");
673
- } catch {
674
- return false;
911
+ buildHeaders() {
912
+ return {
913
+ "Content-Type": "application/json"
914
+ };
675
915
  }
676
- }
677
- function isTonCenterUrl(url) {
678
- try {
679
- const parsed = new URL(url.trim());
680
- return parsed.hostname.includes("toncenter.com");
681
- } catch {
916
+ // TON Center API key is optional (1 RPS without key, 10 RPS with key)
917
+ requiresApiKey() {
682
918
  return false;
683
919
  }
684
- }
685
- function isOrbsUrl(url) {
686
- try {
687
- const parsed = new URL(url.trim());
688
- return parsed.hostname.includes("orbs.network") || parsed.hostname.includes("ton-access");
689
- } catch {
690
- return false;
920
+ };
921
+
922
+ // src/providers/orbs.ts
923
+ var OrbsProvider = class extends BaseProvider {
924
+ constructor(provider) {
925
+ super(provider);
691
926
  }
692
- }
693
- function buildRestUrl(baseEndpoint, method) {
694
- const base = toV2Base(baseEndpoint);
695
- return `${base}/${method}`;
696
- }
697
- function buildGetAddressStateUrl(baseEndpoint, address) {
698
- const base = toV2Base(baseEndpoint);
699
- return `${base}/getAddressState?address=${encodeURIComponent(address)}`;
700
- }
701
- function buildGetAddressBalanceUrl(baseEndpoint, address) {
702
- const base = toV2Base(baseEndpoint);
703
- return `${base}/getAddressBalance?address=${encodeURIComponent(address)}`;
704
- }
705
- function buildGetAddressInfoUrl(baseEndpoint, address) {
706
- const base = toV2Base(baseEndpoint);
707
- return `${base}/getAddressInformation?address=${encodeURIComponent(address)}`;
708
- }
709
- function detectNetworkFromEndpoint(endpoint) {
710
- const lower = endpoint.toLowerCase();
711
- if (lower.includes("testnet") || lower.includes("test") || lower.includes("sandbox")) {
712
- return "testnet";
927
+ normalizeEndpoint(endpoint) {
928
+ let normalized = endpoint.trim();
929
+ if (normalized.endsWith("/")) {
930
+ normalized = normalized.slice(0, -1);
931
+ }
932
+ if (normalized.endsWith("/api/v2")) {
933
+ return normalized;
934
+ }
935
+ return normalized;
936
+ }
937
+ buildHeaders() {
938
+ return {
939
+ "Content-Type": "application/json"
940
+ };
713
941
  }
714
- if (lower.includes("mainnet") || lower.includes("main") || // TonCenter mainnet doesn't have 'mainnet' in URL
715
- lower.includes("toncenter.com") && !lower.includes("testnet")) {
716
- return "mainnet";
942
+ /**
943
+ * Get dynamic endpoint from Orbs TON Access.
944
+ * This should be called before making requests.
945
+ */
946
+ async getDynamicEndpoint() {
947
+ if (!this.provider.isDynamic) {
948
+ return this.normalizeEndpoint(this.provider.endpointV2);
949
+ }
950
+ try {
951
+ const { getHttpEndpoint } = await import('@orbs-network/ton-access');
952
+ const endpoint = await getHttpEndpoint({ network: this.provider.network });
953
+ return this.normalizeEndpoint(endpoint);
954
+ } catch (error) {
955
+ return this.normalizeEndpoint(this.provider.endpointV2);
956
+ }
717
957
  }
718
- return null;
719
- }
720
- function isValidHttpUrl(str) {
721
- try {
722
- const url = new URL(str);
723
- return url.protocol === "http:" || url.protocol === "https:";
724
- } catch {
958
+ requiresApiKey() {
725
959
  return false;
726
960
  }
727
- }
728
- function isValidWsUrl(str) {
729
- try {
730
- const url = new URL(str);
731
- return url.protocol === "ws:" || url.protocol === "wss:";
732
- } catch {
733
- return false;
961
+ };
962
+
963
+ // src/providers/index.ts
964
+ var GenericProvider = class extends BaseProvider {
965
+ normalizeEndpoint(endpoint) {
966
+ let normalized = endpoint.trim();
967
+ if (normalized.endsWith("/")) {
968
+ normalized = normalized.slice(0, -1);
969
+ }
970
+ if (normalized.toLowerCase().endsWith("/jsonrpc")) {
971
+ return normalized;
972
+ }
973
+ try {
974
+ const url = new URL(normalized);
975
+ if (!url.pathname || url.pathname === "/") {
976
+ return normalized + "/jsonRPC";
977
+ }
978
+ } catch {
979
+ }
980
+ return normalized;
981
+ }
982
+ buildHeaders() {
983
+ const headers = {
984
+ "Content-Type": "application/json"
985
+ };
986
+ if (this.provider.apiKey) {
987
+ headers["x-api-key"] = this.provider.apiKey;
988
+ }
989
+ return headers;
990
+ }
991
+ };
992
+ function createProvider(provider) {
993
+ switch (provider.type) {
994
+ case "chainstack":
995
+ return new ChainstackProvider(provider);
996
+ case "tatum":
997
+ return new TatumProvider(provider);
998
+ case "onfinality":
999
+ return new OnFinalityProvider(provider);
1000
+ case "quicknode":
1001
+ return new QuickNodeProvider(provider);
1002
+ case "getblock":
1003
+ return new GetBlockProvider(provider);
1004
+ case "toncenter":
1005
+ return new TonCenterProvider(provider);
1006
+ case "orbs":
1007
+ return new OrbsProvider(provider);
1008
+ case "custom":
1009
+ return new GenericProvider(provider);
1010
+ default:
1011
+ return new GenericProvider(provider);
734
1012
  }
735
1013
  }
736
1014
 
@@ -790,21 +1068,25 @@ var HealthChecker = class {
790
1068
  if (!endpoint) {
791
1069
  throw new Error("No valid endpoint available");
792
1070
  }
793
- if (provider.type === "tatum" && !provider.apiKey) {
794
- throw new Error("Tatum provider requires API key (set TATUM_API_KEY_TESTNET or TATUM_API_KEY_MAINNET)");
1071
+ const providerImpl = createProvider(provider);
1072
+ const validation = providerImpl.validateConfig();
1073
+ if (!validation.valid) {
1074
+ throw new Error(validation.error || "Provider configuration invalid");
795
1075
  }
796
- let normalizedEndpoint = this.normalizeEndpointForProvider(provider, endpoint);
1076
+ const normalizedEndpoint = providerImpl.normalizeEndpoint(endpoint);
797
1077
  if (provider.type === "onfinality") {
798
1078
  this.logger.debug(`OnFinality endpoint: ${endpoint} -> ${normalizedEndpoint}, API key: ${provider.apiKey ? "set" : "not set"}`);
799
1079
  }
800
1080
  let info;
801
1081
  try {
802
- info = await this.callGetMasterchainInfo(normalizedEndpoint, provider);
1082
+ info = await this.callGetMasterchainInfo(normalizedEndpoint, provider, providerImpl);
803
1083
  } catch (error) {
804
1084
  if (provider.type === "onfinality" && normalizedEndpoint.includes("/rpc") && provider.apiKey && error.message?.includes("backend error")) {
805
1085
  this.logger.debug(`OnFinality /rpc failed, retrying with /public endpoint`);
806
1086
  const publicEndpoint = normalizedEndpoint.replace("/rpc", "/public");
807
- info = await this.callGetMasterchainInfo(publicEndpoint, { ...provider, apiKey: void 0 });
1087
+ const publicProvider = { ...provider, apiKey: void 0 };
1088
+ const publicProviderImpl = createProvider(publicProvider);
1089
+ info = await this.callGetMasterchainInfo(publicEndpoint, publicProvider, publicProviderImpl);
808
1090
  } else {
809
1091
  throw error;
810
1092
  }
@@ -950,14 +1232,17 @@ var HealthChecker = class {
950
1232
  }
951
1233
  /**
952
1234
  * Mark a provider as degraded (e.g., on 429 error)
1235
+ *
1236
+ * Providers marked as degraded have failed health checks (e.g., rate limit errors)
1237
+ * and should not be selected. The system will failover to the next available provider.
953
1238
  */
954
1239
  markDegraded(providerId, network, error) {
955
1240
  const key = this.getResultKey(providerId, network);
956
1241
  const existing = this.results.get(key);
957
1242
  const result = existing ? {
958
1243
  ...existing,
959
- success: true,
960
- // Degraded providers are still usable, just slower/rate-limited
1244
+ success: false,
1245
+ // Degraded providers with errors should not be selected
961
1246
  status: "degraded",
962
1247
  error: error || "Marked as degraded",
963
1248
  lastTested: /* @__PURE__ */ new Date(),
@@ -965,8 +1250,8 @@ var HealthChecker = class {
965
1250
  } : {
966
1251
  id: providerId,
967
1252
  network,
968
- success: true,
969
- // Degraded providers are still usable
1253
+ success: false,
1254
+ // Degraded providers with errors should not be selected
970
1255
  status: "degraded",
971
1256
  latencyMs: null,
972
1257
  seqno: null,
@@ -1029,44 +1314,19 @@ var HealthChecker = class {
1029
1314
  }
1030
1315
  return provider.endpointV2 || provider.endpointV3 || null;
1031
1316
  }
1032
- /**
1033
- * Normalize endpoint for provider-specific requirements
1034
- *
1035
- * Note: normalizeV2Endpoint now handles all provider-specific cases correctly,
1036
- * including Tatum (/jsonRPC), OnFinality (/public or /rpc), QuickNode, and GetBlock.
1037
- */
1038
- normalizeEndpointForProvider(provider, endpoint) {
1039
- if (provider.type === "tatum" && endpoint.includes("api.tatum.io/v3/blockchain/node")) {
1040
- const network = provider.network === "testnet" ? "testnet" : "mainnet";
1041
- endpoint = `https://ton-${network}.gateway.tatum.io`;
1042
- }
1043
- return normalizeV2Endpoint(endpoint);
1044
- }
1045
1317
  /**
1046
1318
  * Call getMasterchainInfo API with provider-specific handling
1047
1319
  */
1048
- async callGetMasterchainInfo(endpoint, provider) {
1320
+ async callGetMasterchainInfo(endpoint, provider, providerImpl) {
1049
1321
  const controller = new AbortController();
1050
1322
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
1051
- const headers = {
1052
- "Content-Type": "application/json"
1053
- };
1054
- if (provider.type === "tatum" && provider.apiKey) {
1055
- headers["x-api-key"] = provider.apiKey;
1056
- }
1057
- if (provider.type === "onfinality" && provider.apiKey) {
1058
- headers["apikey"] = provider.apiKey;
1059
- }
1323
+ const headers = providerImpl.buildHeaders();
1324
+ const requestBody = providerImpl.buildRequest("getMasterchainInfo", {});
1060
1325
  try {
1061
1326
  const response = await fetch(endpoint, {
1062
1327
  method: "POST",
1063
1328
  headers,
1064
- body: JSON.stringify({
1065
- id: "1",
1066
- jsonrpc: "2.0",
1067
- method: "getMasterchainInfo",
1068
- params: {}
1069
- }),
1329
+ body: JSON.stringify(requestBody),
1070
1330
  signal: controller.signal
1071
1331
  });
1072
1332
  clearTimeout(timeoutId);
@@ -1092,40 +1352,7 @@ var HealthChecker = class {
1092
1352
  }
1093
1353
  }
1094
1354
  data = await response.json();
1095
- let info;
1096
- if (data && typeof data === "object") {
1097
- const dataObj = data;
1098
- if ("ok" in dataObj) {
1099
- if (!dataObj.ok) {
1100
- const error = dataObj.error;
1101
- throw new Error(error || "API returned ok=false");
1102
- }
1103
- const result = dataObj.result;
1104
- info = result || dataObj;
1105
- } else if ("result" in dataObj) {
1106
- info = dataObj.result;
1107
- } else if ("last" in dataObj || "@type" in dataObj) {
1108
- info = dataObj;
1109
- } else if ("error" in dataObj) {
1110
- const errorObj = dataObj.error;
1111
- const errorMsg = typeof errorObj === "string" ? errorObj : errorObj?.message || errorObj?.code || String(errorObj);
1112
- throw new Error(`API error: ${errorMsg}`);
1113
- } else {
1114
- throw new Error(`Unknown response format from ${provider.type}`);
1115
- }
1116
- } else {
1117
- throw new Error(`Invalid response type: ${typeof data}`);
1118
- }
1119
- if (!info || typeof info !== "object") {
1120
- this.logger.debug(`Invalid response structure from ${provider.type}: ${JSON.stringify(data)}`);
1121
- throw new Error("Invalid response structure");
1122
- }
1123
- const infoObj = info;
1124
- const seqno = infoObj.last?.seqno;
1125
- if (seqno === void 0 || seqno === null || seqno <= 0 || !Number.isInteger(seqno)) {
1126
- this.logger.debug(`Invalid seqno from ${provider.type}:`, { seqno, info });
1127
- throw new Error(`Invalid seqno: ${seqno} (must be positive integer)`);
1128
- }
1355
+ const info = providerImpl.parseMasterchainInfo(data);
1129
1356
  return info;
1130
1357
  } catch (error) {
1131
1358
  clearTimeout(timeoutId);
@@ -1985,6 +2212,193 @@ function createSelector(registry, healthChecker, config, logger, adapter = "node
1985
2212
  return new ProviderSelector(registry, healthChecker, config, logger, adapter);
1986
2213
  }
1987
2214
 
2215
+ // src/utils/endpoint.ts
2216
+ function normalizeV2Endpoint(endpoint, provider) {
2217
+ if (provider) {
2218
+ const providerImpl = createProvider(provider);
2219
+ return providerImpl.normalizeEndpoint(endpoint);
2220
+ }
2221
+ return normalizeV2EndpointFallback(endpoint);
2222
+ }
2223
+ function normalizeV2EndpointFallback(endpoint) {
2224
+ let normalized = endpoint.trim();
2225
+ if (normalized.endsWith("/")) {
2226
+ normalized = normalized.slice(0, -1);
2227
+ }
2228
+ if (normalized.toLowerCase().endsWith("/jsonrpc")) {
2229
+ return normalized;
2230
+ }
2231
+ if (normalized.includes("gateway.tatum.io")) {
2232
+ try {
2233
+ const url = new URL(normalized);
2234
+ if (!url.pathname || url.pathname === "/") {
2235
+ return normalized + "/jsonRPC";
2236
+ }
2237
+ if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
2238
+ return normalized + "/jsonRPC";
2239
+ }
2240
+ } catch {
2241
+ return normalized + "/jsonRPC";
2242
+ }
2243
+ }
2244
+ if (normalized.includes("onfinality.io")) {
2245
+ try {
2246
+ const url = new URL(normalized);
2247
+ const baseUrl = normalized.split("?")[0];
2248
+ if (!url.pathname || url.pathname === "/") {
2249
+ const apikey = url.searchParams.get("apikey");
2250
+ if (apikey && apikey !== "{key}" && apikey.length > 0) {
2251
+ return baseUrl.replace(/\/?$/, "/rpc");
2252
+ }
2253
+ return baseUrl.replace(/\/?$/, "/public");
2254
+ }
2255
+ if (url.pathname === "/rpc" || url.pathname === "/public") {
2256
+ return baseUrl;
2257
+ }
2258
+ return baseUrl;
2259
+ } catch {
2260
+ if (normalized.includes("{key}")) {
2261
+ return normalized.split("?")[0].replace(/\/?$/, "/public");
2262
+ }
2263
+ if (!normalized.includes("/rpc") && !normalized.includes("/public")) {
2264
+ return normalized.split("?")[0] + "/public";
2265
+ }
2266
+ return normalized.split("?")[0];
2267
+ }
2268
+ }
2269
+ if (normalized.endsWith("/api/v2")) {
2270
+ return normalized + "/jsonRPC";
2271
+ }
2272
+ if (normalized.endsWith("/api/v3")) {
2273
+ return normalized.replace("/api/v3", "/api/v2/jsonRPC");
2274
+ }
2275
+ if (normalized.includes("quiknode.pro") || normalized.includes("getblock.io")) {
2276
+ try {
2277
+ const url = new URL(normalized);
2278
+ if (!url.pathname || url.pathname === "/") {
2279
+ return normalized + "/jsonRPC";
2280
+ }
2281
+ if (!url.pathname.toLowerCase().endsWith("/jsonrpc")) {
2282
+ return normalized + "/jsonRPC";
2283
+ }
2284
+ } catch {
2285
+ return normalized + "/jsonRPC";
2286
+ }
2287
+ }
2288
+ try {
2289
+ const url = new URL(normalized);
2290
+ if (!url.pathname || url.pathname === "/") {
2291
+ return normalized + "/jsonRPC";
2292
+ }
2293
+ } catch {
2294
+ }
2295
+ return normalized;
2296
+ }
2297
+ function toV2Base(endpoint) {
2298
+ let normalized = endpoint.trim();
2299
+ if (normalized.endsWith("/")) {
2300
+ normalized = normalized.slice(0, -1);
2301
+ }
2302
+ if (normalized.toLowerCase().endsWith("/jsonrpc")) {
2303
+ normalized = normalized.slice(0, -8);
2304
+ }
2305
+ normalized = normalized.replace(/\/api\/v3\b/, "/api/v2");
2306
+ if (!normalized.endsWith("/api/v2")) {
2307
+ if (normalized.includes("/api/v2")) {
2308
+ const idx = normalized.indexOf("/api/v2");
2309
+ normalized = normalized.slice(0, idx + 7);
2310
+ }
2311
+ }
2312
+ return normalized;
2313
+ }
2314
+ function toV3Base(endpoint) {
2315
+ const normalized = toV2Base(endpoint);
2316
+ return normalized.replace("/api/v2", "/api/v3");
2317
+ }
2318
+ function getBaseUrl(endpoint) {
2319
+ try {
2320
+ const url = new URL(endpoint);
2321
+ return `${url.protocol}//${url.host}`;
2322
+ } catch {
2323
+ return endpoint;
2324
+ }
2325
+ }
2326
+ function isChainstackUrl(url) {
2327
+ try {
2328
+ const parsed = new URL(url.trim());
2329
+ return parsed.hostname.includes("chainstack.com");
2330
+ } catch {
2331
+ return false;
2332
+ }
2333
+ }
2334
+ function isQuickNodeUrl(url) {
2335
+ try {
2336
+ const parsed = new URL(url.trim());
2337
+ return parsed.hostname.includes("quiknode.pro");
2338
+ } catch {
2339
+ return false;
2340
+ }
2341
+ }
2342
+ function isTonCenterUrl(url) {
2343
+ try {
2344
+ const parsed = new URL(url.trim());
2345
+ return parsed.hostname.includes("toncenter.com");
2346
+ } catch {
2347
+ return false;
2348
+ }
2349
+ }
2350
+ function isOrbsUrl(url) {
2351
+ try {
2352
+ const parsed = new URL(url.trim());
2353
+ return parsed.hostname.includes("orbs.network") || parsed.hostname.includes("ton-access");
2354
+ } catch {
2355
+ return false;
2356
+ }
2357
+ }
2358
+ function buildRestUrl(baseEndpoint, method) {
2359
+ const base = toV2Base(baseEndpoint);
2360
+ return `${base}/${method}`;
2361
+ }
2362
+ function buildGetAddressStateUrl(baseEndpoint, address) {
2363
+ const base = toV2Base(baseEndpoint);
2364
+ return `${base}/getAddressState?address=${encodeURIComponent(address)}`;
2365
+ }
2366
+ function buildGetAddressBalanceUrl(baseEndpoint, address) {
2367
+ const base = toV2Base(baseEndpoint);
2368
+ return `${base}/getAddressBalance?address=${encodeURIComponent(address)}`;
2369
+ }
2370
+ function buildGetAddressInfoUrl(baseEndpoint, address) {
2371
+ const base = toV2Base(baseEndpoint);
2372
+ return `${base}/getAddressInformation?address=${encodeURIComponent(address)}`;
2373
+ }
2374
+ function detectNetworkFromEndpoint(endpoint) {
2375
+ const lower = endpoint.toLowerCase();
2376
+ if (lower.includes("testnet") || lower.includes("test") || lower.includes("sandbox")) {
2377
+ return "testnet";
2378
+ }
2379
+ if (lower.includes("mainnet") || lower.includes("main") || // TonCenter mainnet doesn't have 'mainnet' in URL
2380
+ lower.includes("toncenter.com") && !lower.includes("testnet")) {
2381
+ return "mainnet";
2382
+ }
2383
+ return null;
2384
+ }
2385
+ function isValidHttpUrl(str) {
2386
+ try {
2387
+ const url = new URL(str);
2388
+ return url.protocol === "http:" || url.protocol === "https:";
2389
+ } catch {
2390
+ return false;
2391
+ }
2392
+ }
2393
+ function isValidWsUrl(str) {
2394
+ try {
2395
+ const url = new URL(str);
2396
+ return url.protocol === "ws:" || url.protocol === "wss:";
2397
+ } catch {
2398
+ return false;
2399
+ }
2400
+ }
2401
+
1988
2402
  // src/core/manager.ts
1989
2403
  var consoleLogger5 = {
1990
2404
  debug: (msg, data) => console.debug(`[ProviderManager] ${msg}`, data || ""),
@@ -2183,17 +2597,20 @@ var _ProviderManager = class _ProviderManager {
2183
2597
  this.options.logger.warn("No providers available, using fallback");
2184
2598
  return this.getFallbackEndpoint();
2185
2599
  }
2186
- this.selector.getActiveProviderId(this.network) || this.selector.getBestProvider(this.network);
2600
+ const activeProviderId = this.selector.getActiveProviderId(this.network);
2601
+ if (!activeProviderId) {
2602
+ this.selector.getBestProvider(this.network);
2603
+ }
2187
2604
  if (provider.isDynamic && provider.type === "orbs") {
2188
2605
  try {
2189
2606
  const { getHttpEndpoint } = await import('@orbs-network/ton-access');
2190
2607
  const endpoint = await getHttpEndpoint({ network: this.network });
2191
- return normalizeV2Endpoint(endpoint);
2608
+ return normalizeV2Endpoint(endpoint, provider);
2192
2609
  } catch (error) {
2193
2610
  this.options.logger.warn(`Failed to get Orbs endpoint: ${error.message}`);
2194
2611
  }
2195
2612
  }
2196
- return normalizeV2Endpoint(provider.endpointV2);
2613
+ return normalizeV2Endpoint(provider.endpointV2, provider);
2197
2614
  }
2198
2615
  /**
2199
2616
  * Get endpoint with rate limiting
@@ -2216,11 +2633,11 @@ var _ProviderManager = class _ProviderManager {
2216
2633
  this.options.logger.warn(`Rate limit timeout for ${provider.id}`);
2217
2634
  const next = this.selector.getNextProvider(this.network, [provider.id]);
2218
2635
  if (next) {
2219
- return normalizeV2Endpoint(next.endpointV2);
2636
+ return normalizeV2Endpoint(next.endpointV2, next);
2220
2637
  }
2221
2638
  return this.getFallbackEndpoint();
2222
2639
  }
2223
- return normalizeV2Endpoint(provider.endpointV2);
2640
+ return normalizeV2Endpoint(provider.endpointV2, provider);
2224
2641
  }
2225
2642
  /**
2226
2643
  * Get current active provider
@@ -3079,6 +3496,6 @@ async function createBrowserAdapterForNetwork(network, configPath, logger) {
3079
3496
  return new BrowserAdapter(manager, logger);
3080
3497
  }
3081
3498
 
3082
- export { ApiVersionSchema, BrowserAdapter, CHAINSTACK_RATE_LIMIT, ConfigError, DEFAULT_CONTRACT_TIMEOUT_MS, DEFAULT_HEALTH_CHECK_TIMEOUT_MS, DEFAULT_PROVIDERS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_RATE_LIMIT, HealthChecker, NetworkSchema, NodeAdapter, ORBS_RATE_LIMIT, ProviderConfigSchema, ProviderError, ProviderManager, ProviderRegistry, ProviderSelector, ProviderTypeSchema, QUICKNODE_RATE_LIMIT, RateLimitError, RateLimiterManager, RpcConfigSchema, TimeoutError, TokenBucketRateLimiter, buildGetAddressBalanceUrl, buildGetAddressInfoUrl, buildGetAddressStateUrl, buildRestUrl, createBrowserAdapter, createBrowserAdapterForNetwork, createDefaultConfig, createDefaultRegistry, createEmptyConfig, createHealthChecker, createNodeAdapter, createProviderManager, createRateLimiter, createRateLimiterManager, createRegistry, createRegistryFromData, createRegistryFromFile, createSelector, createTimeoutController, detectNetworkFromEndpoint, fetchWithTimeout, getBaseUrl, getDefaultProvidersForNetwork, getEnvVar, getProviderManager, getProvidersForNetwork, getRateLimitForType, getTonClient, getTonClientForNetwork, getTonClientWithRateLimit, isApiVersion, isChainstackUrl, isNetwork, isOrbsUrl, isProviderType, isQuickNodeUrl, isRateLimitError, isTimeoutError, isTonCenterUrl, isValidHttpUrl, isValidWsUrl, loadBuiltinConfig, loadConfig, loadConfigFromData, loadConfigFromUrl, mergeConfigs, mergeWithDefaults, normalizeV2Endpoint, parseProviderConfig, parseRpcConfig, resetNodeAdapter, resolveAllProviders, resolveEndpoints, resolveKeyPlaceholder, resolveProvider, sleep, toV2Base, toV3Base, withRetry, withTimeout, withTimeoutAndRetry, withTimeoutFn };
3499
+ export { ApiVersionSchema, BaseProvider, BrowserAdapter, CHAINSTACK_RATE_LIMIT, ChainstackProvider, ConfigError, DEFAULT_CONTRACT_TIMEOUT_MS, DEFAULT_HEALTH_CHECK_TIMEOUT_MS, DEFAULT_PROVIDERS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_RATE_LIMIT, GenericProvider, GetBlockProvider, HealthChecker, NetworkSchema, NodeAdapter, ORBS_RATE_LIMIT, OnFinalityProvider, OrbsProvider, ProviderConfigSchema, ProviderError, ProviderManager, ProviderRegistry, ProviderSelector, ProviderTypeSchema, QUICKNODE_RATE_LIMIT, QuickNodeProvider, RateLimitError, RateLimiterManager, RpcConfigSchema, TatumProvider, TimeoutError, TokenBucketRateLimiter, TonCenterProvider, buildGetAddressBalanceUrl, buildGetAddressInfoUrl, buildGetAddressStateUrl, buildRestUrl, createBrowserAdapter, createBrowserAdapterForNetwork, createDefaultConfig, createDefaultRegistry, createEmptyConfig, createHealthChecker, createNodeAdapter, createProvider, createProviderManager, createRateLimiter, createRateLimiterManager, createRegistry, createRegistryFromData, createRegistryFromFile, createSelector, createTimeoutController, detectNetworkFromEndpoint, fetchWithTimeout, getBaseUrl, getDefaultProvidersForNetwork, getEnvVar, getProviderManager, getProvidersForNetwork, getRateLimitForType, getTonClient, getTonClientForNetwork, getTonClientWithRateLimit, isApiVersion, isChainstackUrl, isNetwork, isOrbsUrl, isProviderType, isQuickNodeUrl, isRateLimitError, isTimeoutError, isTonCenterUrl, isValidHttpUrl, isValidWsUrl, loadBuiltinConfig, loadConfig, loadConfigFromData, loadConfigFromUrl, mergeConfigs, mergeWithDefaults, normalizeV2Endpoint, parseProviderConfig, parseRpcConfig, resetNodeAdapter, resolveAllProviders, resolveEndpoints, resolveKeyPlaceholder, resolveProvider, sleep, toV2Base, toV3Base, withRetry, withTimeout, withTimeoutAndRetry, withTimeoutFn };
3083
3500
  //# sourceMappingURL=index.js.map
3084
3501
  //# sourceMappingURL=index.js.map