@pioneer-platform/markets 8.12.0 → 8.13.0

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/lib/index.js CHANGED
@@ -58,6 +58,11 @@ var TAG = " | market-module | ";
58
58
  var pioneer_discovery_1 = require("@pioneer-platform/pioneer-discovery");
59
59
  var caip_1 = require("@shapeshiftoss/caip");
60
60
  var Bottleneck = require('bottleneck');
61
+ var token_bucket_manager_1 = require("./token-bucket-manager");
62
+ var metrics_logger_1 = require("./metrics-logger");
63
+ var refill_scheduler_1 = require("./refill-scheduler");
64
+ var ccxt_pricing_1 = require("./ccxt-pricing");
65
+ var api_metrics_reporter_1 = require("./api-metrics-reporter");
61
66
  var axiosLib = require('axios');
62
67
  var Axios = axiosLib.default || axiosLib;
63
68
  var https = require('https');
@@ -121,15 +126,44 @@ var unpriceableTokensCache = new Map();
121
126
  var UNPRICEABLE_TOKEN_PREFIX = 'unpriceable_token:';
122
127
  var UNPRICEABLE_TOKEN_TTL = 7 * 24 * 60 * 60; // 7 days
123
128
  var UNPRICEABLE_TOKEN_TTL_MS = UNPRICEABLE_TOKEN_TTL * 1000;
129
+ // Redis keys for monitoring
130
+ var REDIS_UNPRICEABLE_SET = 'markets:unpriceable_tokens';
131
+ var REDIS_API_METRICS_KEY = 'markets:api_metrics';
132
+ // CRITICAL: Major cryptocurrencies that should NEVER be marked as unpriceable
133
+ // Even if all APIs fail, these are known legitimate assets - API failures are temporary
134
+ var MAJOR_CRYPTO_WHITELIST = new Set([
135
+ 'bip122:000000000019d6689c085ae165831e93/slip44:0', // Bitcoin
136
+ 'eip155:1/slip44:60', // Ethereum
137
+ 'eip155:56/slip44:60', // BNB Chain
138
+ 'eip155:137/slip44:60', // Polygon
139
+ 'eip155:42161/slip44:60', // Arbitrum
140
+ 'eip155:10/slip44:60', // Optimism
141
+ 'eip155:8453/slip44:60', // Base
142
+ 'cosmos:cosmoshub-4/slip44:118', // Cosmos
143
+ 'cosmos:osmosis-1/slip44:118', // Osmosis
144
+ 'cosmos:thorchain-mainnet-v1/slip44:931', // Thorchain
145
+ 'cosmos:mayachain-mainnet-v1/slip44:931', // Mayachain
146
+ 'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144', // XRP
147
+ 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2', // Litecoin
148
+ 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3', // Dogecoin
149
+ 'bip122:000007d91d1254d60e2dd1ae58038307/slip44:5', // Dash
150
+ 'bip122:000000000000000000651ef99cb9fcbe/slip44:145', // Bitcoin Cash
151
+ ]);
124
152
  // NOTE: Rate limiting is now enforced at the worker level (pioneer-server)
125
153
  // using a Redis mutex to ensure ONLY ONE market API call at a time across all processes
126
154
  // The Bottleneck limiters below are kept as a safety net for direct module usage
155
+ // Token bucket and metrics system
156
+ var tokenBucketManager = null;
157
+ var metricsLogger = null;
158
+ var isInitializing = false;
127
159
  module.exports = {
128
160
  init: function (settings) {
129
161
  if (settings === null || settings === void 0 ? void 0 : settings.apiKey) {
130
162
  COINGECKO_API_KEY = settings.apiKey;
131
163
  }
132
164
  //if(!COINGECKO_API_KEY) throw Error("api key required! set env COINGECKO_API_KEY")
165
+ // Start hourly metrics reporting to Discord
166
+ (0, api_metrics_reporter_1.startHourlyReporting)();
133
167
  },
134
168
  // NEW: CAIP-first individual asset price lookup
135
169
  getAssetPriceByCaip: function (caip, returnSource) {
@@ -139,6 +173,26 @@ module.exports = {
139
173
  getBatchPricesByCaip: function (caips, returnSource) {
140
174
  return get_batch_prices_by_caip(caips, returnSource);
141
175
  },
176
+ // NEW: CCXT direct access (for testing/debugging)
177
+ getPriceFromCCXT: function (symbol) {
178
+ return (0, ccxt_pricing_1.getPriceFromCCXT)(symbol);
179
+ },
180
+ getBatchPricesFromCCXT: function (symbols) {
181
+ return (0, ccxt_pricing_1.getBatchPricesFromCCXT)(symbols);
182
+ },
183
+ // NEW: Metrics and monitoring
184
+ getCurrentMetrics: function () {
185
+ return (0, api_metrics_reporter_1.getCurrentMetrics)();
186
+ },
187
+ getUnpriceableTokens: function (limit) {
188
+ return (0, api_metrics_reporter_1.getUnpriceableTokens)(limit);
189
+ },
190
+ clearUnpriceableToken: function (caip) {
191
+ return (0, api_metrics_reporter_1.clearUnpriceableToken)(caip);
192
+ },
193
+ sendMetricsReport: function () {
194
+ return (0, api_metrics_reporter_1.sendHourlyMetricsReport)();
195
+ },
142
196
  // Legacy bulk fetch functions
143
197
  getAssetsCoinCap: function () {
144
198
  return get_assets_coincap();
@@ -163,6 +217,16 @@ module.exports = {
163
217
  },
164
218
  buildBalances: function (marketInfoCoinCap, marketInfoCoinGecko, pubkeys, context) {
165
219
  return build_balances(marketInfoCoinCap, marketInfoCoinGecko, pubkeys);
220
+ },
221
+ // NEW: Token bucket management
222
+ getTokenBucketManager: function () {
223
+ return tokenBucketManager;
224
+ },
225
+ getMetricsLogger: function () {
226
+ return metricsLogger;
227
+ },
228
+ initializeTokenBuckets: function () {
229
+ return initializeTokenBuckets();
166
230
  }
167
231
  };
168
232
  var update_cache = function () {
@@ -736,54 +800,151 @@ var is_unpriceable_token = function (caip) {
736
800
  * Cache token as unpriceable (not found in any of our 3 paid APIs)
737
801
  * Stores in-memory for instant lookups + Redis for persistence
738
802
  * This prevents wasting API calls on scam/spam tokens
803
+ *
804
+ * CRITICAL PROTECTION: Major cryptocurrencies are NEVER marked as unpriceable
805
+ * API failures for BTC, ETH, etc. are temporary - we retry instead of caching failure
739
806
  */
740
807
  var cache_unpriceable_token = function (caip) {
741
808
  return __awaiter(this, void 0, void 0, function () {
742
- var tag, key;
809
+ var tag, error_1, key;
743
810
  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));
811
+ switch (_a.label) {
812
+ case 0:
813
+ tag = TAG + ' | cache_unpriceable_token | ';
814
+ // CRITICAL: Never mark major cryptocurrencies as unpriceable!
815
+ // If all APIs fail for Bitcoin/Ethereum/etc., it's an API issue, not a scam token
816
+ if (MAJOR_CRYPTO_WHITELIST.has(caip)) {
817
+ log.error(tag, "\uD83D\uDEA8 BLOCKED: Attempted to mark major cryptocurrency as unpriceable: ".concat(caip));
818
+ log.error(tag, "This indicates all 3 price APIs failed - likely rate limiting or API outage");
819
+ log.error(tag, "NOT caching as unpriceable - will retry on next request");
820
+ return [2 /*return*/]; // Do NOT cache - allow retries for major coins
821
+ }
822
+ // Store in-memory for instant lookups
823
+ unpriceableTokensCache.set(caip, {
824
+ markedAt: Date.now(),
825
+ maybeScam: true
826
+ });
827
+ _a.label = 1;
828
+ case 1:
829
+ _a.trys.push([1, 3, , 4]);
830
+ return [4 /*yield*/, redis.sadd(REDIS_UNPRICEABLE_SET, caip)];
831
+ case 2:
832
+ _a.sent();
833
+ log.debug(tag, "Added ".concat(caip, " to unpriceable tokens set"));
834
+ return [3 /*break*/, 4];
835
+ case 3:
836
+ error_1 = _a.sent();
837
+ log.debug(tag, "Error adding to unpriceable set: ".concat(error_1));
838
+ return [3 /*break*/, 4];
839
+ case 4:
840
+ // Also persist to Redis for cross-process sharing (but don't wait/block)
841
+ try {
842
+ key = UNPRICEABLE_TOKEN_PREFIX + caip;
843
+ redis.setex(key, UNPRICEABLE_TOKEN_TTL, JSON.stringify({
844
+ caip: caip,
845
+ markedAt: new Date().toISOString(),
846
+ reason: 'not_found_in_paid_apis',
847
+ maybeScam: true
848
+ })).catch(function (err) {
849
+ // Ignore Redis errors - in-memory cache is primary
850
+ log.debug(tag, "Redis error (non-blocking): ".concat(err.message));
851
+ });
852
+ log.info(tag, "\u2705 Cached token as unpriceable for 7 days: ".concat(caip));
853
+ }
854
+ catch (error) {
855
+ // If Redis fails, don't block - in-memory cache is already set
856
+ log.debug(tag, "Redis error caching unpriceable token (non-critical): ".concat(error));
857
+ }
858
+ return [2 /*return*/];
763
859
  }
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));
860
+ });
861
+ });
862
+ };
863
+ /**
864
+ * Track API success/failure for monitoring
865
+ */
866
+ var trackAPIMetric = function (api, success, error) {
867
+ return __awaiter(this, void 0, void 0, function () {
868
+ var tag, metricsData, metrics, apiKey, error_2;
869
+ return __generator(this, function (_a) {
870
+ switch (_a.label) {
871
+ case 0:
872
+ tag = TAG + ' | trackAPIMetric | ';
873
+ _a.label = 1;
874
+ case 1:
875
+ _a.trys.push([1, 4, , 5]);
876
+ return [4 /*yield*/, redis.get(REDIS_API_METRICS_KEY)];
877
+ case 2:
878
+ metricsData = _a.sent();
879
+ metrics = metricsData ? JSON.parse(metricsData) : {
880
+ coingecko: { success: 0, failures: 0 },
881
+ coinmarketcap: { success: 0, failures: 0 },
882
+ coincap: { success: 0, failures: 0 },
883
+ ccxt: { success: 0, failures: 0 },
884
+ timestamp: Date.now()
885
+ };
886
+ apiKey = api.toLowerCase().replace(/[^a-z]/g, '');
887
+ if (metrics[apiKey]) {
888
+ if (success) {
889
+ metrics[apiKey].success++;
890
+ }
891
+ else {
892
+ metrics[apiKey].failures++;
893
+ if (error) {
894
+ metrics[apiKey].lastError = error;
895
+ }
896
+ }
897
+ }
898
+ // Save back to Redis (expires after 2 hours)
899
+ return [4 /*yield*/, redis.setex(REDIS_API_METRICS_KEY, 7200, JSON.stringify(metrics))];
900
+ case 3:
901
+ // Save back to Redis (expires after 2 hours)
902
+ _a.sent();
903
+ return [3 /*break*/, 5];
904
+ case 4:
905
+ error_2 = _a.sent();
906
+ log.debug(tag, "Error tracking API metric: ".concat(error_2));
907
+ return [3 /*break*/, 5];
908
+ case 5: return [2 /*return*/];
767
909
  }
768
- return [2 /*return*/];
769
910
  });
770
911
  });
771
912
  };
