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