@pioneer-platform/markets 8.11.13 → 8.11.24

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.
@@ -1,2 +1 @@
1
-
2
- $ tsc -p .
1
+ $ tsc -p .
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @pioneer-platform/markets
2
2
 
3
+ ## 8.11.24
4
+
5
+ ### Patch Changes
6
+
7
+ - cache work
8
+ - Updated dependencies
9
+ - @pioneer-platform/pro-token@0.8.2
10
+ - @pioneer-platform/pioneer-discovery@8.11.24
11
+
12
+ ## 8.11.23
13
+
14
+ ### Patch Changes
15
+
16
+ - cache work
17
+ - Updated dependencies
18
+ - @pioneer-platform/pro-token@0.8.1
19
+ - @pioneer-platform/pioneer-discovery@8.11.23
20
+
3
21
  ## 8.11.13
4
22
 
5
23
  ### Patch Changes
package/lib/index.js CHANGED
@@ -57,6 +57,7 @@ var TAG = " | market-module | ";
57
57
  // @ts-ignore
58
58
  var pioneer_discovery_1 = require("@pioneer-platform/pioneer-discovery");
59
59
  var caip_1 = require("@shapeshiftoss/caip");
60
+ var Bottleneck = require('bottleneck');
60
61
  var axiosLib = require('axios');
61
62
  var Axios = axiosLib.default || axiosLib;
62
63
  var https = require('https');
@@ -68,15 +69,22 @@ var axios = Axios.create({
68
69
  });
69
70
  var axiosRetry = require('axios-retry');
70
71
  axiosRetry(axios, {
71
- retries: 5, // number of retries
72
+ retries: 3, // Reduced from 5 to 3
72
73
  retryDelay: function (retryCount) {
73
74
  console.log("retry attempt: ".concat(retryCount));
74
- return retryCount * 1000; // time interval between retries
75
+ return retryCount * 2000; // Increased from 1s to 2s backoff
75
76
  },
76
77
  retryCondition: function (error) {
77
- console.error(error);
78
- // if retry condition is not specified, by default idempotent requests are retried
79
- return error.response.status === 503;
78
+ var _a;
79
+ var status = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status;
80
+ // CRITICAL FIX: Never retry rate limits (429) - respect the API!
81
+ // Also never retry 4xx client errors (except 503 service unavailable)
82
+ if (status === 429 || status === 403) {
83
+ console.warn("Rate limit or forbidden (".concat(status, "), not retrying"));
84
+ return false;
85
+ }
86
+ // Only retry on actual service unavailability
87
+ return status === 503 || status === 502 || status === 504;
80
88
  },
81
89
  });
82
90
  var ProToken = require("@pioneer-platform/pro-token");