772
913
  /**
773
914
  * Get price from CoinGecko by coin ID
774
- * Now with rate limiting to prevent API spam
915
+ * Now with token bucket rate limiting and metrics logging
775
916
  */
776
917
  var get_price_from_coingecko = function (coingeckoId) {
777
918
  return __awaiter(this, void 0, void 0, function () {
778
- var tag, url, headers, response, price, error_1, status_3;
919
+ var tag, startTime, hasTokens, url, headers, response, responseTime, price, error_3, responseTime, status_3, wasRateLimited, discordNotifier, importError_1, alertError_1;
779
920
  var _a;
780
921
  return __generator(this, function (_b) {
781
922
  switch (_b.label) {
782
923
  case 0:
783
924
  tag = TAG + ' | get_price_from_coingecko | ';
784
- _b.label = 1;
925
+ startTime = Date.now();
926
+ if (!(tokenBucketManager && tokenBucketManager.isInitialized())) return [3 /*break*/, 4];
927
+ return [4 /*yield*/, tokenBucketManager.tryConsume('coingecko', 1)];
785
928
  case 1:
786
- _b.trys.push([1, 3, , 4]);
929
+ hasTokens = _b.sent();
930
+ if (!!hasTokens) return [3 /*break*/, 4];
931
+ log.warn(tag, '🚫 CoinGecko token bucket exhausted! Skipping API call.');
932
+ if (!metricsLogger) return [3 /*break*/, 3];
933
+ return [4 /*yield*/, metricsLogger.logAPICall({
934
+ apiName: 'coingecko',
935
+ endpoint: '/simple/price',
936
+ success: false,
937
+ wasThrottled: true,
938
+ error: 'Token bucket exhausted',
939
+ tokensUsed: 0,
940
+ timestamp: new Date()
941
+ })];
942
+ case 2:
943
+ _b.sent();
944
+ _b.label = 3;
945
+ case 3: return [2 /*return*/, 0]; // Don't call API if no tokens!
946
+ case 4:
947
+ _b.trys.push([4, 11, , 24]);
787
948
  url = "".concat(URL_COINGECKO, "simple/price?ids=").concat(coingeckoId, "&vs_currencies=usd");
788
949
  log.debug(tag, "Fetching from CoinGecko: ".concat(url));
789
950
  headers = {};
@@ -791,26 +952,101 @@ var get_price_from_coingecko = function (coingeckoId) {
791
952
  headers['x-cg-pro-api-key'] = COINGECKO_API_KEY;
792
953
  }
793
954
  return [4 /*yield*/, axios.get(url, { headers: headers })];
794
- case 2:
955
+ case 5:
795
956
  response = _b.sent();
796
- if (response.data && response.data[coingeckoId] && response.data[coingeckoId].usd) {
797
- price = parseFloat(response.data[coingeckoId].usd);
798
- log.debug(tag, "\u2705 CoinGecko price for ".concat(coingeckoId, ": $").concat(price));
799
- return [2 /*return*/, price];
800
- }
957
+ responseTime = Date.now() - startTime;
958
+ if (!(response.data && response.data[coingeckoId] && response.data[coingeckoId].usd)) return [3 /*break*/, 8];
959
+ price = parseFloat(response.data[coingeckoId].usd);
960
+ log.debug(tag, "\u2705 CoinGecko price for ".concat(coingeckoId, ": $").concat(price));
961
+ if (!metricsLogger) return [3 /*break*/, 7];
962
+ return [4 /*yield*/, metricsLogger.logAPICall({
963
+ apiName: 'coingecko',
964
+ endpoint: '/simple/price',
965
+ success: true,
966
+ responseTime: responseTime,
967
+ price: price,
968
+ tokensUsed: 1,
969
+ costUSD: 0.02,
970
+ timestamp: new Date()
971
+ })];
972
+ case 6:
973
+ _b.sent();
974
+ _b.label = 7;
975
+ case 7: return [2 /*return*/, price];
976
+ case 8:
801
977
  log.debug(tag, "No price data from CoinGecko for ".concat(coingeckoId));
802
- return [2 /*return*/, 0];
803
- case 3:
804
- error_1 = _b.sent();
805
- status_3 = ((_a = error_1.response) === null || _a === void 0 ? void 0 : _a.status) || error_1.status;
806
- if (status_3 === 429 || status_3 === 403) {
807
- log.warn(tag, "CoinGecko rate limit (".concat(status_3, ") for ").concat(coingeckoId));
808
- }
809
- else {
810
- log.debug(tag, "CoinGecko error for ".concat(coingeckoId, ": ").concat(error_1.message));
811
- }
812
- return [2 /*return*/, 0];
813
- case 4: return [2 /*return*/];
978
+ if (!metricsLogger) return [3 /*break*/, 10];
979
+ return [4 /*yield*/, metricsLogger.logAPICall({
980
+ apiName: 'coingecko',
981
+ endpoint: '/simple/price',
982
+ success: false,
983
+ responseTime: responseTime,
984
+ error: 'No price data',
985
+ tokensUsed: 1,
986
+ costUSD: 0.02,
987
+ timestamp: new Date()
988
+ })];
989
+ case 9:
990
+ _b.sent();
991
+ _b.label = 10;
992
+ case 10: return [2 /*return*/, 0];
993
+ case 11:
994
+ error_3 = _b.sent();
995
+ responseTime = Date.now() - startTime;
996
+ status_3 = ((_a = error_3.response) === null || _a === void 0 ? void 0 : _a.status) || error_3.status;
997
+ wasRateLimited = status_3 === 429 || status_3 === 403;
998
+ if (!metricsLogger) return [3 /*break*/, 13];
999
+ return [4 /*yield*/, metricsLogger.logAPICall({
1000
+ apiName: 'coingecko',
1001
+ endpoint: '/simple/price',
1002
+ success: false,
1003
+ responseTime: responseTime,
1004
+ statusCode: status_3,
1005
+ error: error_3.message,
1006
+ wasRateLimited: wasRateLimited,
1007
+ tokensUsed: 1, // Token was consumed even on error!
1008
+ costUSD: 0.02,
1009
+ timestamp: new Date()
1010
+ })];
1011
+ case 12:
1012
+ _b.sent();
1013
+ _b.label = 13;
1014
+ case 13:
1015
+ if (!wasRateLimited) return [3 /*break*/, 22];
1016
+ log.warn(tag, "CoinGecko rate limit (".concat(status_3, ") for ").concat(coingeckoId));
1017
+ _b.label = 14;
1018
+ case 14:
1019
+ _b.trys.push([14, 20, , 21]);
1020
+ if (!(typeof require !== 'undefined')) return [3 /*break*/, 19];
1021
+ _b.label = 15;
1022
+ case 15:
1023
+ _b.trys.push([15, 18, , 19]);
1024
+ discordNotifier = require('@pioneer-platform/pioneer-server/services/discord-notifier.service').discordNotifier;
1025
+ if (!(discordNotifier && discordNotifier.isEnabled())) return [3 /*break*/, 17];
1026
+ return [4 /*yield*/, discordNotifier.sendRateLimitAlert('CoinGecko', status_3, {
1027
+ endpoint: '/simple/price',
1028
+ asset: coingeckoId
1029
+ })];
1030
+ case 16:
1031
+ _b.sent();
1032
+ _b.label = 17;
1033
+ case 17: return [3 /*break*/, 19];
1034
+ case 18:
1035
+ importError_1 = _b.sent();
1036
+ // Server context not available - running standalone, skip alert
1037
+ log.debug(tag, 'Discord notifier not available (standalone mode)');
1038
+ return [3 /*break*/, 19];
1039
+ case 19: return [3 /*break*/, 21];
1040
+ case 20:
1041
+ alertError_1 = _b.sent();
1042
+ log.debug(tag, 'Failed to send rate limit alert:', alertError_1);
1043
+ return [3 /*break*/, 21];
1044
+ case 21: return [3 /*break*/, 23];
1045
+ case 22:
1046
+ log.debug(tag, "CoinGecko error for ".concat(coingeckoId, ": ").concat(error_3.message));
1047
+ _b.label = 23;
1048
+ case 23: return [2 /*return*/, 0];
1049
+ case 24: return [2 /*return*/];
814
1050
  }
815
1051
  });
816
1052
  });
@@ -822,7 +1058,7 @@ var get_price_from_coingecko = function (coingeckoId) {
822
1058
  */
823
1059
  var get_price_from_coingecko_contract = function (contractAddress_1) {
824
1060
  return __awaiter(this, arguments, void 0, function (contractAddress, chainId) {
825
- var tag, url, headers, response, contractKey, price, error_2, status_4;
1061
+ var tag, url, headers, response, contractKey, price, error_4, status_4;
826
1062
  var _a;
827
1063
  if (chainId === void 0) { chainId = 'ethereum'; }
828
1064
  return __generator(this, function (_b) {
@@ -850,13 +1086,13 @@ var get_price_from_coingecko_contract = function (contractAddress_1) {
850
1086
  log.debug(tag, "No price data from CoinGecko for contract ".concat(contractAddress));
851
1087
  return [2 /*return*/, 0];
852
1088
  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;
1089
+ error_4 = _b.sent();
1090
+ status_4 = ((_a = error_4.response) === null || _a === void 0 ? void 0 : _a.status) || error_4.status;
855
1091
  if (status_4 === 429 || status_4 === 403) {
856
1092
  log.warn(tag, "CoinGecko rate limit (".concat(status_4, ") for contract ").concat(contractAddress));
857
1093
  }
858
1094
  else {
859
- log.debug(tag, "CoinGecko contract error for ".concat(contractAddress, ": ").concat(error_2.message));
1095
+ log.debug(tag, "CoinGecko contract error for ".concat(contractAddress, ": ").concat(error_4.message));
860
1096
  }
861
1097
  return [2 /*return*/, 0];
862
1098
  case 4: return [2 /*return*/];
@@ -866,23 +1102,43 @@ var get_price_from_coingecko_contract = function (contractAddress_1) {
866
1102
  };
867
1103
  /**
868
1104
  * Get price from CoinMarketCap by symbol
869
- * Now with rate limiting to prevent API spam
1105
+ * Now with token bucket rate limiting and metrics logging
870
1106
  */
871
1107
  var get_price_from_coinmarketcap = function (symbol) {
872
1108
  return __awaiter(this, void 0, void 0, function () {
873
- var tag, url, response, coinData, priceData, price, error_3, status_5;
1109
+ var tag, startTime, hasTokens, url, response, responseTime, coinData, priceData, price, error_5, responseTime, status_5, wasRateLimited, discordNotifier, importError_2, alertError_2;
874
1110
  var _a, _b, _c;
875
1111
  return __generator(this, function (_d) {
876
1112
  switch (_d.label) {
877
1113
  case 0:
878
1114
  tag = TAG + ' | get_price_from_coinmarketcap | ';
1115
+ startTime = Date.now();
879
1116
  if (!CMC_PRO_API_KEY) {
880
1117
  log.debug(tag, 'CMC_PRO_API_KEY not set, skipping CoinMarketCap');
881
1118
  return [2 /*return*/, 0];
882
1119
  }
883
- _d.label = 1;
1120
+ if (!(tokenBucketManager && tokenBucketManager.isInitialized())) return [3 /*break*/, 4];
1121
+ return [4 /*yield*/, tokenBucketManager.tryConsume('coinmarketcap', 1)];
884
1122
  case 1:
885
- _d.trys.push([1, 3, , 4]);
1123
+ hasTokens = _d.sent();
1124
+ if (!!hasTokens) return [3 /*break*/, 4];
1125
+ log.warn(tag, '🚫 CoinMarketCap token bucket exhausted! Skipping API call.');
1126
+ if (!metricsLogger) return [3 /*break*/, 3];
1127
+ return [4 /*yield*/, metricsLogger.logAPICall({
1128
+ apiName: 'coinmarketcap',
1129
+ endpoint: '/cryptocurrency/quotes/latest',
1130
+ success: false,
1131
+ wasThrottled: true,
1132
+ error: 'Token bucket exhausted',
1133
+ tokensUsed: 0,
1134
+ timestamp: new Date()
1135
+ })];
1136
+ case 2:
1137
+ _d.sent();
1138
+ _d.label = 3;
1139
+ case 3: return [2 /*return*/, 0];
1140
+ case 4:
1141
+ _d.trys.push([4, 14, , 27]);
886
1142
  url = "".concat(URL_COINMARKETCAP, "cryptocurrency/quotes/latest?symbol=").concat(symbol, "&convert=USD");
887
1143
  log.debug(tag, "Fetching from CoinMarketCap: ".concat(url));
888
1144
  return [4 /*yield*/, axios.get(url, {
@@ -891,56 +1147,164 @@ var get_price_from_coinmarketcap = function (symbol) {
891
1147
  'Accept': 'application/json'
892
1148
  }
893
1149
  })];
894
- case 2:
1150
+ case 5:
895
1151
  response = _d.sent();
896
- if (response.data && response.data.data && response.data.data[symbol]) {
897
- coinData = response.data.data[symbol];
898
- priceData = (_b = (_a = coinData.quote) === null || _a === void 0 ? void 0 : _a.USD) === null || _b === void 0 ? void 0 : _b.price;
899
- if (!priceData || isNaN(parseFloat(priceData))) {
900
- log.warn(tag, "\u26A0\uFE0F CoinMarketCap returned invalid price for ".concat(symbol));
901
- log.warn(tag, "Symbol: ".concat(coinData.symbol, ", Slug: ").concat(coinData.slug, ", Quote keys:"), Object.keys(coinData.quote || {}));
902
- return [2 /*return*/, 0];
903
- }
904
- price = parseFloat(priceData);
905
- log.debug(tag, "\u2705 CoinMarketCap price for ".concat(symbol, ": $").concat(price));
906
- return [2 /*return*/, price];
907
- }
1152
+ responseTime = Date.now() - startTime;
1153
+ if (!(response.data && response.data.data && response.data.data[symbol])) return [3 /*break*/, 11];
1154
+ coinData = response.data.data[symbol];
1155
+ priceData = (_b = (_a = coinData.quote) === null || _a === void 0 ? void 0 : _a.USD) === null || _b === void 0 ? void 0 : _b.price;
1156
+ if (!(!priceData || isNaN(parseFloat(priceData)))) return [3 /*break*/, 8];
1157
+ log.warn(tag, "\u26A0\uFE0F CoinMarketCap returned invalid price for ".concat(symbol));
1158
+ if (!metricsLogger) return [3 /*break*/, 7];
1159
+ return [4 /*yield*/, metricsLogger.logAPICall({
1160
+ apiName: 'coinmarketcap',
1161
+ endpoint: '/cryptocurrency/quotes/latest',
1162
+ success: false,
1163
+ responseTime: responseTime,
1164
+ error: 'Invalid price data',
1165
+ tokensUsed: 1,
1166
+ costUSD: 0.015,
1167
+ timestamp: new Date()
1168
+ })];
1169
+ case 6:
1170
+ _d.sent();
1171
+ _d.label = 7;
1172
+ case 7: return [2 /*return*/, 0];
1173
+ case 8:
1174
+ price = parseFloat(priceData);
1175
+ log.debug(tag, "\u2705 CoinMarketCap price for ".concat(symbol, ": $").concat(price));
1176
+ if (!metricsLogger) return [3 /*break*/, 10];
1177
+ return [4 /*yield*/, metricsLogger.logAPICall({
1178
+ apiName: 'coinmarketcap',
1179
+ endpoint: '/cryptocurrency/quotes/latest',
1180
+ success: true,
1181
+ responseTime: responseTime,
1182
+ price: price,
1183
+ tokensUsed: 1,
1184
+ costUSD: 0.015,
1185
+ timestamp: new Date()
1186
+ })];
1187
+ case 9:
1188
+ _d.sent();
1189
+ _d.label = 10;
1190
+ case 10: return [2 /*return*/, price];
1191
+ case 11:
908
1192
  log.debug(tag, "No price data from CoinMarketCap for ".concat(symbol));
909
- return [2 /*return*/, 0];
910
- case 3:
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));
915
- }
916
- else {
917
- log.debug(tag, "CoinMarketCap error for ".concat(symbol, ": ").concat(error_3.message));
918
- }
919
- return [2 /*return*/, 0];
920
- case 4: return [2 /*return*/];
1193
+ if (!metricsLogger) return [3 /*break*/, 13];
1194
+ return [4 /*yield*/, metricsLogger.logAPICall({
1195
+ apiName: 'coinmarketcap',
1196
+ endpoint: '/cryptocurrency/quotes/latest',
1197
+ success: false,
1198
+ responseTime: responseTime,
1199
+ error: 'No price data',
1200
+ tokensUsed: 1,
1201
+ costUSD: 0.015,
1202
+ timestamp: new Date()
1203
+ })];
1204
+ case 12:
1205
+ _d.sent();
1206
+ _d.label = 13;
1207
+ case 13: return [2 /*return*/, 0];
1208
+ case 14:
1209
+ error_5 = _d.sent();
1210
+ responseTime = Date.now() - startTime;
1211
+ status_5 = ((_c = error_5.response) === null || _c === void 0 ? void 0 : _c.status) || error_5.status;
1212
+ wasRateLimited = status_5 === 429 || status_5 === 403;
1213
+ if (!metricsLogger) return [3 /*break*/, 16];
1214
+ return [4 /*yield*/, metricsLogger.logAPICall({
1215
+ apiName: 'coinmarketcap',
1216
+ endpoint: '/cryptocurrency/quotes/latest',
1217
+ success: false,
1218
+ responseTime: responseTime,
1219
+ statusCode: status_5,
1220
+ error: error_5.message,
1221
+ wasRateLimited: wasRateLimited,
1222
+ tokensUsed: 1,
1223
+ costUSD: 0.015,
1224
+ timestamp: new Date()
1225
+ })];
1226
+ case 15:
1227
+ _d.sent();
1228
+ _d.label = 16;
1229
+ case 16:
1230
+ if (!wasRateLimited) return [3 /*break*/, 25];
1231
+ log.warn(tag, "CoinMarketCap rate limit (".concat(status_5, ") for ").concat(symbol));
1232
+ _d.label = 17;
1233
+ case 17:
1234
+ _d.trys.push([17, 23, , 24]);
1235
+ if (!(typeof require !== 'undefined')) return [3 /*break*/, 22];
1236
+ _d.label = 18;
1237
+ case 18:
1238
+ _d.trys.push([18, 21, , 22]);
1239
+ discordNotifier = require('@pioneer-platform/pioneer-server/services/discord-notifier.service').discordNotifier;
1240
+ if (!(discordNotifier && discordNotifier.isEnabled())) return [3 /*break*/, 20];
1241
+ return [4 /*yield*/, discordNotifier.sendRateLimitAlert('CoinMarketCap', status_5, {
1242
+ endpoint: '/cryptocurrency/quotes/latest',
1243
+ symbol: symbol
1244
+ })];
1245
+ case 19:
1246
+ _d.sent();
1247
+ _d.label = 20;
1248
+ case 20: return [3 /*break*/, 22];
1249
+ case 21:
1250
+ importError_2 = _d.sent();
1251
+ // Server context not available - running standalone, skip alert
1252
+ log.debug(tag, 'Discord notifier not available (standalone mode)');
1253
+ return [3 /*break*/, 22];
1254
+ case 22: return [3 /*break*/, 24];
1255
+ case 23:
1256
+ alertError_2 = _d.sent();
1257
+ log.debug(tag, 'Failed to send rate limit alert:', alertError_2);
1258
+ return [3 /*break*/, 24];
1259
+ case 24: return [3 /*break*/, 26];
1260
+ case 25:
1261
+ log.debug(tag, "CoinMarketCap error for ".concat(symbol, ": ").concat(error_5.message));
1262
+ _d.label = 26;
1263
+ case 26: return [2 /*return*/, 0];
1264
+ case 27: return [2 /*return*/];
921
1265
  }
922
1266
  });
923
1267
  });
924
1268
  };
925
1269
  /**
926
1270
  * Get price from CoinCap by symbol
927
- * Now with rate limiting to prevent API spam
1271
+ * Now with token bucket rate limiting and metrics logging
928
1272
  */
929
1273
  var get_price_from_coincap = function (symbol) {
930
1274
  return __awaiter(this, void 0, void 0, function () {
931
- var tag, url, response, price, error_4, status_6;
1275
+ var tag, startTime, hasTokens, url, response, responseTime, price, error_6, responseTime, status_6, wasRateLimited, discordNotifier, importError_3, alertError_3;
932
1276
  var _a;
933
1277
  return __generator(this, function (_b) {
934
1278
  switch (_b.label) {
935
1279
  case 0:
936
1280
  tag = TAG + ' | get_price_from_coincap | ';
1281
+ startTime = Date.now();
937
1282
  if (!COINCAP_API_KEY) {
938
1283
  log.debug(tag, 'COINCAP_API_KEY not set, skipping CoinCap');
939
1284
  return [2 /*return*/, 0];
940
1285
  }
941
- _b.label = 1;
1286
+ if (!(tokenBucketManager && tokenBucketManager.isInitialized())) return [3 /*break*/, 4];
1287
+ return [4 /*yield*/, tokenBucketManager.tryConsume('coincap', 1)];
942
1288
  case 1:
943
- _b.trys.push([1, 3, , 4]);
1289
+ hasTokens = _b.sent();
1290
+ if (!!hasTokens) return [3 /*break*/, 4];
1291
+ log.warn(tag, '🚫 CoinCap token bucket exhausted! Skipping API call.');
1292
+ if (!metricsLogger) return [3 /*break*/, 3];
1293
+ return [4 /*yield*/, metricsLogger.logAPICall({
1294
+ apiName: 'coincap',
1295
+ endpoint: '/assets',
1296
+ success: false,
1297
+ wasThrottled: true,
1298
+ error: 'Token bucket exhausted',
1299
+ tokensUsed: 0,
1300
+ timestamp: new Date()
1301
+ })];
1302
+ case 2:
1303
+ _b.sent();
1304
+ _b.label = 3;
1305
+ case 3: return [2 /*return*/, 0];
1306
+ case 4:
1307
+ _b.trys.push([4, 11, , 24]);
944
1308
  url = "".concat(URL_COINCAP, "assets/").concat(symbol);
945
1309
  log.debug(tag, "Fetching from CoinCap: ".concat(url));
946
1310
  return [4 /*yield*/, axios.get(url, {
@@ -949,26 +1313,101 @@ var get_price_from_coincap = function (symbol) {
949
1313
  'Authorization': "Bearer ".concat(COINCAP_API_KEY)
950
1314
  }
951
1315
  })];
952
- case 2:
1316
+ case 5:
953
1317
  response = _b.sent();
954
- if (response.data && response.data.data && response.data.data.priceUsd) {
955
- price = parseFloat(response.data.data.priceUsd);
956
- log.debug(tag, "\u2705 CoinCap price for ".concat(symbol, ": $").concat(price));
957
- return [2 /*return*/, price];
958
- }
1318
+ responseTime = Date.now() - startTime;
1319
+ if (!(response.data && response.data.data && response.data.data.priceUsd)) return [3 /*break*/, 8];
1320
+ price = parseFloat(response.data.data.priceUsd);
1321
+ log.debug(tag, "\u2705 CoinCap price for ".concat(symbol, ": $").concat(price));
1322
+ if (!metricsLogger) return [3 /*break*/, 7];
1323
+ return [4 /*yield*/, metricsLogger.logAPICall({
1324
+ apiName: 'coincap',
1325
+ endpoint: '/assets',
1326
+ success: true,
1327
+ responseTime: responseTime,
1328
+ price: price,
1329
+ tokensUsed: 1,
1330
+ costUSD: 0.01,
1331
+ timestamp: new Date()
1332
+ })];
1333
+ case 6:
1334
+ _b.sent();
1335
+ _b.label = 7;
1336
+ case 7: return [2 /*return*/, price];
1337
+ case 8:
959
1338
  log.debug(tag, "No price data from CoinCap for ".concat(symbol));
960
- return [2 /*return*/, 0];
961
- case 3:
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));
966
- }
967
- else {
968
- log.debug(tag, "CoinCap error for ".concat(symbol, ": ").concat(error_4.message));
969
- }
970
- return [2 /*return*/, 0];
971
- case 4: return [2 /*return*/];
1339
+ if (!metricsLogger) return [3 /*break*/, 10];
1340
+ return [4 /*yield*/, metricsLogger.logAPICall({
1341
+ apiName: 'coincap',
1342
+ endpoint: '/assets',
1343
+ success: false,
1344
+ responseTime: responseTime,
1345
+ error: 'No price data',
1346
+ tokensUsed: 1,
1347
+ costUSD: 0.01,
1348
+ timestamp: new Date()
1349
+ })];
1350
+ case 9:
1351
+ _b.sent();
1352
+ _b.label = 10;
1353
+ case 10: return [2 /*return*/, 0];
1354
+ case 11:
1355
+ error_6 = _b.sent();
1356
+ responseTime = Date.now() - startTime;
1357
+ status_6 = ((_a = error_6.response) === null || _a === void 0 ? void 0 : _a.status) || error_6.status;
1358
+ wasRateLimited = status_6 === 429 || status_6 === 403;
1359
+ if (!metricsLogger) return [3 /*break*/, 13];
1360
+ return [4 /*yield*/, metricsLogger.logAPICall({
1361
+ apiName: 'coincap',
1362
+ endpoint: '/assets',
1363
+ success: false,
1364
+ responseTime: responseTime,
1365
+ statusCode: status_6,
1366
+ error: error_6.message,
1367
+ wasRateLimited: wasRateLimited,
1368
+ tokensUsed: 1,
1369
+ costUSD: 0.01,
1370
+ timestamp: new Date()
1371
+ })];
1372
+ case 12:
1373
+ _b.sent();
1374
+ _b.label = 13;
1375
+ case 13:
1376
+ if (!wasRateLimited) return [3 /*break*/, 22];
1377
+ log.warn(tag, "CoinCap rate limit (".concat(status_6, ") for ").concat(symbol));
1378
+ _b.label = 14;
1379
+ case 14:
1380
+ _b.trys.push([14, 20, , 21]);
1381
+ if (!(typeof require !== 'undefined')) return [3 /*break*/, 19];
1382
+ _b.label = 15;
1383
+ case 15:
1384
+ _b.trys.push([15, 18, , 19]);
1385
+ discordNotifier = require('@pioneer-platform/pioneer-server/services/discord-notifier.service').discordNotifier;
1386
+ if (!(discordNotifier && discordNotifier.isEnabled())) return [3 /*break*/, 17];
1387
+ return [4 /*yield*/, discordNotifier.sendRateLimitAlert('CoinCap', status_6, {
1388
+ endpoint: '/assets',
1389
+ symbol: symbol
1390
+ })];
1391
+ case 16:
1392
+ _b.sent();
1393
+ _b.label = 17;
1394
+ case 17: return [3 /*break*/, 19];
1395
+ case 18:
1396
+ importError_3 = _b.sent();
1397
+ // Server context not available - running standalone, skip alert
1398
+ log.debug(tag, 'Discord notifier not available (standalone mode)');
1399
+ return [3 /*break*/, 19];
1400
+ case 19: return [3 /*break*/, 21];
1401
+ case 20:
1402
+ alertError_3 = _b.sent();
1403
+ log.debug(tag, 'Failed to send rate limit alert:', alertError_3);
1404
+ return [3 /*break*/, 21];
1405
+ case 21: return [3 /*break*/, 23];
1406
+ case 22:
1407
+ log.debug(tag, "CoinCap error for ".concat(symbol, ": ").concat(error_6.message));
1408
+ _b.label = 23;
1409
+ case 23: return [2 /*return*/, 0];
1410
+ case 24: return [2 /*return*/];
972
1411
  }
973
1412
  });
974
1413
  });
@@ -980,7 +1419,7 @@ var get_price_from_coincap = function (symbol) {
980
1419
  */
981
1420
  var get_price_from_mayascan = function () {
982
1421
  return __awaiter(this, arguments, void 0, function (asset) {
983
- var tag, url, response, price, error_5;
1422
+ var tag, url, response, price, error_7;
984
1423
  if (asset === void 0) { asset = 'maya'; }
985
1424
  return __generator(this, function (_a) {
986
1425
  switch (_a.label) {
@@ -1010,8 +1449,8 @@ var get_price_from_mayascan = function () {
1010
1449
  log.debug(tag, "No ".concat(asset.toUpperCase(), " price data from MayaScan"));
1011
1450
  return [2 /*return*/, 0];
1012
1451
  case 3:
1013
- error_5 = _a.sent();
1014
- log.debug(tag, "MayaScan error: ".concat(error_5.message));
1452
+ error_7 = _a.sent();
1453
+ log.debug(tag, "MayaScan error: ".concat(error_7.message));
1015
1454
  return [2 /*return*/, 0];
1016
1455
  case 4: return [2 /*return*/];
1017
1456
  }
@@ -1052,7 +1491,8 @@ var get_asset_price_by_caip = function (caip_2) {
1052
1491
  */
1053
1492
  var get_asset_price_by_caip_internal = function (caip_2) {
1054
1493
  return __awaiter(this, arguments, void 0, function (caip, returnSource) {
1055
- var tag, mayaPrice, isCacao, identifiers, price, contractAddress, chainIdMatch, chainId, chainMap, platformName, asset, symbol, cacaoPrice;
1494
+ var tag, mayaPrice, isCacao, identifiers, price, apiStrategies, shuffledStrategies, _i, shuffledStrategies_1, strategy, error_8, contractAddress, chainIdMatch, chainId, chainMap, platformName, asset, symbol, cacaoPrice, ccxtPrice, ccxtError_1;
1495
+ var _this = this;
1056
1496
  if (returnSource === void 0) { returnSource = false; }
1057
1497
  return __generator(this, function (_a) {
1058
1498
  switch (_a.label) {
@@ -1078,26 +1518,138 @@ var get_asset_price_by_caip_internal = function (caip_2) {
1078
1518
  isCacao = caip === 'cosmos:mayachain-mainnet-v1/slip44:931';
1079
1519
  identifiers = caip_to_identifiers(caip);
1080
1520
  price = 0;
1081
- if (!identifiers) return [3 /*break*/, 4];
1521
+ if (!identifiers) return [3 /*break*/, 14];
1082
1522
  log.debug(tag, "Identifiers for ".concat(caip, ":"), identifiers);
1083
- // Try CoinGecko with mapped ID
1084
- console.log("\uD83E\uDD8E Trying CoinGecko with mapped ID: ".concat(identifiers.coingeckoId));
1085
- return [4 /*yield*/, get_price_from_coingecko(identifiers.coingeckoId)];
1523
+ apiStrategies = [
1524
+ {
1525
+ name: 'CoinGecko',
1526
+ icon: '🦎',
1527
+ fetch: function () { return __awaiter(_this, void 0, void 0, function () {
1528
+ var p;
1529
+ return __generator(this, function (_a) {
1530
+ switch (_a.label) {
1531
+ case 0:
1532
+ console.log("\uD83E\uDD8E Trying CoinGecko with mapped ID: ".concat(identifiers.coingeckoId));
1533
+ return [4 /*yield*/, get_price_from_coingecko(identifiers.coingeckoId)];
1534
+ case 1:
1535
+ p = _a.sent();
1536
+ console.log("\uD83E\uDD8E CoinGecko returned price: $".concat(p));
1537
+ return [2 /*return*/, p];
1538
+ }
1539
+ });
1540
+ }); },
1541
+ source: 'coingecko'
1542
+ },
1543
+ {
1544
+ name: 'CoinMarketCap',
1545
+ icon: '📊',
1546
+ fetch: function () { return __awaiter(_this, void 0, void 0, function () {
1547
+ var p;
1548
+ return __generator(this, function (_a) {
1549
+ switch (_a.label) {
1550
+ case 0:
1551
+ console.log("\uD83D\uDCCA Trying CoinMarketCap for: ".concat(identifiers.cmcSymbol));
1552
+ return [4 /*yield*/, get_price_from_coinmarketcap(identifiers.cmcSymbol)];
1553
+ case 1:
1554
+ p = _a.sent();
1555
+ console.log("\uD83D\uDCCA CoinMarketCap returned price: $".concat(p));
1556
+ return [2 /*return*/, p];
1557
+ }
1558
+ });
1559
+ }); },
1560
+ source: 'coinmarketcap'
1561
+ },
1562
+ {
1563
+ name: 'CoinCap',
1564
+ icon: '💰',
1565
+ fetch: function () { return __awaiter(_this, void 0, void 0, function () {
1566
+ var p;
1567
+ return __generator(this, function (_a) {
1568
+ switch (_a.label) {
1569
+ case 0:
1570
+ console.log("\uD83D\uDCB0 Trying CoinCap for: ".concat(identifiers.coincapSymbol));
1571
+ return [4 /*yield*/, get_price_from_coincap(identifiers.coincapSymbol)];
1572
+ case 1:
1573
+ p = _a.sent();
1574
+ console.log("\uD83D\uDCB0 CoinCap returned price: $".concat(p));
1575
+ return [2 /*return*/, p];
1576
+ }
1577
+ });
1578
+ }); },
1579
+ source: 'coincap'
1580
+ },
1581
+ {
1582
+ name: 'CCXT',
1583
+ icon: '🔄',
1584
+ fetch: function () { return __awaiter(_this, void 0, void 0, function () {
1585
+ var p;
1586
+ return __generator(this, function (_a) {
1587
+ switch (_a.label) {
1588
+ case 0:
1589
+ console.log("\uD83D\uDD04 Trying CCXT for: ".concat(identifiers.symbol));
1590
+ return [4 /*yield*/, (0, ccxt_pricing_1.getPriceFromCCXT)(identifiers.symbol)];
1591
+ case 1:
1592
+ p = _a.sent();
1593
+ console.log("\uD83D\uDD04 CCXT returned price: $".concat(p));
1594
+ return [2 /*return*/, p];
1595
+ }
1596
+ });
1597
+ }); },
1598
+ source: 'ccxt'
1599
+ }
1600
+ ];
1601
+ shuffledStrategies = apiStrategies.sort(function () { return Math.random() - 0.5; });
1602
+ log.debug(tag, "Randomized API order: ".concat(shuffledStrategies.map(function (s) { return s.name; }).join(' → ')));
1603
+ _i = 0, shuffledStrategies_1 = shuffledStrategies;
1604
+ _a.label = 3;
1086
1605
  case 3:
1087
- price = _a.sent();
1088
- console.log("\uD83E\uDD8E CoinGecko returned price: $".concat(price));
1089
- if (price > 0) {
1090
- return [2 /*return*/, returnSource ? { price: price, source: 'coingecko' } : price];
1091
- }
1092
- return [3 /*break*/, 5];
1606
+ if (!(_i < shuffledStrategies_1.length)) return [3 /*break*/, 13];
1607
+ strategy = shuffledStrategies_1[_i];
1608
+ _a.label = 4;
1093
1609
  case 4:
1094
- log.debug(tag, "No identifier mapping found for CAIP: ".concat(caip));
1095
- _a.label = 5;
1610
+ _a.trys.push([4, 10, , 12]);
1611
+ return [4 /*yield*/, strategy.fetch()];
1096
1612
  case 5:
1097
- if (!caip.includes('/erc20:0x')) return [3 /*break*/, 13];
1613
+ price = _a.sent();
1614
+ if (!(price > 0)) return [3 /*break*/, 7];
1615
+ log.info(tag, "\u2705 Got price from ".concat(strategy.name, ": $").concat(price));
1616
+ // Track successful API call
1617
+ return [4 /*yield*/, trackAPIMetric(strategy.source, true)];
1618
+ case 6:
1619
+ // Track successful API call
1620
+ _a.sent();
1621
+ return [2 /*return*/, returnSource ? { price: price, source: strategy.source } : price];
1622
+ case 7:
1623
+ // Track failure (0 price returned)
1624
+ return [4 /*yield*/, trackAPIMetric(strategy.source, false, 'Returned 0 price')];
1625
+ case 8:
1626
+ // Track failure (0 price returned)
1627
+ _a.sent();
1628
+ _a.label = 9;
1629
+ case 9: return [3 /*break*/, 12];
1630
+ case 10:
1631
+ error_8 = _a.sent();
1632
+ log.debug(tag, "".concat(strategy.name, " failed:"), error_8);
1633
+ // Track failure with error message
1634
+ return [4 /*yield*/, trackAPIMetric(strategy.source, false, error_8.message)];
1635
+ case 11:
1636
+ // Track failure with error message
1637
+ _a.sent();
1638
+ return [3 /*break*/, 12];
1639
+ case 12:
1640
+ _i++;
1641
+ return [3 /*break*/, 3];
1642
+ case 13:
1643
+ log.debug(tag, "All APIs returned 0 or failed for ".concat(caip));
1644
+ return [3 /*break*/, 15];
1645
+ case 14:
1646
+ log.debug(tag, "No identifier mapping found for CAIP: ".concat(caip));
1647
+ _a.label = 15;
1648
+ case 15:
1649
+ if (!caip.includes('/erc20:0x')) return [3 /*break*/, 23];
1098
1650
  contractAddress = caip.split('/erc20:')[1];
1099
1651
  chainIdMatch = caip.match(/eip155:(\d+)/);
1100
- if (!(contractAddress && chainIdMatch)) return [3 /*break*/, 7];
1652
+ if (!(contractAddress && chainIdMatch)) return [3 /*break*/, 17];
1101
1653
  chainId = chainIdMatch[1];
1102
1654
  chainMap = {
1103
1655
  '1': 'ethereum',
@@ -1112,23 +1664,23 @@ var get_asset_price_by_caip_internal = function (caip_2) {
1112
1664
  platformName = chainMap[chainId] || 'ethereum';
1113
1665
  console.log("\uD83D\uDD04 Trying CoinGecko contract lookup: ".concat(contractAddress, " on ").concat(platformName));
1114
1666
  return [4 /*yield*/, get_price_from_coingecko_contract(contractAddress, platformName)];
1115
- case 6:
1667
+ case 16:
1116
1668
  price = _a.sent();
1117
1669
  console.log("\uD83D\uDD04 CoinGecko contract returned price: $".concat(price));
1118
1670
  if (price > 0) {
1119
1671
  return [2 /*return*/, returnSource ? { price: price, source: 'coingecko-contract' } : price];
1120
1672
  }
1121
- _a.label = 7;
1122
- case 7:
1123
- if (!(price === 0)) return [3 /*break*/, 13];
1673
+ _a.label = 17;
1674
+ case 17:
1675
+ if (!(price === 0)) return [3 /*break*/, 23];
1124
1676
  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];
1677
+ if (!(asset && asset.symbol)) return [3 /*break*/, 21];
1126
1678
  symbol = asset.symbol.toUpperCase();
1127
1679
  log.info(tag, "\uD83D\uDD04 Contract lookup failed, falling back to symbol-based APIs for: ".concat(symbol));
1128
1680
  // Try CoinMarketCap
1129
1681
  console.log("\uD83D\uDCCA Trying CoinMarketCap for ERC20 token: ".concat(symbol));
1130
1682
  return [4 /*yield*/, get_price_from_coinmarketcap(symbol)];
1131
- case 8:
1683
+ case 18:
1132
1684
  price = _a.sent();
1133
1685
  console.log("\uD83D\uDCCA CoinMarketCap returned price: $".concat(price));
1134
1686
  if (price > 0) {
@@ -1137,7 +1689,7 @@ var get_asset_price_by_caip_internal = function (caip_2) {
1137
1689
  // Try CoinCap
1138
1690
  console.log("\uD83D\uDCB0 Trying CoinCap for ERC20 token: ".concat(symbol.toLowerCase()));
1139
1691
  return [4 /*yield*/, get_price_from_coincap(symbol.toLowerCase())];
1140
- case 9:
1692
+ case 19:
1141
1693
  price = _a.sent();
1142
1694
  console.log("\uD83D\uDCB0 CoinCap returned price: $".concat(price));
1143
1695
  if (price > 0) {
@@ -1145,38 +1697,52 @@ var get_asset_price_by_caip_internal = function (caip_2) {
1145
1697
  }
1146
1698
  // All 3 paid APIs failed - mark as unpriceable (likely scam/spam token)
1147
1699
  log.warn(tag, "\u274C Token not found in any of 3 paid APIs (CoinGecko, CMC, CoinCap): ".concat(caip));
1700
+ // CRITICAL: Double-check this isn't a major crypto before marking unpriceable
1701
+ if (MAJOR_CRYPTO_WHITELIST.has(caip)) {
1702
+ log.error(tag, "\uD83D\uDEA8 CRITICAL: Major crypto ".concat(caip, " failed all API lookups!"));
1703
+ log.error(tag, "This indicates API outage or rate limiting - NOT marking as unpriceable");
1704
+ return [2 /*return*/, returnSource ? { price: 0, source: 'api_failure' } : 0];
1705
+ }
1148
1706
  log.warn(tag, "\uD83D\uDEA8 Marking as maybeScam - will not retry pricing for this token");
1149
1707
  // Cache the failure so we never try again
1150
1708
  return [4 /*yield*/, cache_unpriceable_token(caip)];
1151
- case 10:
1709
+ case 20:
1152
1710
  // Cache the failure so we never try again
1153
1711
  _a.sent();
1154
1712
  return [2 /*return*/, returnSource ? { price: 0, source: 'unpriceable', maybeScam: true } : 0];
1155
- case 11:
1713
+ case 21:
1156
1714
  // Token not in our discovery database - can't get symbol for API calls
1157
1715
  log.warn(tag, "\u274C Token not in discovery database, cannot lookup symbol: ".concat(caip));
1158
1716
  log.warn(tag, "\uD83D\uDEA8 Marking as maybeScam - likely user-added spam token");
1159
1717
  // Cache the failure so we never try again
1160
1718
  return [4 /*yield*/, cache_unpriceable_token(caip)];
1161
- case 12:
1719
+ case 22:
1162
1720
  // Cache the failure so we never try again
1163
1721
  _a.sent();
1164
1722
  return [2 /*return*/, returnSource ? { price: 0, source: 'unpriceable', maybeScam: true } : 0];
1165
- case 13:
1166
- if (!!identifiers) return [3 /*break*/, 15];
1723
+ case 23:
1724
+ if (!!identifiers) return [3 /*break*/, 25];
1167
1725
  log.warn(tag, "No way to lookup price for CAIP: ".concat(caip, " (no mapping, not ERC20, or all lookups failed)"));
1726
+ // CRITICAL: Never mark major cryptocurrencies as unpriceable!
1727
+ // This is a second layer of protection in case whitelist check fails
1728
+ if (MAJOR_CRYPTO_WHITELIST.has(caip)) {
1729
+ log.error(tag, "\uD83D\uDEA8 MAJOR CRYPTO WITHOUT IDENTIFIERS: ".concat(caip));
1730
+ log.error(tag, "This should NEVER happen - check coingeckoMapping and assetData");
1731
+ log.error(tag, "Returning 0 but NOT caching as unpriceable");
1732
+ return [2 /*return*/, returnSource ? { price: 0, source: 'mapping_error' } : 0];
1733
+ }
1168
1734
  log.warn(tag, "\uD83D\uDEA8 Marking as maybeScam - token has no price source");
1169
1735
  // Cache the failure so we never try again
1170
1736
  return [4 /*yield*/, cache_unpriceable_token(caip)];
1171
- case 14:
1737
+ case 24:
1172
1738
  // Cache the failure so we never try again
1173
1739
  _a.sent();
1174
1740
  return [2 /*return*/, returnSource ? { price: 0, source: 'unpriceable', maybeScam: true } : 0];
1175
- case 15:
1741
+ case 25:
1176
1742
  // STEP 4: Try CoinMarketCap by symbol (requires API key)
1177
1743
  console.log("\uD83D\uDCCA Trying CoinMarketCap for: ".concat(identifiers.cmcSymbol));
1178
1744
  return [4 /*yield*/, get_price_from_coinmarketcap(identifiers.cmcSymbol)];
1179
- case 16:
1745
+ case 26:
1180
1746
  price = _a.sent();
1181
1747
  console.log("\uD83D\uDCCA CoinMarketCap returned price: $".concat(price));
1182
1748
  if (price > 0) {
@@ -1185,23 +1751,41 @@ var get_asset_price_by_caip_internal = function (caip_2) {
1185
1751
  // STEP 5: Try CoinCap by symbol (last resort)
1186
1752
  console.log("\uD83D\uDCB0 Trying CoinCap for: ".concat(identifiers.coincapSymbol));
1187
1753
  return [4 /*yield*/, get_price_from_coincap(identifiers.coincapSymbol)];
1188
- case 17:
1754
+ case 27:
1189
1755
  price = _a.sent();
1190
1756
  console.log("\uD83D\uDCB0 CoinCap returned price: $".concat(price));
1191
1757
  if (price > 0) {
1192
1758
  return [2 /*return*/, returnSource ? { price: price, source: 'coincap' } : price];
1193
1759
  }
1194
- if (!isCacao) return [3 /*break*/, 19];
1760
+ if (!isCacao) return [3 /*break*/, 29];
1195
1761
  log.debug(tag, '🏔️ All pricing APIs failed for CACAO, trying MayaScan as final fallback');
1196
1762
  return [4 /*yield*/, get_price_from_mayascan('cacao')];
1197
- case 18:
1763
+ case 28:
1198
1764
  cacaoPrice = _a.sent();
1199
1765
  if (cacaoPrice > 0) {
1200
1766
  return [2 /*return*/, returnSource ? { price: cacaoPrice, source: 'mayascan' } : cacaoPrice];
1201
1767
  }
1202
- _a.label = 19;
1203
- case 19:
1204
- log.warn(tag, "\u274C No price found from any API for: ".concat(caip));
1768
+ _a.label = 29;
1769
+ case 29:
1770
+ if (!(identifiers && identifiers.symbol)) return [3 /*break*/, 33];
1771
+ log.debug(tag, "\uD83D\uDD04 Trying CCXT as final fallback for: ".concat(identifiers.symbol));
1772
+ _a.label = 30;
1773
+ case 30:
1774
+ _a.trys.push([30, 32, , 33]);
1775
+ return [4 /*yield*/, (0, ccxt_pricing_1.getPriceFromCCXT)(identifiers.symbol)];
1776
+ case 31:
1777
+ ccxtPrice = _a.sent();
1778
+ if (ccxtPrice > 0) {
1779
+ log.info(tag, "\u2705 Got price from CCXT for ".concat(caip, ": $").concat(ccxtPrice));
1780
+ return [2 /*return*/, returnSource ? { price: ccxtPrice, source: 'ccxt' } : ccxtPrice];
1781
+ }
1782
+ return [3 /*break*/, 33];
1783
+ case 32:
1784
+ ccxtError_1 = _a.sent();
1785
+ log.debug(tag, "CCXT lookup failed for ".concat(identifiers.symbol, ":"), ccxtError_1);
1786
+ return [3 /*break*/, 33];
1787
+ case 33:
1788
+ log.warn(tag, "\u274C No price found from any API (including CCXT) for: ".concat(caip));
1205
1789
  return [2 /*return*/, returnSource ? { price: 0, source: 'none' } : 0];
1206
1790
  }
1207
1791
  });
@@ -1346,7 +1930,7 @@ var get_price = function (asset) {
1346
1930
  */
1347
1931
  var get_batch_prices_by_caip = function (caips_1) {
1348
1932
  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;
1933
+ var tag, results, caipToIdMap, specialCaips, _i, caips_2, caip, identifiers, coingeckoIds, batchSize, i, batch, idsParam, url, response, _a, batch_1, coingeckoId, mapping, price, error_9, _b, specialCaips_1, caip, price, error_10, missingCaips, _c, missingCaips_1, caip, _d, _e, error_11;
1350
1934
  var _f;
1351
1935
  if (returnSource === void 0) { returnSource = false; }
1352
1936
  return __generator(this, function (_g) {
@@ -1413,8 +1997,8 @@ var get_batch_prices_by_caip = function (caips_1) {
1413
1997
  log.info(tag, "\u2705 Batch fetched ".concat(Object.keys(results).length, " prices from CoinGecko"));
1414
1998
  return [3 /*break*/, 8];
1415
1999
  case 7:
1416
- error_6 = _g.sent();
1417
- log.error(tag, "CoinGecko batch fetch failed:", error_6.message);
2000
+ error_9 = _g.sent();
2001
+ log.error(tag, "CoinGecko batch fetch failed:", error_9.message);
1418
2002
  return [3 /*break*/, 8];
1419
2003
  case 8:
1420
2004
  _b = 0, specialCaips_1 = specialCaips;
@@ -1431,7 +2015,7 @@ var get_batch_prices_by_caip = function (caips_1) {
1431
2015
  results[caip] = price;
1432
2016
  return [3 /*break*/, 13];
1433
2017
  case 12:
1434
- error_7 = _g.sent();
2018
+ error_10 = _g.sent();
1435
2019
  log.warn(tag, "Failed to fetch special case: ".concat(caip));
1436
2020
  return [3 /*break*/, 13];
1437
2021
  case 13:
@@ -1456,7 +2040,7 @@ var get_batch_prices_by_caip = function (caips_1) {
1456
2040
  _d[_e] = _g.sent();
1457
2041
  return [3 /*break*/, 19];
1458
2042
  case 18:
1459
- error_8 = _g.sent();
2043
+ error_11 = _g.sent();
1460
2044
  log.warn(tag, "Failed to fetch fallback price for ".concat(caip));
1461
2045
  results[caip] = returnSource ? { price: 0, source: 'none' } : 0;
1462
2046
  return [3 /*break*/, 19];
@@ -1470,3 +2054,58 @@ var get_batch_prices_by_caip = function (caips_1) {
1470
2054
  });
1471
2055
  });
1472
2056
  };
2057
+ /**
2058
+ * Initialize token buckets and metrics logging
2059
+ * Call this on module load
2060
+ */
2061
+ function initializeTokenBuckets() {
2062
+ return __awaiter(this, void 0, void 0, function () {
2063
+ var tag, mongo, db, error_12;
2064
+ return __generator(this, function (_a) {
2065
+ switch (_a.label) {
2066
+ case 0:
2067
+ tag = TAG + ' | initializeTokenBuckets | ';
2068
+ // Prevent double initialization
2069
+ if (isInitializing || tokenBucketManager) {
2070
+ log.debug(tag, 'Token buckets already initialized or initializing');
2071
+ return [2 /*return*/];
2072
+ }
2073
+ isInitializing = true;
2074
+ _a.label = 1;
2075
+ case 1:
2076
+ _a.trys.push([1, 3, 4, 5]);
2077
+ log.info(tag, 'Initializing token bucket system...');
2078
+ mongo = require('@pioneer-platform/default-mongo').mongo;
2079
+ db = mongo.db('pioneer');
2080
+ // Initialize metrics logger first
2081
+ metricsLogger = new metrics_logger_1.MetricsLogger(db);
2082
+ log.info(tag, '✅ MetricsLogger initialized');
2083
+ // Initialize token bucket manager
2084
+ tokenBucketManager = new token_bucket_manager_1.TokenBucketManager(db);
2085
+ return [4 /*yield*/, tokenBucketManager.initialize()];
2086
+ case 2:
2087
+ _a.sent();
2088
+ log.info(tag, '✅ TokenBucketManager initialized');
2089
+ // Schedule daily refill at midnight UTC
2090
+ (0, refill_scheduler_1.scheduleDailyRefill)(tokenBucketManager, metricsLogger);
2091
+ log.info(tag, '✅ Daily refill scheduler started');
2092
+ log.info(tag, '🎉 Token bucket system fully initialized!');
2093
+ return [3 /*break*/, 5];
2094
+ case 3:
2095
+ error_12 = _a.sent();
2096
+ log.error(tag, 'Failed to initialize token buckets:', error_12);
2097
+ return [3 /*break*/, 5];
2098
+ case 4:
2099
+ isInitializing = false;
2100
+ return [7 /*endfinally*/];
2101
+ case 5: return [2 /*return*/];
2102
+ }
2103
+ });
2104
+ });
2105
+ }
2106
+ // Call on module load (with delay to ensure MongoDB is ready)
2107
+ setTimeout(function () {
2108
+ initializeTokenBuckets().catch(function (err) {
2109
+ log.error(TAG, 'Token bucket initialization failed:', err);
2110
+ });
2111
+ }, 5000); // 5 second delay to let MongoDB connect