@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.
- package/CHANGELOG.md +15 -0
- package/lib/index.js +354 -73
- 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:
|
|
72
|
+
retries: 3, // Reduced from 5 to 3
|
|
72
73
|
retryDelay: function (retryCount) {
|
|
73
74
|
console.log("retry attempt: ".concat(retryCount));
|
|
74
|
-
return retryCount *
|
|
75
|
+
return retryCount * 2000; // Increased from 1s to 2s backoff
|
|
75
76
|
},
|
|
76
77
|
retryCondition: function (error) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
639
|
-
coincapSymbol:
|
|
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:
|
|
649
|
-
coincapSymbol:
|
|
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,
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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,
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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,
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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,
|
|
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
|
|
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
|
|
827
|
-
price =
|
|
828
|
-
|
|
829
|
-
|
|
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,
|
|
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,
|
|
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 + ' |
|
|
855
|
-
log.debug(tag, "
|
|
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.
|
|
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.
|
|
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.
|
|
10
|
+
"@pioneer-platform/pioneer-discovery": "^8.11.23",
|
|
11
11
|
"@pioneer-platform/pioneer-types": "^8.11.0",
|
|
12
|
-
"@pioneer-platform/pro-token": "^0.8.
|
|
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": {
|