@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.
- package/.turbo/turbo-build.log +1 -2
- package/CHANGELOG.md +18 -0
- package/lib/index.js +429 -41
- package/package.json +5 -4
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
[0m[2m[35m$[0m [2m[1mtsc -p .[0m
|
|
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:
|
|
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,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
|
-
|
|
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,
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
if (
|
|
772
|
-
log.warn(tag, "CoinMarketCap rate limit (".concat(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
if (
|
|
822
|
-
log.warn(tag, "CoinCap rate limit (".concat(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
871
|
-
log.debug(tag, "MayaScan error: ".concat(
|
|
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,
|
|
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 + ' |
|
|
891
|
-
log.debug(tag, "
|
|
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
|
-
|
|
906
|
-
|
|
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
|
|
911
|
-
console.log("\uD83E\uDD8E Trying CoinGecko
|
|
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
|
-
|
|
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
|
|
929
|
-
console.log("\uD83D\uDCB0
|
|
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
|
|
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*/,
|
|
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
|
|
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 =
|
|
946
|
-
case
|
|
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.
|
|
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.
|
|
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.24",
|
|
11
11
|
"@pioneer-platform/pioneer-types": "^8.11.0",
|
|
12
|
-
"@pioneer-platform/pro-token": "^0.8.
|
|
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": {
|