@pioneer-platform/markets 8.11.12 → 8.11.23

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/index.js +354 -73
  3. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @pioneer-platform/markets
2
2
 
3
+ ## 8.11.23
4
+
5
+ ### Patch Changes
6
+
7
+ - cache work
8
+ - Updated dependencies
9
+ - @pioneer-platform/pro-token@0.8.1
10
+ - @pioneer-platform/pioneer-discovery@8.11.23
11
+
12
+ ## 8.11.13
13
+
14
+ ### Patch Changes
15
+
16
+ - feed4f1: Fix Polygon (MATIC → POL) CoinMarketCap symbol mapping and add validation for invalid price data
17
+
3
18
  ## 8.11.12
4
19
 
5
20
  ### 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,30 @@ 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
+ // FIX: Rate limiters for external APIs
118
+ // CoinGecko free tier: 50 calls/minute = 1.2s between calls (we use 1.5s for safety)
119
+ var coingeckoLimiter = new Bottleneck({
120
+ minTime: 1500, // 1.5 seconds between calls (40 calls/minute)
121
+ maxConcurrent: 1
122
+ });
123
+ // CoinMarketCap: Varies by plan, using conservative limits
124
+ // Basic plan: 333 calls/day ≈ 1 call every 4 minutes, but allow bursts
125
+ var coinmarketcapLimiter = new Bottleneck({
126
+ reservoir: 10, // Allow 10 calls initially
127
+ reservoirRefreshAmount: 10,
128
+ reservoirRefreshInterval: 60 * 1000, // Refresh 10 calls every minute
129
+ minTime: 2000, // 2 seconds between calls
130
+ maxConcurrent: 1
131
+ });
132
+ // CoinCap API 3.0: Similar to CoinGecko
133
+ var coincapLimiter = new Bottleneck({
134
+ minTime: 1500, // 1.5 seconds between calls
135
+ maxConcurrent: 1
136
+ });
105
137
  module.exports = {
106
138
  init: function (settings) {
107
139
  if (settings === null || settings === void 0 ? void 0 : settings.apiKey) {
@@ -113,6 +145,10 @@ module.exports = {
113
145
  getAssetPriceByCaip: function (caip, returnSource) {
114
146
  return get_asset_price_by_caip(caip, returnSource);
115
147
  },
148
+ // NEW: Batch price lookup (significantly reduces API calls)
149
+ getBatchPricesByCaip: function (caips, returnSource) {
150
+ return get_batch_prices_by_caip(caips, returnSource);
151
+ },
116
152
  // Legacy bulk fetch functions
117
153
  getAssetsCoinCap: function () {
118
154
  return get_assets_coincap();
@@ -194,8 +230,10 @@ var update_cache = function () {
194
230
  if (!(j < assetsMatchSymbol.length)) return [3 /*break*/, 4];
195
231
  asset = assetsMatchSymbol[j];
196
232
  key = "coincap:" + asset.caip;
197
- return [4 /*yield*/, redis.setex(key, 3600, JSON.stringify(entry))];
233
+ // NEVER EXPIRE - data persists forever for instant responses
234
+ return [4 /*yield*/, redis.set(key, JSON.stringify(entry))];
198
235
  case 2:
236
+ // NEVER EXPIRE - data persists forever for instant responses
199
237
  _b.sent();
200
238
  log.info(tag, "saved: " + key);
201
239
  populatedCaips_1.add(asset.caip);
@@ -245,8 +283,10 @@ var update_cache = function () {
245
283
  if (!(_c < matchingAssets_1.length)) return [3 /*break*/, 4];
246
284
  matchingAsset = matchingAssets_1[_c];
247
285
  key = "coingecko:".concat(matchingAsset.caip);
248
- return [4 /*yield*/, redis.setex(key, 3600, JSON.stringify(coin))];
286
+ // NEVER EXPIRE - data persists forever for instant responses
287
+ return [4 /*yield*/, redis.set(key, JSON.stringify(coin))];
249
288
  case 2:
289
+ // NEVER EXPIRE - data persists forever for instant responses
250
290
  _d.sent();
251
291
  log.info(tag, "Saved ".concat(coin.symbol, " under ").concat(key));
252
292
  populatedCaips_1.add(matchingAsset.caip);
@@ -632,21 +672,36 @@ var caip_to_identifiers = function (caip) {
632
672
  // @ts-ignore
633
673
  var mappedCoingeckoId = pioneer_discovery_1.coingeckoMapping[caip];
634
674
  if (mappedCoingeckoId) {
675
+ // Special case: Polygon rebranded from MATIC to POL on CoinMarketCap (March 2024)
676
+ // CMC still has MATIC symbol but with null price - must use POL
677
+ var cmcSymbol = (caip === 'eip155:137/slip44:60') ? 'POL' : symbol;
678
+ // Special case: Base network (eip155:8453) uses ETH as native asset
679
+ // On CMC and CoinCap, "BASE" symbol refers to Base Protocol (different token)
680
+ // Must use "ETH" to get correct Ethereum price
681
+ if (caip === 'eip155:8453/slip44:60') {
682
+ cmcSymbol = 'ETH';
683
+ }
635
684
  return {
636
685
  symbol: symbol,
637
686
  coingeckoId: mappedCoingeckoId,
638
- cmcSymbol: symbol,
639
- coincapSymbol: symbol.toLowerCase()
687
+ cmcSymbol: cmcSymbol,
688
+ coincapSymbol: cmcSymbol.toLowerCase() // Use cmcSymbol for CoinCap too
640
689
  };
641
690
  }
642
691
  // PRIORITY 2: Try ShapeShift CAIP adapter (for coins we might have missed)
643
692
  var shapeshiftCoingeckoId = caip_1.adapters.assetIdToCoingecko(caip);
644
693
  if (shapeshiftCoingeckoId) {
694
+ // Special case: Polygon rebranded from MATIC to POL on CoinMarketCap (March 2024)
695
+ var cmcSymbol = (caip === 'eip155:137/slip44:60') ? 'POL' : symbol;
696
+ // Special case: Base network uses ETH as native asset
697
+ if (caip === 'eip155:8453/slip44:60') {
698
+ cmcSymbol = 'ETH';
699
+ }
645
700
  return {
646
701
  symbol: symbol,
647
702
  coingeckoId: shapeshiftCoingeckoId,
648
- cmcSymbol: symbol,
649
- coincapSymbol: symbol.toLowerCase()
703
+ cmcSymbol: cmcSymbol,
704
+ coincapSymbol: cmcSymbol.toLowerCase()
650
705
  };
651
706
  }
652
707
  // PRIORITY 3: Fallback to asset data
@@ -665,10 +720,12 @@ var caip_to_identifiers = function (caip) {
665
720
  };
666
721
  /**
667
722
  * Get price from CoinGecko by coin ID
723
+ * Now with rate limiting to prevent API spam
668
724
  */
669
725
  var get_price_from_coingecko = function (coingeckoId) {
670
726
  return __awaiter(this, void 0, void 0, function () {
671
- var tag, url, response, price, error_1, status_3;
727
+ var tag, error_1, status_3;
728
+ var _this = this;
672
729
  var _a;
673
730
  return __generator(this, function (_b) {
674
731
  switch (_b.label) {
@@ -677,18 +734,29 @@ var get_price_from_coingecko = function (coingeckoId) {
677
734
  _b.label = 1;
678
735
  case 1:
679
736
  _b.trys.push([1, 3, , 4]);
680
- url = "".concat(URL_COINGECKO, "simple/price?ids=").concat(coingeckoId, "&vs_currencies=usd");
681
- log.debug(tag, "Fetching from CoinGecko: ".concat(url));
682
- return [4 /*yield*/, axios.get(url)];
683
- case 2:
684
- response = _b.sent();
685
- if (response.data && response.data[coingeckoId] && response.data[coingeckoId].usd) {
686
- price = parseFloat(response.data[coingeckoId].usd);
687
- log.debug(tag, "\u2705 CoinGecko price for ".concat(coingeckoId, ": $").concat(price));
688
- return [2 /*return*/, price];
689
- }
690
- log.debug(tag, "No price data from CoinGecko for ".concat(coingeckoId));
691
- return [2 /*return*/, 0];
737
+ return [4 /*yield*/, coingeckoLimiter.schedule(function () { return __awaiter(_this, void 0, void 0, function () {
738
+ var url, response, price;
739
+ return __generator(this, function (_a) {
740
+ switch (_a.label) {
741
+ case 0:
742
+ url = "".concat(URL_COINGECKO, "simple/price?ids=").concat(coingeckoId, "&vs_currencies=usd");
743
+ log.debug(tag, "Fetching from CoinGecko: ".concat(url));
744
+ return [4 /*yield*/, axios.get(url)];
745
+ case 1:
746
+ response = _a.sent();
747
+ if (response.data && response.data[coingeckoId] && response.data[coingeckoId].usd) {
748
+ price = parseFloat(response.data[coingeckoId].usd);
749
+ log.debug(tag, "\u2705 CoinGecko price for ".concat(coingeckoId, ": $").concat(price));
750
+ return [2 /*return*/, price];
751
+ }
752
+ log.debug(tag, "No price data from CoinGecko for ".concat(coingeckoId));
753
+ return [2 /*return*/, 0];
754
+ }
755
+ });
756
+ }); })];
757
+ case 2:
758
+ // Apply rate limiting
759
+ return [2 /*return*/, _b.sent()];
692
760
  case 3:
693
761
  error_1 = _b.sent();
694
762
  status_3 = ((_a = error_1.response) === null || _a === void 0 ? void 0 : _a.status) || error_1.status;
@@ -706,10 +774,12 @@ var get_price_from_coingecko = function (coingeckoId) {
706
774
  };
707
775
  /**
708
776
  * Get price from CoinMarketCap by symbol
777
+ * Now with rate limiting to prevent API spam
709
778
  */
710
779
  var get_price_from_coinmarketcap = function (symbol) {
711
780
  return __awaiter(this, void 0, void 0, function () {
712
- var tag, url, response, price, error_2, status_4;
781
+ var tag, error_2, status_4;
782
+ var _this = this;
713
783
  var _a;
714
784
  return __generator(this, function (_b) {
715
785
  switch (_b.label) {
@@ -722,23 +792,42 @@ var get_price_from_coinmarketcap = function (symbol) {
722
792
  _b.label = 1;
723
793
  case 1:
724
794
  _b.trys.push([1, 3, , 4]);
725
- url = "".concat(URL_COINMARKETCAP, "cryptocurrency/quotes/latest?symbol=").concat(symbol, "&convert=USD");
726
- log.debug(tag, "Fetching from CoinMarketCap: ".concat(url));
727
- return [4 /*yield*/, axios.get(url, {
728
- headers: {
729
- 'X-CMC_PRO_API_KEY': CMC_PRO_API_KEY,
730
- 'Accept': 'application/json'
731
- }
732
- })];
733
- case 2:
734
- response = _b.sent();
735
- if (response.data && response.data.data && response.data.data[symbol]) {
736
- price = parseFloat(response.data.data[symbol].quote.USD.price);
737
- log.debug(tag, "\u2705 CoinMarketCap price for ".concat(symbol, ": $").concat(price));
738
- return [2 /*return*/, price];
739
- }
740
- log.debug(tag, "No price data from CoinMarketCap for ".concat(symbol));
741
- return [2 /*return*/, 0];
795
+ return [4 /*yield*/, coinmarketcapLimiter.schedule(function () { return __awaiter(_this, void 0, void 0, function () {
796
+ var url, response, coinData, priceData, price;
797
+ var _a, _b;
798
+ return __generator(this, function (_c) {
799
+ switch (_c.label) {
800
+ case 0:
801
+ url = "".concat(URL_COINMARKETCAP, "cryptocurrency/quotes/latest?symbol=").concat(symbol, "&convert=USD");
802
+ log.debug(tag, "Fetching from CoinMarketCap: ".concat(url));
803
+ return [4 /*yield*/, axios.get(url, {
804
+ headers: {
805
+ 'X-CMC_PRO_API_KEY': CMC_PRO_API_KEY,
806
+ 'Accept': 'application/json'
807
+ }
808
+ })];
809
+ case 1:
810
+ response = _c.sent();
811
+ if (response.data && response.data.data && response.data.data[symbol]) {
812
+ coinData = response.data.data[symbol];
813
+ priceData = (_b = (_a = coinData.quote) === null || _a === void 0 ? void 0 : _a.USD) === null || _b === void 0 ? void 0 : _b.price;
814
+ if (!priceData || isNaN(parseFloat(priceData))) {
815
+ log.warn(tag, "\u26A0\uFE0F CoinMarketCap returned invalid price for ".concat(symbol));
816
+ log.warn(tag, "Symbol: ".concat(coinData.symbol, ", Slug: ").concat(coinData.slug, ", Quote keys:"), Object.keys(coinData.quote || {}));
817
+ return [2 /*return*/, 0];
818
+ }
819
+ price = parseFloat(priceData);
820
+ log.debug(tag, "\u2705 CoinMarketCap price for ".concat(symbol, ": $").concat(price));
821
+ return [2 /*return*/, price];
822
+ }
823
+ log.debug(tag, "No price data from CoinMarketCap for ".concat(symbol));
824
+ return [2 /*return*/, 0];
825
+ }
826
+ });
827
+ }); })];
828
+ case 2:
829
+ // Apply rate limiting
830
+ return [2 /*return*/, _b.sent()];
742
831
  case 3:
743
832
  error_2 = _b.sent();
744
833
  status_4 = ((_a = error_2.response) === null || _a === void 0 ? void 0 : _a.status) || error_2.status;
@@ -756,10 +845,12 @@ var get_price_from_coinmarketcap = function (symbol) {
756
845
  };
757
846
  /**
758
847
  * Get price from CoinCap by symbol
848
+ * Now with rate limiting to prevent API spam
759
849
  */
760
850
  var get_price_from_coincap = function (symbol) {
761
851
  return __awaiter(this, void 0, void 0, function () {
762
- var tag, url, response, price, error_3, status_5;
852
+ var tag, error_3, status_5;
853
+ var _this = this;
763
854
  var _a;
764
855
  return __generator(this, function (_b) {
765
856
  switch (_b.label) {
@@ -772,23 +863,34 @@ var get_price_from_coincap = function (symbol) {
772
863
  _b.label = 1;
773
864
  case 1:
774
865
  _b.trys.push([1, 3, , 4]);
775
- url = "".concat(URL_COINCAP, "assets/").concat(symbol);
776
- log.debug(tag, "Fetching from CoinCap: ".concat(url));
777
- return [4 /*yield*/, axios.get(url, {
778
- headers: {
779
- 'Accept': 'application/json',
780
- 'Authorization': "Bearer ".concat(COINCAP_API_KEY)
781
- }
782
- })];
783
- case 2:
784
- response = _b.sent();
785
- if (response.data && response.data.data && response.data.data.priceUsd) {
786
- price = parseFloat(response.data.data.priceUsd);
787
- log.debug(tag, "\u2705 CoinCap price for ".concat(symbol, ": $").concat(price));
788
- return [2 /*return*/, price];
789
- }
790
- log.debug(tag, "No price data from CoinCap for ".concat(symbol));
791
- return [2 /*return*/, 0];
866
+ return [4 /*yield*/, coincapLimiter.schedule(function () { return __awaiter(_this, void 0, void 0, function () {
867
+ var url, response, price;
868
+ return __generator(this, function (_a) {
869
+ switch (_a.label) {
870
+ case 0:
871
+ url = "".concat(URL_COINCAP, "assets/").concat(symbol);
872
+ log.debug(tag, "Fetching from CoinCap: ".concat(url));
873
+ return [4 /*yield*/, axios.get(url, {
874
+ headers: {
875
+ 'Accept': 'application/json',
876
+ 'Authorization': "Bearer ".concat(COINCAP_API_KEY)
877
+ }
878
+ })];
879
+ case 1:
880
+ response = _a.sent();
881
+ if (response.data && response.data.data && response.data.data.priceUsd) {
882
+ price = parseFloat(response.data.data.priceUsd);
883
+ log.debug(tag, "\u2705 CoinCap price for ".concat(symbol, ": $").concat(price));
884
+ return [2 /*return*/, price];
885
+ }
886
+ log.debug(tag, "No price data from CoinCap for ".concat(symbol));
887
+ return [2 /*return*/, 0];
888
+ }
889
+ });
890
+ }); })];
891
+ case 2:
892
+ // Apply rate limiting
893
+ return [2 /*return*/, _b.sent()];
792
894
  case 3:
793
895
  error_3 = _b.sent();
794
896
  status_5 = ((_a = error_3.response) === null || _a === void 0 ? void 0 : _a.status) || error_3.status;
@@ -805,12 +907,14 @@ var get_price_from_coincap = function (symbol) {
805
907
  });
806
908
  };
807
909
  /**
808
- * Get price from MayaScan for MAYA token
809
- * MAYA token is different from CACAO (the gas token)
910
+ * Get price from MayaScan for MAYA token or CACAO gas token
911
+ * MAYA token (denom:maya) is different from CACAO (slip44:931 - the gas token)
912
+ * @param asset - 'maya' for MAYA token, 'cacao' for CACAO gas token
810
913
  */
811
914
  var get_price_from_mayascan = function () {
812
- return __awaiter(this, void 0, void 0, function () {
915
+ return __awaiter(this, arguments, void 0, function (asset) {
813
916
  var tag, url, response, price, error_4;
917
+ if (asset === void 0) { asset = 'maya'; }
814
918
  return __generator(this, function (_a) {
815
919
  switch (_a.label) {
816
920
  case 0:
@@ -819,16 +923,24 @@ var get_price_from_mayascan = function () {
819
923
  case 1:
820
924
  _a.trys.push([1, 3, , 4]);
821
925
  url = 'https://www.mayascan.org/api/maya/price?days=1';
822
- log.debug(tag, "Fetching MAYA token price from MayaScan: ".concat(url));
926
+ log.debug(tag, "Fetching ".concat(asset.toUpperCase(), " price from MayaScan: ").concat(url));
823
927
  return [4 /*yield*/, axios.get(url)];
824
928
  case 2:
825
929
  response = _a.sent();
826
- if (response.data && response.data.mayaPriceInUsd) {
827
- price = parseFloat(response.data.mayaPriceInUsd);
828
- log.debug(tag, "\u2705 MayaScan price for MAYA token: $".concat(price));
829
- return [2 /*return*/, price];
930
+ if (response.data) {
931
+ price = 0;
932
+ if (asset === 'maya' && response.data.mayaPriceInUsd) {
933
+ price = parseFloat(response.data.mayaPriceInUsd);
934
+ }
935
+ else if (asset === 'cacao' && response.data.cacaoPriceInUsd) {
936
+ price = parseFloat(response.data.cacaoPriceInUsd);
937
+ }
938
+ if (price > 0) {
939
+ log.debug(tag, "\u2705 MayaScan price for ".concat(asset.toUpperCase(), ": $").concat(price));
940
+ return [2 /*return*/, price];
941
+ }
830
942
  }
831
- log.debug(tag, 'No MAYA price data from MayaScan');
943
+ log.debug(tag, "No ".concat(asset.toUpperCase(), " price data from MayaScan"));
832
944
  return [2 /*return*/, 0];
833
945
  case 3:
834
946
  error_4 = _a.sent();
@@ -846,16 +958,43 @@ var get_price_from_mayascan = function () {
846
958
  */
847
959
  var get_asset_price_by_caip = function (caip_2) {
848
960
  return __awaiter(this, arguments, void 0, function (caip, returnSource) {
849
- var tag, mayaPrice, identifiers, price;
961
+ var tag, pendingMap, existingRequest, pricePromise;
962
+ if (returnSource === void 0) { returnSource = false; }
963
+ return __generator(this, function (_a) {
964
+ tag = TAG + ' | get_asset_price_by_caip | ';
965
+ log.debug(tag, "Looking up price for: ".concat(caip));
966
+ pendingMap = returnSource ? pendingPriceRequestsWithSource : pendingPriceRequests;
967
+ existingRequest = pendingMap.get(caip);
968
+ if (existingRequest) {
969
+ log.debug(tag, "Coalescing duplicate request for: ".concat(caip));
970
+ return [2 /*return*/, existingRequest];
971
+ }
972
+ pricePromise = get_asset_price_by_caip_internal(caip, returnSource);
973
+ // Store promise for request coalescing
974
+ pendingMap.set(caip, pricePromise);
975
+ // Cleanup after completion (success or failure)
976
+ pricePromise.finally(function () {
977
+ pendingMap.delete(caip);
978
+ });
979
+ return [2 /*return*/, pricePromise];
980
+ });
981
+ });
982
+ };
983
+ /**
984
+ * Internal implementation (separated for request coalescing)
985
+ */
986
+ var get_asset_price_by_caip_internal = function (caip_2) {
987
+ return __awaiter(this, arguments, void 0, function (caip, returnSource) {
988
+ var tag, mayaPrice, isCacao, identifiers, price, cacaoPrice;
850
989
  if (returnSource === void 0) { returnSource = false; }
851
990
  return __generator(this, function (_a) {
852
991
  switch (_a.label) {
853
992
  case 0:
854
- tag = TAG + ' | get_asset_price_by_caip | ';
855
- log.debug(tag, "Looking up price for: ".concat(caip));
993
+ tag = TAG + ' | get_asset_price_by_caip_internal | ';
994
+ log.debug(tag, "Fetching price for: ".concat(caip));
856
995
  if (!(caip === 'cosmos:mayachain-mainnet-v1/denom:maya' || caip.toLowerCase().includes('denom:maya'))) return [3 /*break*/, 2];
857
996
  log.debug(tag, 'MAYA token detected, using MayaScan API');
858
- return [4 /*yield*/, get_price_from_mayascan()];
997
+ return [4 /*yield*/, get_price_from_mayascan('maya')];
859
998
  case 1:
860
999
  mayaPrice = _a.sent();
861
1000
  if (mayaPrice > 0) {
@@ -864,6 +1003,7 @@ var get_asset_price_by_caip = function (caip_2) {
864
1003
  log.warn(tag, '❌ Failed to get MAYA price from MayaScan, trying standard APIs');
865
1004
  _a.label = 2;
866
1005
  case 2:
1006
+ isCacao = caip === 'cosmos:mayachain-mainnet-v1/slip44:931';
867
1007
  identifiers = caip_to_identifiers(caip);
868
1008
  if (!identifiers) {
869
1009
  log.warn(tag, "No identifier mapping found for CAIP: ".concat(caip));
@@ -897,6 +1037,16 @@ var get_asset_price_by_caip = function (caip_2) {
897
1037
  if (price > 0) {
898
1038
  return [2 /*return*/, returnSource ? { price: price, source: 'coincap' } : price];
899
1039
  }
1040
+ if (!isCacao) return [3 /*break*/, 7];
1041
+ log.debug(tag, '🏔️ All pricing APIs failed for CACAO, trying MayaScan as final fallback');
1042
+ return [4 /*yield*/, get_price_from_mayascan('cacao')];
1043
+ case 6:
1044
+ cacaoPrice = _a.sent();
1045
+ if (cacaoPrice > 0) {
1046
+ return [2 /*return*/, returnSource ? { price: cacaoPrice, source: 'mayascan' } : cacaoPrice];
1047
+ }
1048
+ _a.label = 7;
1049
+ case 7:
900
1050
  log.warn(tag, "\u274C No price found from any API for: ".concat(caip));
901
1051
  return [2 /*return*/, returnSource ? { price: 0, source: 'none' } : 0];
902
1052
  }
@@ -1035,3 +1185,134 @@ var get_price = function (asset) {
1035
1185
  });
1036
1186
  });
1037
1187
  };
1188
+ /**
1189
+ * NEW: Batch price lookup by CAIP
1190
+ * Fetches multiple asset prices in a single API call (up to 250 assets)
1191
+ * Reduces API calls by 95%+ for portfolio operations
1192
+ */
1193
+ var get_batch_prices_by_caip = function (caips_1) {
1194
+ return __awaiter(this, arguments, void 0, function (caips, returnSource) {
1195
+ var tag, results, caipToIdMap, specialCaips, _i, caips_2, caip, identifiers, coingeckoIds, batchSize, i, batch, idsParam, url, response, _a, batch_1, coingeckoId, mapping, price, error_5, _b, specialCaips_1, caip, price, error_6, missingCaips, _c, missingCaips_1, caip, _d, _e, error_7;
1196
+ var _f;
1197
+ if (returnSource === void 0) { returnSource = false; }
1198
+ return __generator(this, function (_g) {
1199
+ switch (_g.label) {
1200
+ case 0:
1201
+ tag = TAG + ' | get_batch_prices_by_caip | ';
1202
+ log.info(tag, "Fetching batch prices for ".concat(caips.length, " assets"));
1203
+ results = {};
1204
+ caipToIdMap = {};
1205
+ specialCaips = [];
1206
+ for (_i = 0, caips_2 = caips; _i < caips_2.length; _i++) {
1207
+ caip = caips_2[_i];
1208
+ // Handle special cases (MAYA, CACAO)
1209
+ if (caip === 'cosmos:mayachain-mainnet-v1/denom:maya' || caip.toLowerCase().includes('denom:maya')) {
1210
+ specialCaips.push(caip);
1211
+ continue;
1212
+ }
1213
+ if (caip === 'cosmos:mayachain-mainnet-v1/slip44:931') {
1214
+ specialCaips.push(caip);
1215
+ continue;
1216
+ }
1217
+ identifiers = caip_to_identifiers(caip);
1218
+ if (identifiers === null || identifiers === void 0 ? void 0 : identifiers.coingeckoId) {
1219
+ caipToIdMap[identifiers.coingeckoId] = { caip: caip, coingeckoId: identifiers.coingeckoId };
1220
+ }
1221
+ }
1222
+ coingeckoIds = Object.keys(caipToIdMap);
1223
+ if (!(coingeckoIds.length > 0)) return [3 /*break*/, 8];
1224
+ _g.label = 1;
1225
+ case 1:
1226
+ _g.trys.push([1, 7, , 8]);
1227
+ batchSize = 250;
1228
+ i = 0;
1229
+ _g.label = 2;
1230
+ case 2:
1231
+ if (!(i < coingeckoIds.length)) return [3 /*break*/, 6];
1232
+ batch = coingeckoIds.slice(i, i + batchSize);
1233
+ idsParam = batch.join(',');
1234
+ url = "".concat(URL_COINGECKO, "simple/price?ids=").concat(idsParam, "&vs_currencies=usd");
1235
+ log.debug(tag, "Fetching batch ".concat(i / batchSize + 1, ": ").concat(batch.length, " assets"));
1236
+ return [4 /*yield*/, axios.get(url)];
1237
+ case 3:
1238
+ response = _g.sent();
1239
+ // Map results back to CAIPs
1240
+ for (_a = 0, batch_1 = batch; _a < batch_1.length; _a++) {
1241
+ coingeckoId = batch_1[_a];
1242
+ mapping = caipToIdMap[coingeckoId];
1243
+ if ((_f = response.data[coingeckoId]) === null || _f === void 0 ? void 0 : _f.usd) {
1244
+ price = parseFloat(response.data[coingeckoId].usd);
1245
+ results[mapping.caip] = returnSource
1246
+ ? { price: price, source: 'coingecko' }
1247
+ : price;
1248
+ }
1249
+ }
1250
+ if (!(i + batchSize < coingeckoIds.length)) return [3 /*break*/, 5];
1251
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1500); })];
1252
+ case 4:
1253
+ _g.sent();
1254
+ _g.label = 5;
1255
+ case 5:
1256
+ i += batchSize;
1257
+ return [3 /*break*/, 2];
1258
+ case 6:
1259
+ log.info(tag, "\u2705 Batch fetched ".concat(Object.keys(results).length, " prices from CoinGecko"));
1260
+ return [3 /*break*/, 8];
1261
+ case 7:
1262
+ error_5 = _g.sent();
1263
+ log.error(tag, "CoinGecko batch fetch failed:", error_5.message);
1264
+ return [3 /*break*/, 8];
1265
+ case 8:
1266
+ _b = 0, specialCaips_1 = specialCaips;
1267
+ _g.label = 9;
1268
+ case 9:
1269
+ if (!(_b < specialCaips_1.length)) return [3 /*break*/, 14];
1270
+ caip = specialCaips_1[_b];
1271
+ _g.label = 10;
1272
+ case 10:
1273
+ _g.trys.push([10, 12, , 13]);
1274
+ return [4 /*yield*/, get_asset_price_by_caip(caip, returnSource)];
1275
+ case 11:
1276
+ price = _g.sent();
1277
+ results[caip] = price;
1278
+ return [3 /*break*/, 13];
1279
+ case 12:
1280
+ error_6 = _g.sent();
1281
+ log.warn(tag, "Failed to fetch special case: ".concat(caip));
1282
+ return [3 /*break*/, 13];
1283
+ case 13:
1284
+ _b++;
1285
+ return [3 /*break*/, 9];
1286
+ case 14:
1287
+ missingCaips = caips.filter(function (caip) { return !results[caip]; });
1288
+ if (!(missingCaips.length > 0)) return [3 /*break*/, 20];
1289
+ log.info(tag, "Falling back to individual fetch for ".concat(missingCaips.length, " missing prices"));
1290
+ _c = 0, missingCaips_1 = missingCaips;
1291
+ _g.label = 15;
1292
+ case 15:
1293
+ if (!(_c < missingCaips_1.length)) return [3 /*break*/, 20];
1294
+ caip = missingCaips_1[_c];
1295
+ _g.label = 16;
1296
+ case 16:
1297
+ _g.trys.push([16, 18, , 19]);
1298
+ _d = results;
1299
+ _e = caip;
1300
+ return [4 /*yield*/, get_asset_price_by_caip(caip, returnSource)];
1301
+ case 17:
1302
+ _d[_e] = _g.sent();
1303
+ return [3 /*break*/, 19];
1304
+ case 18:
1305
+ error_7 = _g.sent();
1306
+ log.warn(tag, "Failed to fetch fallback price for ".concat(caip));
1307
+ results[caip] = returnSource ? { price: 0, source: 'none' } : 0;
1308
+ return [3 /*break*/, 19];
1309
+ case 19:
1310
+ _c++;
1311
+ return [3 /*break*/, 15];
1312
+ case 20:
1313
+ log.info(tag, "\u2705 Batch complete: ".concat(Object.keys(results).length, "/").concat(caips.length, " prices fetched"));
1314
+ return [2 /*return*/, results];
1315
+ }
1316
+ });
1317
+ });
1318
+ };
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@pioneer-platform/markets",
3
- "version": "8.11.12",
3
+ "version": "8.11.23",
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.23",
11
11
  "@pioneer-platform/pioneer-types": "^8.11.0",
12
- "@pioneer-platform/pro-token": "^0.8.0",
12
+ "@pioneer-platform/pro-token": "^0.8.1",
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": {