@pioneer-platform/pioneer-cache 1.28.15 → 2.0.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.
@@ -1,5 +1,5 @@
1
1
 
2
2
  
3
- > @pioneer-platform/pioneer-cache@1.28.15 build /Users/highlander/WebstormProjects/keepkey-stack/projects/pioneer/modules/pioneer/pioneer-cache
3
+ > @pioneer-platform/pioneer-cache@2.0.0 build /Users/highlander/WebstormProjects/keepkey-stack/projects/pioneer/modules/pioneer/pioneer-cache
4
4
  > tsc
5
5
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @pioneer-platform/pioneer-cache
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - fix: prevent $0 price death spiral for major cryptocurrencies
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @pioneer-platform/pioneer-discovery@9.0.0
13
+
3
14
  ## 1.28.15
4
15
 
5
16
  ### Patch Changes
@@ -103,14 +103,37 @@ class BaseCache {
103
103
  normalizedCaip &&
104
104
  Array.from(MAJOR_CRYPTO_WHITELIST).some(wl => wl.toLowerCase() === normalizedCaip));
105
105
  if (isInvalidMajorCryptoPrice) {
106
- log.warn(tag, `🚨 Zero price cached for major cryptocurrency: ${params.caip} - triggering HIGH PRIORITY refresh`);
107
- this.triggerAsyncRefresh(params, 'high');
108
- // Still return the cached zero, but mark as not fresh
106
+ log.warn(tag, `🚨 Zero price cached for major cryptocurrency: ${params.caip} - BLOCKING to fetch fresh price`);
107
+ // DELETE the $0 entry so it can't poison future requests
108
+ try {
109
+ await this.redis.del(key);
110
+ log.warn(tag, `🗑️ Deleted $0 cache entry for major crypto: ${key}`);
111
+ }
112
+ catch (delErr) {
113
+ log.error(tag, `Failed to delete $0 entry:`, delErr);
114
+ }
115
+ // BLOCK and wait for a real price — $0 BTC is never acceptable
116
+ try {
117
+ const freshValue = await this.fetchFresh(params);
118
+ if (freshValue.price > 0) {
119
+ return {
120
+ success: true,
121
+ value: freshValue,
122
+ cached: false,
123
+ fresh: true,
124
+ age: 0
125
+ };
126
+ }
127
+ }
128
+ catch (fetchErr) {
129
+ log.error(tag, `Fresh fetch failed for major crypto:`, fetchErr);
130
+ }
131
+ // Last resort: return $0 but mark as invalid
109
132
  return {
110
133
  success: true,
111
134
  value: cachedValue.value,
112
135
  cached: true,
113
- fresh: false, // NOT FRESH - force UI to show loading state
136
+ fresh: false,
114
137
  age,
115
138
  invalidPrice: true
116
139
  };
@@ -237,6 +260,15 @@ class BaseCache {
237
260
  // CRITICAL: Never overwrite existing non-zero price with $0
238
261
  // This protects against API failures/rate limits overwriting good cached prices
239
262
  if (this.config.name === 'price' && value.price === 0) {
263
+ // Extract CAIP from key (format: price_v2:<caip>)
264
+ const caipFromKey = key.replace(this.config.keyPrefix, '');
265
+ const isMajorCrypto = Array.from(MAJOR_CRYPTO_WHITELIST).some(wl => wl.toLowerCase() === caipFromKey.toLowerCase());
266
+ if (isMajorCrypto) {
267
+ // NEVER cache $0 for major crypto — this is always an API failure
268
+ log.error(tag, `🛡️ BLOCKING $0 cache write for major crypto: ${key}`);
269
+ log.error(tag, ` $0 for BTC/ETH/etc is ALWAYS an API failure, never caching it`);
270
+ return; // Do NOT write — $0 for major crypto is NEVER valid
271
+ }
240
272
  const existingCache = await this.getCached(key);
241
273
  if (existingCache && existingCache.value.price > 0) {
242
274
  log.warn(tag, `🛡️ Refusing to overwrite $${existingCache.value.price} with $0 for ${key}`);
@@ -87,6 +87,26 @@ class PriceCache extends base_cache_1.BaseCache {
87
87
  throw new Error(`No valid price available for ${caip}`);
88
88
  }
89
89
  log.debug(tag, `Fetched price for ${caip}: $${price}`);
90
+ // CRITICAL: $0 for major crypto = API failure, NOT a valid price
91
+ // Throw so fetchFresh() falls back to stale cache instead of caching $0
92
+ if (price === 0) {
93
+ const normalizedCaip = caip.toLowerCase();
94
+ const MAJOR_CAIPS = [
95
+ 'bip122:000000000019d6689c085ae165831e93/slip44:0', // BTC
96
+ 'eip155:1/slip44:60', // ETH
97
+ 'eip155:56/slip44:60', // BNB
98
+ 'cosmos:cosmoshub-4/slip44:118', // ATOM
99
+ 'cosmos:thorchain-mainnet-v1/slip44:931', // RUNE
100
+ 'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144', // XRP
101
+ 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2', // LTC
102
+ 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3', // DOGE
103
+ ];
104
+ const isMajorCrypto = MAJOR_CAIPS.some(wl => wl.toLowerCase() === normalizedCaip);
105
+ if (isMajorCrypto) {
106
+ log.error(tag, `🚨 All APIs returned $0 for major crypto ${caip} — treating as API failure`);
107
+ throw new Error(`API failure: $0 price for major crypto ${caip}`);
108
+ }
109
+ }
90
110
  // FIX #8: Mark zero prices with special source to indicate they are unpriceable tokens
91
111
  // This allows us to track and monitor unpriceable token caching
92
112
  const source = price === 0 ? 'unpriceable' : 'markets-caip';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pioneer-platform/pioneer-cache",
3
- "version": "1.28.15",
3
+ "version": "2.0.0",
4
4
  "description": "Unified caching system for Pioneer platform with Redis backend",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -15,10 +15,10 @@
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
17
  "@pioneer-platform/loggerdog": "8.11.0",
18
- "@pioneer-platform/redis-queue": "8.12.21",
19
18
  "@pioneer-platform/default-redis": "8.11.11",
20
- "@pioneer-platform/pioneer-caip": "9.27.10",
21
- "@pioneer-platform/pioneer-discovery": "8.51.15"
19
+ "@pioneer-platform/pioneer-discovery": "9.0.0",
20
+ "@pioneer-platform/redis-queue": "8.12.21",
21
+ "@pioneer-platform/pioneer-caip": "9.27.10"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/jest": "^29.5.0",
@@ -130,15 +130,38 @@ export abstract class BaseCache<T> {
130
130
  );
131
131
 
132
132
  if (isInvalidMajorCryptoPrice) {
133
- log.warn(tag, `🚨 Zero price cached for major cryptocurrency: ${params.caip} - triggering HIGH PRIORITY refresh`);
134
- this.triggerAsyncRefresh(params, 'high');
135
-
136
- // Still return the cached zero, but mark as not fresh
133
+ log.warn(tag, `🚨 Zero price cached for major cryptocurrency: ${params.caip} - BLOCKING to fetch fresh price`);
134
+
135
+ // DELETE the $0 entry so it can't poison future requests
136
+ try {
137
+ await this.redis.del(key);
138
+ log.warn(tag, `🗑️ Deleted $0 cache entry for major crypto: ${key}`);
139
+ } catch (delErr) {
140
+ log.error(tag, `Failed to delete $0 entry:`, delErr);
141
+ }
142
+
143
+ // BLOCK and wait for a real price — $0 BTC is never acceptable
144
+ try {
145
+ const freshValue = await this.fetchFresh(params);
146
+ if ((freshValue as any).price > 0) {
147
+ return {
148
+ success: true,
149
+ value: freshValue,
150
+ cached: false,
151
+ fresh: true,
152
+ age: 0
153
+ };
154
+ }
155
+ } catch (fetchErr) {
156
+ log.error(tag, `Fresh fetch failed for major crypto:`, fetchErr);
157
+ }
158
+
159
+ // Last resort: return $0 but mark as invalid
137
160
  return {
138
161
  success: true,
139
162
  value: cachedValue.value,
140
163
  cached: true,
141
- fresh: false, // NOT FRESH - force UI to show loading state
164
+ fresh: false,
142
165
  age,
143
166
  invalidPrice: true
144
167
  };
@@ -281,6 +304,19 @@ export abstract class BaseCache<T> {
281
304
  // CRITICAL: Never overwrite existing non-zero price with $0
282
305
  // This protects against API failures/rate limits overwriting good cached prices
283
306
  if (this.config.name === 'price' && (value as any).price === 0) {
307
+ // Extract CAIP from key (format: price_v2:<caip>)
308
+ const caipFromKey = key.replace(this.config.keyPrefix, '');
309
+ const isMajorCrypto = Array.from(MAJOR_CRYPTO_WHITELIST).some(
310
+ wl => wl.toLowerCase() === caipFromKey.toLowerCase()
311
+ );
312
+
313
+ if (isMajorCrypto) {
314
+ // NEVER cache $0 for major crypto — this is always an API failure
315
+ log.error(tag, `🛡️ BLOCKING $0 cache write for major crypto: ${key}`);
316
+ log.error(tag, ` $0 for BTC/ETH/etc is ALWAYS an API failure, never caching it`);
317
+ return; // Do NOT write — $0 for major crypto is NEVER valid
318
+ }
319
+
284
320
  const existingCache = await this.getCached(key);
285
321
  if (existingCache && (existingCache.value as any).price > 0) {
286
322
  log.warn(tag, `🛡️ Refusing to overwrite $${(existingCache.value as any).price} with $0 for ${key}`);
@@ -112,6 +112,27 @@ export class PriceCache extends BaseCache<PriceData> {
112
112
 
113
113
  log.debug(tag, `Fetched price for ${caip}: $${price}`);
114
114
 
115
+ // CRITICAL: $0 for major crypto = API failure, NOT a valid price
116
+ // Throw so fetchFresh() falls back to stale cache instead of caching $0
117
+ if (price === 0) {
118
+ const normalizedCaip = caip.toLowerCase();
119
+ const MAJOR_CAIPS = [
120
+ 'bip122:000000000019d6689c085ae165831e93/slip44:0', // BTC
121
+ 'eip155:1/slip44:60', // ETH
122
+ 'eip155:56/slip44:60', // BNB
123
+ 'cosmos:cosmoshub-4/slip44:118', // ATOM
124
+ 'cosmos:thorchain-mainnet-v1/slip44:931', // RUNE
125
+ 'ripple:4109c6f2045fc7eff4cde8f9905d19c2/slip44:144', // XRP
126
+ 'bip122:12a765e31ffd4059bada1e25190f6e98/slip44:2', // LTC
127
+ 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3', // DOGE
128
+ ];
129
+ const isMajorCrypto = MAJOR_CAIPS.some(wl => wl.toLowerCase() === normalizedCaip);
130
+ if (isMajorCrypto) {
131
+ log.error(tag, `🚨 All APIs returned $0 for major crypto ${caip} — treating as API failure`);
132
+ throw new Error(`API failure: $0 price for major crypto ${caip}`);
133
+ }
134
+ }
135
+
115
136
  // FIX #8: Mark zero prices with special source to indicate they are unpriceable tokens
116
137
  // This allows us to track and monitor unpriceable token caching
117
138
  const source = price === 0 ? 'unpriceable' : 'markets-caip';