@@ -102,6 +110,20 @@ if (CMC_PRO_API_KEY) {
102
110
  var GLOBAL_RATES_COINCAP;
103
111
  var GLOBAL_RATES_COINGECKO;
104
112
  var GLOBAL_RATES_CMC;
113
+ // FIX: Request coalescing to prevent thundering herd
114
+ // Tracks in-flight API requests to prevent duplicate calls for same asset
115
+ var pendingPriceRequests = new Map();
116
+ var pendingPriceRequestsWithSource = new Map();
117
+ // Cache of unpriceable tokens (scam/spam tokens not in any price feed)
118
+ // Using in-memory Map for instant lookups (no Redis timeout issues)
119
+ // Also persisted to Redis for cross-process sharing and persistence
120
+ var unpriceableTokensCache = new Map();
121
+ var UNPRICEABLE_TOKEN_PREFIX = 'unpriceable_token:';
122
+ var UNPRICEABLE_TOKEN_TTL = 7 * 24 * 60 * 60; // 7 days
123
+ var UNPRICEABLE_TOKEN_TTL_MS = UNPRICEABLE_TOKEN_TTL * 1000;
124
+ // NOTE: Rate limiting is now enforced at the worker level (pioneer-server)
125
+ // using a Redis mutex to ensure ONLY ONE market API call at a time across all processes
126
+ // The Bottleneck limiters below are kept as a safety net for direct module usage
105
127
  module.exports = {
106
128
  init: function (settings) {
107
129
  if (settings === null || settings === void 0 ? void 0 : settings.apiKey) {
@@ -113,6 +135,10 @@ module.exports = {
113
135
  getAssetPriceByCaip: function (caip, returnSource) {
114
136
  return get_asset_price_by_caip(caip, returnSource);
115
137
  },
138
+ // NEW: Batch price lookup (significantly reduces API calls)
139
+ getBatchPricesByCaip: function (caips, returnSource) {
140
+ return get_batch_prices_by_caip(caips, returnSource);
141
+ },
116
142
  // Legacy bulk fetch functions
117
143
  getAssetsCoinCap: function () {
118
144
  return get_assets_coincap();
@@ -682,12 +708,74 @@ var caip_to_identifiers = function (caip) {
682
708
  coincapSymbol: symbol.toLowerCase()
683
709
  };
684
710
  };
711
+ /**
712
+ * Check if token is marked as unpriceable (scam/spam token)
713
+ * Uses in-memory cache first for instant lookups (no Redis timeout issues)
714
+ * Returns true if cached as unpriceable, false otherwise
715
+ */
716
+ var is_unpriceable_token = function (caip) {
717
+ var tag = TAG + ' | is_unpriceable_token | ';
718
+ // Check in-memory cache first (instant, no timeout issues)
719
+ var cached = unpriceableTokensCache.get(caip);
720
+ if (cached) {
721
+ // Check if still valid (7 days)
722
+ var age = Date.now() - cached.markedAt;
723
+ if (age < UNPRICEABLE_TOKEN_TTL_MS) {
724
+ log.debug(tag, "\uD83D\uDEA8 Token is cached as unpriceable (maybeScam), skipping: ".concat(caip));
725
+ return true;
726
+ }
727
+ else {
728
+ // Expired, remove from cache
729
+ unpriceableTokensCache.delete(caip);
730
+ return false;
731
+ }
732
+ }
733
+ return false;
734
+ };
735
+ /**
736
+ * Cache token as unpriceable (not found in any of our 3 paid APIs)
737
+ * Stores in-memory for instant lookups + Redis for persistence
738
+ * This prevents wasting API calls on scam/spam tokens
739
+ */
740
+ var cache_unpriceable_token = function (caip) {
741
+ return __awaiter(this, void 0, void 0, function () {
742
+ var tag, key;
743
+ return __generator(this, function (_a) {
744
+ tag = TAG + ' | cache_unpriceable_token | ';
745
+ // Store in-memory for instant lookups
746
+ unpriceableTokensCache.set(caip, {
747
+ markedAt: Date.now(),
748
+ maybeScam: true
749
+ });
750
+ // Also persist to Redis for cross-process sharing (but don't wait/block)
751
+ try {
752
+ key = UNPRICEABLE_TOKEN_PREFIX + caip;
753
+ redis.setex(key, UNPRICEABLE_TOKEN_TTL, JSON.stringify({
754
+ caip: caip,
755
+ markedAt: new Date().toISOString(),
756
+ reason: 'not_found_in_paid_apis',
757
+ maybeScam: true
758
+ })).catch(function (err) {
759
+ // Ignore Redis errors - in-memory cache is primary
760
+ log.debug(tag, "Redis error (non-blocking): ".concat(err.message));
761
+ });
762
+ log.info(tag, "\u2705 Cached token as unpriceable for 7 days: ".concat(caip));
763
+ }
764
+ catch (error) {
765
+ // If Redis fails, don't block - in-memory cache is already set
766
+ log.debug(tag, "Redis error caching unpriceable token (non-critical): ".concat(error));
767
+ }
768
+ return [2 /*return*/];
769
+ });
770
+ });
771
+ };
685
772
  /**
686
773
  * Get price from CoinGecko by coin ID
774
+ * Now with rate limiting to prevent API spam
687
775
  */
688
776
  var get_price_from_coingecko = function (coingeckoId) {
689
777
  return __awaiter(this, void 0, void 0, function () {
690
- var tag, url, response, price, error_1, status_3;
778
+ var tag, url, headers, response, price, error_1, status_3;
691
779
  var _a;
692
780
  return __generator(this, function (_b) {
693
781
  switch (_b.label) {
@@ -698,7 +786,11 @@ var get_price_from_coingecko = function (coingeckoId) {
698
786
  _b.trys.push([1, 3, , 4]);
699
787
  url = "".concat(URL_COINGECKO, "simple/price?ids=").concat(coingeckoId, "&vs_currencies=usd");
700
788
  log.debug(tag, "Fetching from CoinGecko: ".concat(url));
701
- return [4 /*yield*/, axios.get(url)];
789
+ headers = {};
790
+ if (COINGECKO_API_KEY) {
791
+ headers['x-cg-pro-api-key'] = COINGECKO_API_KEY;
792
+ }
793
+ return [4 /*yield*/, axios.get(url, { headers: headers })];
702
794
  case 2:
703
795
  response = _b.sent();
704
796
  if (response.data && response.data[coingeckoId] && response.data[coingeckoId].usd) {
@@ -723,12 +815,62 @@ var get_price_from_coingecko = function (coingeckoId) {
723
815
  });
724
816
  });
725
817
  };
818
+ /**
819
+ * Get price from CoinGecko by ERC20 contract address
820
+ * Fallback for tokens without CoinGecko ID mapping
821
+ * Now with rate limiting to prevent API spam
822
+ */
823
+ var get_price_from_coingecko_contract = function (contractAddress_1) {
824
+ return __awaiter(this, arguments, void 0, function (contractAddress, chainId) {
825
+ var tag, url, headers, response, contractKey, price, error_2, status_4;
826
+ var _a;
827
+ if (chainId === void 0) { chainId = 'ethereum'; }
828
+ return __generator(this, function (_b) {
829
+ switch (_b.label) {
830
+ case 0:
831
+ tag = TAG + ' | get_price_from_coingecko_contract | ';
832
+ _b.label = 1;
833
+ case 1:
834
+ _b.trys.push([1, 3, , 4]);
835
+ url = "".concat(URL_COINGECKO, "simple/token_price/").concat(chainId, "?contract_addresses=").concat(contractAddress, "&vs_currencies=usd");
836
+ log.debug(tag, "Fetching from CoinGecko contract endpoint: ".concat(url));
837
+ headers = {};
838
+ if (COINGECKO_API_KEY) {
839
+ headers['x-cg-pro-api-key'] = COINGECKO_API_KEY;
840
+ }
841
+ return [4 /*yield*/, axios.get(url, { headers: headers })];
842
+ case 2:
843
+ response = _b.sent();
844
+ contractKey = contractAddress.toLowerCase();
845
+ if (response.data && response.data[contractKey] && response.data[contractKey].usd) {
846
+ price = parseFloat(response.data[contractKey].usd);
847
+ log.debug(tag, "\u2705 CoinGecko contract price for ".concat(contractAddress, ": $").concat(price));
848
+ return [2 /*return*/, price];
849
+ }
850
+ log.debug(tag, "No price data from CoinGecko for contract ".concat(contractAddress));
851
+ return [2 /*return*/, 0];
852
+ case 3:
853
+ error_2 = _b.sent();
854
+ status_4 = ((_a = error_2.response) === null || _a === void 0 ? void 0 : _a.status) || error_2.status;
855
+ if (status_4 === 429 || status_4 === 403) {
856
+ log.warn(tag, "CoinGecko rate limit (".concat(status_4, ") for contract ").concat(contractAddress));
857
+ }
858
+ else {
859
+ log.debug(tag, "CoinGecko contract error for ".concat(contractAddress, ": ").concat(error_2.message));
860
+ }
861
+ return [2 /*return*/, 0];
862
+ case 4: return [2 /*return*/];
863
+ }
864
+ });
865
+ });
866
+ };
726
867
  /**
727
868
  * Get price from CoinMarketCap by symbol
869
+ * Now with rate limiting to prevent API spam
728
870
  */
729
871
  var get_price_from_coinmarketcap = function (symbol) {
730
872
  return __awaiter(this, void 0, void 0, function () {
731
- var tag, url, response, coinData, priceData, price, error_2, status_4;
873
+ var tag, url, response, coinData, priceData, price, error_3, status_5;
732
874
  var _a, _b, _c;
733
875
  return __generator(this, function (_d) {
734
876
  switch (_d.label) {
@@ -766,13 +908,13 @@ var get_price_from_coinmarketcap = function (symbol) {
766
908
  log.debug(tag, "No price data from CoinMarketCap for ".concat(symbol));
767
909
  return [2 /*return*/, 0];
768
910
  case 3:
769
- error_2 = _d.sent();
770
- status_4 = ((_c = error_2.response) === null || _c === void 0 ? void 0 : _c.status) || error_2.status;
771
- if (status_4 === 429 || status_4 === 403) {
772
- log.warn(tag, "CoinMarketCap rate limit (".concat(status_4, ") for ").concat(symbol));
911
+ error_3 = _d.sent();
912
+ status_5 = ((_c = error_3.response) === null || _c === void 0 ? void 0 : _c.status) || error_3.status;
913
+ if (status_5 === 429 || status_5 === 403) {
914
+ log.warn(tag, "CoinMarketCap rate limit (".concat(status_5, ") for ").concat(symbol));
773
915
  }
774
916
  else {
775
- log.debug(tag, "CoinMarketCap error for ".concat(symbol, ": ").concat(error_2.message));
917
+ log.debug(tag, "CoinMarketCap error for ".concat(symbol, ": ").concat(error_3.message));
776
918
  }
777
919
  return [2 /*return*/, 0];
778
920
  case 4: return [2 /*return*/];
@@ -782,10 +924,11 @@ var get_price_from_coinmarketcap = function (symbol) {
782
924
  };
783
925
  /**
784
926
  * Get price from CoinCap by symbol
927
+ * Now with rate limiting to prevent API spam
785
928
  */
786
929
  var get_price_from_coincap = function (symbol) {
787
930
  return __awaiter(this, void 0, void 0, function () {
788
- var tag, url, response, price, error_3, status_5;
931
+ var tag, url, response, price, error_4, status_6;
789
932
  var _a;
790
933
  return __generator(this, function (_b) {
791
934
  switch (_b.label) {
@@ -816,13 +959,13 @@ var get_price_from_coincap = function (symbol) {
816
959
  log.debug(tag, "No price data from CoinCap for ".concat(symbol));
817
960
  return [2 /*return*/, 0];
818
961
  case 3:
819
- error_3 = _b.sent();
820
- status_5 = ((_a = error_3.response) === null || _a === void 0 ? void 0 : _a.status) || error_3.status;
821
- if (status_5 === 429 || status_5 === 403) {
822
- log.warn(tag, "CoinCap rate limit (".concat(status_5, ") for ").concat(symbol));
962
+ error_4 = _b.sent();
963
+ status_6 = ((_a = error_4.response) === null || _a === void 0 ? void 0 : _a.status) || error_4.status;
964
+ if (status_6 === 429 || status_6 === 403) {
965
+ log.warn(tag, "CoinCap rate limit (".concat(status_6, ") for ").concat(symbol));
823
966
  }
824
967
  else {
825
- log.debug(tag, "CoinCap error for ".concat(symbol, ": ").concat(error_3.message));
968
+ log.debug(tag, "CoinCap error for ".concat(symbol, ": ").concat(error_4.message));
826
969
  }
827
970
  return [2 /*return*/, 0];
828
971
  case 4: return [2 /*return*/];
@@ -837,7 +980,7 @@ var get_price_from_coincap = function (symbol) {
837
980
  */
838
981
  var get_price_from_mayascan = function () {
839
982
  return __awaiter(this, arguments, void 0, function (asset) {
840
- var tag, url, response, price, error_4;
983
+ var tag, url, response, price, error_5;
841
984
  if (asset === void 0) { asset = 'maya'; }
842
985
  return __generator(this, function (_a) {
843
986
  switch (_a.label) {
@@ -867,8 +1010,8 @@ var get_price_from_mayascan = function () {
867
1010
  log.debug(tag, "No ".concat(asset.toUpperCase(), " price data from MayaScan"));
868
1011
  return [2 /*return*/, 0];
869
1012
  case 3:
870
- error_4 = _a.sent();
871
- log.debug(tag, "MayaScan error: ".concat(error_4.message));
1013
+ error_5 = _a.sent();
1014
+ log.debug(tag, "MayaScan error: ".concat(error_5.message));
872
1015
  return [2 /*return*/, 0];
873
1016
  case 4: return [2 /*return*/];
874
1017
  }
@@ -882,13 +1025,45 @@ var get_price_from_mayascan = function () {
882
1025
  */
883
1026
  var get_asset_price_by_caip = function (caip_2) {
884
1027
  return __awaiter(this, arguments, void 0, function (caip, returnSource) {
885
- var tag, mayaPrice, isCacao, identifiers, price, cacaoPrice;
1028
+ var tag, pendingMap, existingRequest, pricePromise;
1029
+ if (returnSource === void 0) { returnSource = false; }
1030
+ return __generator(this, function (_a) {
1031
+ tag = TAG + ' | get_asset_price_by_caip | ';
1032
+ log.debug(tag, "Looking up price for: ".concat(caip));
1033
+ pendingMap = returnSource ? pendingPriceRequestsWithSource : pendingPriceRequests;
1034
+ existingRequest = pendingMap.get(caip);
1035
+ if (existingRequest) {
1036
+ log.debug(tag, "Coalescing duplicate request for: ".concat(caip));
1037
+ return [2 /*return*/, existingRequest];
1038
+ }
1039
+ pricePromise = get_asset_price_by_caip_internal(caip, returnSource);
1040
+ // Store promise for request coalescing
1041
+ pendingMap.set(caip, pricePromise);
1042
+ // Cleanup after completion (success or failure)
1043
+ pricePromise.finally(function () {
1044
+ pendingMap.delete(caip);
1045
+ });
1046
+ return [2 /*return*/, pricePromise];
1047
+ });
1048
+ });
1049
+ };
1050
+ /**
1051
+ * Internal implementation (separated for request coalescing)
1052
+ */
1053
+ var get_asset_price_by_caip_internal = function (caip_2) {
1054
+ return __awaiter(this, arguments, void 0, function (caip, returnSource) {
1055
+ var tag, mayaPrice, isCacao, identifiers, price, contractAddress, chainIdMatch, chainId, chainMap, platformName, asset, symbol, cacaoPrice;
886
1056
  if (returnSource === void 0) { returnSource = false; }
887
1057
  return __generator(this, function (_a) {
888
1058
  switch (_a.label) {
889
1059
  case 0:
890
- tag = TAG + ' | get_asset_price_by_caip | ';
891
- log.debug(tag, "Looking up price for: ".concat(caip));
1060
+ tag = TAG + ' | get_asset_price_by_caip_internal | ';
1061
+ log.debug(tag, "Fetching price for: ".concat(caip));
1062
+ // EARLY EXIT: Check if token is cached as unpriceable (scam/spam token)
1063
+ // This is instant (in-memory check, no Redis timeout issues)
1064
+ if (is_unpriceable_token(caip)) {
1065
+ return [2 /*return*/, returnSource ? { price: 0, source: 'unpriceable', maybeScam: true } : 0];
1066
+ }
892
1067
  if (!(caip === 'cosmos:mayachain-mainnet-v1/denom:maya' || caip.toLowerCase().includes('denom:maya'))) return [3 /*break*/, 2];
893
1068
  log.debug(tag, 'MAYA token detected, using MayaScan API');
894
1069
  return [4 /*yield*/, get_price_from_mayascan('maya')];
@@ -902,13 +1077,11 @@ var get_asset_price_by_caip = function (caip_2) {
902
1077
  case 2:
903
1078
  isCacao = caip === 'cosmos:mayachain-mainnet-v1/slip44:931';
904
1079
  identifiers = caip_to_identifiers(caip);
905
- if (!identifiers) {
906
- log.warn(tag, "No identifier mapping found for CAIP: ".concat(caip));
907
- return [2 /*return*/, returnSource ? { price: 0, source: 'none' } : 0];
908
- }
1080
+ price = 0;
1081
+ if (!identifiers) return [3 /*break*/, 4];
909
1082
  log.debug(tag, "Identifiers for ".concat(caip, ":"), identifiers);
910
- // Try CoinGecko first (free, no API key needed, most comprehensive)
911
- console.log("\uD83E\uDD8E Trying CoinGecko for: ".concat(identifiers.coingeckoId));
1083
+ // Try CoinGecko with mapped ID
1084
+ console.log("\uD83E\uDD8E Trying CoinGecko with mapped ID: ".concat(identifiers.coingeckoId));
912
1085
  return [4 /*yield*/, get_price_from_coingecko(identifiers.coingeckoId)];
913
1086
  case 3:
914
1087
  price = _a.sent();
@@ -916,34 +1089,118 @@ var get_asset_price_by_caip = function (caip_2) {
916
1089
  if (price > 0) {
917
1090
  return [2 /*return*/, returnSource ? { price: price, source: 'coingecko' } : price];
918
1091
  }
919
- // Try CoinMarketCap second (requires API key, very comprehensive)
920
- console.log("\uD83D\uDCCA CoinGecko failed (price=$0), trying CoinMarketCap for: ".concat(identifiers.cmcSymbol));
921
- return [4 /*yield*/, get_price_from_coinmarketcap(identifiers.cmcSymbol)];
1092
+ return [3 /*break*/, 5];
922
1093
  case 4:
1094
+ log.debug(tag, "No identifier mapping found for CAIP: ".concat(caip));
1095
+ _a.label = 5;
1096
+ case 5:
1097
+ if (!caip.includes('/erc20:0x')) return [3 /*break*/, 13];
1098
+ contractAddress = caip.split('/erc20:')[1];
1099
+ chainIdMatch = caip.match(/eip155:(\d+)/);
1100
+ if (!(contractAddress && chainIdMatch)) return [3 /*break*/, 7];
1101
+ chainId = chainIdMatch[1];
1102
+ chainMap = {
1103
+ '1': 'ethereum',
1104
+ '137': 'polygon-pos',
1105
+ '56': 'binance-smart-chain',
1106
+ '43114': 'avalanche',
1107
+ '250': 'fantom',
1108
+ '42161': 'arbitrum-one',
1109
+ '10': 'optimistic-ethereum',
1110
+ '8453': 'base'
1111
+ };
1112
+ platformName = chainMap[chainId] || 'ethereum';
1113
+ console.log("\uD83D\uDD04 Trying CoinGecko contract lookup: ".concat(contractAddress, " on ").concat(platformName));
1114
+ return [4 /*yield*/, get_price_from_coingecko_contract(contractAddress, platformName)];
1115
+ case 6:
1116
+ price = _a.sent();
1117
+ console.log("\uD83D\uDD04 CoinGecko contract returned price: $".concat(price));
1118
+ if (price > 0) {
1119
+ return [2 /*return*/, returnSource ? { price: price, source: 'coingecko-contract' } : price];
1120
+ }
1121
+ _a.label = 7;
1122
+ case 7:
1123
+ if (!(price === 0)) return [3 /*break*/, 13];
1124
+ asset = pioneer_discovery_1.assetData[caip] || pioneer_discovery_1.assetData[caip.toUpperCase()] || pioneer_discovery_1.assetData[caip.toLowerCase()];
1125
+ if (!(asset && asset.symbol)) return [3 /*break*/, 11];
1126
+ symbol = asset.symbol.toUpperCase();
1127
+ log.info(tag, "\uD83D\uDD04 Contract lookup failed, falling back to symbol-based APIs for: ".concat(symbol));
1128
+ // Try CoinMarketCap
1129
+ console.log("\uD83D\uDCCA Trying CoinMarketCap for ERC20 token: ".concat(symbol));
1130
+ return [4 /*yield*/, get_price_from_coinmarketcap(symbol)];
1131
+ case 8:
923
1132
  price = _a.sent();
924
1133
  console.log("\uD83D\uDCCA CoinMarketCap returned price: $".concat(price));
925
1134
  if (price > 0) {
926
1135
  return [2 /*return*/, returnSource ? { price: price, source: 'coinmarketcap' } : price];
927
1136
  }
928
- // Try CoinCap last (requires API key, currently rate limited)
929
- console.log("\uD83D\uDCB0 CoinMarketCap failed (price=$0), trying CoinCap for: ".concat(identifiers.coincapSymbol));
1137
+ // Try CoinCap
1138
+ console.log("\uD83D\uDCB0 Trying CoinCap for ERC20 token: ".concat(symbol.toLowerCase()));
1139
+ return [4 /*yield*/, get_price_from_coincap(symbol.toLowerCase())];
1140
+ case 9:
1141
+ price = _a.sent();
1142
+ console.log("\uD83D\uDCB0 CoinCap returned price: $".concat(price));
1143
+ if (price > 0) {
1144
+ return [2 /*return*/, returnSource ? { price: price, source: 'coincap' } : price];
1145
+ }
1146
+ // All 3 paid APIs failed - mark as unpriceable (likely scam/spam token)
1147
+ log.warn(tag, "\u274C Token not found in any of 3 paid APIs (CoinGecko, CMC, CoinCap): ".concat(caip));
1148
+ log.warn(tag, "\uD83D\uDEA8 Marking as maybeScam - will not retry pricing for this token");
1149
+ // Cache the failure so we never try again
1150
+ return [4 /*yield*/, cache_unpriceable_token(caip)];
1151
+ case 10:
1152
+ // Cache the failure so we never try again
1153
+ _a.sent();
1154
+ return [2 /*return*/, returnSource ? { price: 0, source: 'unpriceable', maybeScam: true } : 0];
1155
+ case 11:
1156
+ // Token not in our discovery database - can't get symbol for API calls
1157
+ log.warn(tag, "\u274C Token not in discovery database, cannot lookup symbol: ".concat(caip));
1158
+ log.warn(tag, "\uD83D\uDEA8 Marking as maybeScam - likely user-added spam token");
1159
+ // Cache the failure so we never try again
1160
+ return [4 /*yield*/, cache_unpriceable_token(caip)];
1161
+ case 12:
1162
+ // Cache the failure so we never try again
1163
+ _a.sent();
1164
+ return [2 /*return*/, returnSource ? { price: 0, source: 'unpriceable', maybeScam: true } : 0];
1165
+ case 13:
1166
+ if (!!identifiers) return [3 /*break*/, 15];
1167
+ log.warn(tag, "No way to lookup price for CAIP: ".concat(caip, " (no mapping, not ERC20, or all lookups failed)"));
1168
+ log.warn(tag, "\uD83D\uDEA8 Marking as maybeScam - token has no price source");
1169
+ // Cache the failure so we never try again
1170
+ return [4 /*yield*/, cache_unpriceable_token(caip)];
1171
+ case 14:
1172
+ // Cache the failure so we never try again
1173
+ _a.sent();
1174
+ return [2 /*return*/, returnSource ? { price: 0, source: 'unpriceable', maybeScam: true } : 0];
1175
+ case 15:
1176
+ // STEP 4: Try CoinMarketCap by symbol (requires API key)
1177
+ console.log("\uD83D\uDCCA Trying CoinMarketCap for: ".concat(identifiers.cmcSymbol));
1178
+ return [4 /*yield*/, get_price_from_coinmarketcap(identifiers.cmcSymbol)];
1179
+ case 16:
1180
+ price = _a.sent();
1181
+ console.log("\uD83D\uDCCA CoinMarketCap returned price: $".concat(price));
1182
+ if (price > 0) {
1183
+ return [2 /*return*/, returnSource ? { price: price, source: 'coinmarketcap' } : price];
1184
+ }
1185
+ // STEP 5: Try CoinCap by symbol (last resort)
1186
+ console.log("\uD83D\uDCB0 Trying CoinCap for: ".concat(identifiers.coincapSymbol));
930
1187
  return [4 /*yield*/, get_price_from_coincap(identifiers.coincapSymbol)];
931
- case 5:
1188
+ case 17:
932
1189
  price = _a.sent();
933
1190
  console.log("\uD83D\uDCB0 CoinCap returned price: $".concat(price));
934
1191
  if (price > 0) {
935
1192
  return [2 /*return*/, returnSource ? { price: price, source: 'coincap' } : price];
936
1193
  }
937
- if (!isCacao) return [3 /*break*/, 7];
1194
+ if (!isCacao) return [3 /*break*/, 19];
938
1195
  log.debug(tag, '🏔️ All pricing APIs failed for CACAO, trying MayaScan as final fallback');
939
1196
  return [4 /*yield*/, get_price_from_mayascan('cacao')];
940
- case 6:
1197
+ case 18:
941
1198
  cacaoPrice = _a.sent();
942
1199
  if (cacaoPrice > 0) {
943
1200
  return [2 /*return*/, returnSource ? { price: cacaoPrice, source: 'mayascan' } : cacaoPrice];
944
1201
  }
945
- _a.label = 7;
946
- case 7:
1202
+ _a.label = 19;
1203
+ case 19:
947
1204
  log.warn(tag, "\u274C No price found from any API for: ".concat(caip));
948
1205
  return [2 /*return*/, returnSource ? { price: 0, source: 'none' } : 0];
949
1206
  }
@@ -1082,3 +1339,134 @@ var get_price = function (asset) {
1082
1339
  });
1083
1340
  });
1084
1341
  };
1342
+ /**
1343
+ * NEW: Batch price lookup by CAIP
1344
+ * Fetches multiple asset prices in a single API call (up to 250 assets)
1345
+ * Reduces API calls by 95%+ for portfolio operations
1346
+ */
1347
+ var get_batch_prices_by_caip = function (caips_1) {
1348
+ return __awaiter(this, arguments, void 0, function (caips, returnSource) {
1349
+ var tag, results, caipToIdMap, specialCaips, _i, caips_2, caip, identifiers, coingeckoIds, batchSize, i, batch, idsParam, url, response, _a, batch_1, coingeckoId, mapping, price, error_6, _b, specialCaips_1, caip, price, error_7, missingCaips, _c, missingCaips_1, caip, _d, _e, error_8;
1350
+ var _f;
1351
+ if (returnSource === void 0) { returnSource = false; }
1352
+ return __generator(this, function (_g) {
1353
+ switch (_g.label) {
1354
+ case 0:
1355
+ tag = TAG + ' | get_batch_prices_by_caip | ';
1356
+ log.info(tag, "Fetching batch prices for ".concat(caips.length, " assets"));
1357
+ results = {};
1358
+ caipToIdMap = {};
1359
+ specialCaips = [];
1360
+ for (_i = 0, caips_2 = caips; _i < caips_2.length; _i++) {
1361
+ caip = caips_2[_i];
1362
+ // Handle special cases (MAYA, CACAO)
1363
+ if (caip === 'cosmos:mayachain-mainnet-v1/denom:maya' || caip.toLowerCase().includes('denom:maya')) {
1364
+ specialCaips.push(caip);
1365
+ continue;
1366
+ }
1367
+ if (caip === 'cosmos:mayachain-mainnet-v1/slip44:931') {
1368
+ specialCaips.push(caip);
1369
+ continue;
1370
+ }
1371
+ identifiers = caip_to_identifiers(caip);
1372
+ if (identifiers === null || identifiers === void 0 ? void 0 : identifiers.coingeckoId) {
1373
+ caipToIdMap[identifiers.coingeckoId] = { caip: caip, coingeckoId: identifiers.coingeckoId };
1374
+ }
1375
+ }
1376
+ coingeckoIds = Object.keys(caipToIdMap);
1377
+ if (!(coingeckoIds.length > 0)) return [3 /*break*/, 8];
1378
+ _g.label = 1;
1379
+ case 1:
1380
+ _g.trys.push([1, 7, , 8]);
1381
+ batchSize = 250;
1382
+ i = 0;
1383
+ _g.label = 2;
1384
+ case 2:
1385
+ if (!(i < coingeckoIds.length)) return [3 /*break*/, 6];
1386
+ batch = coingeckoIds.slice(i, i + batchSize);
1387
+ idsParam = batch.join(',');
1388
+ url = "".concat(URL_COINGECKO, "simple/price?ids=").concat(idsParam, "&vs_currencies=usd");
1389
+ log.debug(tag, "Fetching batch ".concat(i / batchSize + 1, ": ").concat(batch.length, " assets"));
1390
+ return [4 /*yield*/, axios.get(url)];
1391
+ case 3:
1392
+ response = _g.sent();
1393
+ // Map results back to CAIPs
1394
+ for (_a = 0, batch_1 = batch; _a < batch_1.length; _a++) {
1395
+ coingeckoId = batch_1[_a];
1396
+ mapping = caipToIdMap[coingeckoId];
1397
+ if ((_f = response.data[coingeckoId]) === null || _f === void 0 ? void 0 : _f.usd) {
1398
+ price = parseFloat(response.data[coingeckoId].usd);
1399
+ results[mapping.caip] = returnSource
1400
+ ? { price: price, source: 'coingecko' }
1401
+ : price;
1402
+ }
1403
+ }
1404
+ if (!(i + batchSize < coingeckoIds.length)) return [3 /*break*/, 5];
1405
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1500); })];
1406
+ case 4:
1407
+ _g.sent();
1408
+ _g.label = 5;
1409
+ case 5:
1410
+ i += batchSize;
1411
+ return [3 /*break*/, 2];
1412
+ case 6:
1413
+ log.info(tag, "\u2705 Batch fetched ".concat(Object.keys(results).length, " prices from CoinGecko"));
1414
+ return [3 /*break*/, 8];
1415
+ case 7:
1416
+ error_6 = _g.sent();
1417
+ log.error(tag, "CoinGecko batch fetch failed:", error_6.message);
1418
+ return [3 /*break*/, 8];
1419
+ case 8:
1420
+ _b = 0, specialCaips_1 = specialCaips;
1421
+ _g.label = 9;
1422
+ case 9:
1423
+ if (!(_b < specialCaips_1.length)) return [3 /*break*/, 14];
1424
+ caip = specialCaips_1[_b];
1425
+ _g.label = 10;
1426
+ case 10:
1427
+ _g.trys.push([10, 12, , 13]);
1428
+ return [4 /*yield*/, get_asset_price_by_caip(caip, returnSource)];
1429
+ case 11:
1430
+ price = _g.sent();
1431
+ results[caip] = price;
1432
+ return [3 /*break*/, 13];
1433
+ case 12:
1434
+ error_7 = _g.sent();
1435
+ log.warn(tag, "Failed to fetch special case: ".concat(caip));
1436
+ return [3 /*break*/, 13];
1437
+ case 13:
1438
+ _b++;
1439
+ return [3 /*break*/, 9];
1440
+ case 14:
1441
+ missingCaips = caips.filter(function (caip) { return !results[caip]; });
1442
+ if (!(missingCaips.length > 0)) return [3 /*break*/, 20];
1443
+ log.info(tag, "Falling back to individual fetch for ".concat(missingCaips.length, " missing prices"));
1444
+ _c = 0, missingCaips_1 = missingCaips;
1445
+ _g.label = 15;
1446
+ case 15:
1447
+ if (!(_c < missingCaips_1.length)) return [3 /*break*/, 20];
1448
+ caip = missingCaips_1[_c];
1449
+ _g.label = 16;
1450
+ case 16:
1451
+ _g.trys.push([16, 18, , 19]);
1452
+ _d = results;
1453
+ _e = caip;
1454
+ return [4 /*yield*/, get_asset_price_by_caip(caip, returnSource)];
1455
+ case 17:
1456
+ _d[_e] = _g.sent();
1457
+ return [3 /*break*/, 19];
1458
+ case 18:
1459
+ error_8 = _g.sent();
1460
+ log.warn(tag, "Failed to fetch fallback price for ".concat(caip));
1461
+ results[caip] = returnSource ? { price: 0, source: 'none' } : 0;
1462
+ return [3 /*break*/, 19];
1463
+ case 19:
1464
+ _c++;
1465
+ return [3 /*break*/, 15];
1466
+ case 20:
1467
+ log.info(tag, "\u2705 Batch complete: ".concat(Object.keys(results).length, "/").concat(caips.length, " prices fetched"));
1468
+ return [2 /*return*/, results];
1469
+ }
1470
+ });
1471
+ });
1472
+ };
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@pioneer-platform/markets",
3
- "version": "8.11.13",
3
+ "version": "8.11.24",
4
4
  "main": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "dependencies": {
7
- "@pioneer-platform/default-redis": "^8.11.0",
7
+ "@pioneer-platform/default-redis": "^8.11.7",
8
8
  "@pioneer-platform/loggerdog": "^8.11.0",
9
9
  "@pioneer-platform/pioneer-coins": "^9.11.0",
10
- "@pioneer-platform/pioneer-discovery": "^8.11.11",
10
+ "@pioneer-platform/pioneer-discovery": "^8.11.24",
11
11
  "@pioneer-platform/pioneer-types": "^8.11.0",
12
- "@pioneer-platform/pro-token": "^0.8.0",
12
+ "@pioneer-platform/pro-token": "^0.8.2",
13
13
  "@shapeshiftoss/caip": "^9.0.0-alpha.0",
14
14
  "axios": "^1.6.0",
15
15
  "axios-retry": "^3.2.0",
16
+ "bottleneck": "^2.19.5",
16
17
  "dotenv": "^8.2.0"
17
18
  },
18
19
  "scripts": {