Files
worldmonitor/scripts/seed-crypto-sectors.mjs
Elie Habib c0bf784d21 feat(finance): crypto sectors heatmap + DeFi/AI/Alt token panels + expanded crypto news (#1900)
* feat(market): add crypto sectors heatmap and token panels (DeFi, AI, Other) backend

- Add shared/crypto-sectors.json, defi-tokens.json, ai-tokens.json, other-tokens.json configs
- Add scripts/seed-crypto-sectors.mjs and seed-token-panels.mjs seed scripts
- Add proto messages for ListCryptoSectors, ListDefiTokens, ListAiTokens, ListOtherTokens
- Add change7d field (field 6) to CryptoQuote proto message
- Run buf generate to produce updated TypeScript bindings
- Add server handlers for all 4 new RPCs reading from seeded Redis cache
- Wire handlers into marketHandler and register cache keys with BOOTSTRAP_TIERS=slow
- Wire seedCryptoSectors and seedTokenPanels into ais-relay.cjs seedAllMarketData loop

* feat(panels): add crypto sectors heatmap and token panels (DeFi, AI, Other)

- Add TokenData interface to src/types/index.ts
- Wire ListCryptoSectorsResponse/ListDefiTokensResponse/ListAiTokensResponse/ListOtherTokensResponse into market service with circuit breakers and hydration fallbacks
- Add CryptoHeatmapPanel, TokenListPanel, DefiTokensPanel, AiTokensPanel, OtherTokensPanel to MarketPanel.ts
- Register 4 new panels in panels.ts FINANCE_PANELS and cryptoDigital category
- Instantiate new panels in panel-layout.ts
- Load data in data-loader.ts loadMarkets() alongside existing crypto fetch

* fix(crypto-panels): resolve test failures and type errors post-review

- Add @ts-nocheck to regenerated market service_server/client (matches repo convention)
- Add 4 new RPC routes to RPC_CACHE_TIER in gateway.ts (route-cache-tier test)
- Sync scripts/shared/ with shared/ for new token/sector JSON configs
- Restore non-market generated files to origin/main state (avoid buf version diff)

* fix(crypto-panels): address code review findings (P1-P3)

- ais-relay seedTokenPanels: add empty-guard before Redis write to
  prevent overwriting cached data when all IDs are unresolvable
- server _feeds.ts: sync 4 missing crypto feeds (Wu Blockchain, Messari,
  NFT News, Stablecoin Policy) with client-side feeds.ts
- data-loader: expose panel refs outside try block so catch can call
  showRetrying(); log error instead of swallowing silently
- MarketPanel: replace hardcoded English error strings with t() calls
  (failedSectorData / failedCryptoData) to honour user locale
- seed-token-panels.mjs: remove unused getRedisCredentials import
- cache-keys.ts: one BOOTSTRAP_TIERS entry per line for consistency

* fix(crypto-panels): three correctness fixes for RSS proxy, refresh, and Redis write visibility

- api/_rss-allowed-domains.js: add 7 new crypto domains (decrypt.co,
  blockworks.co, thedefiant.io, bitcoinmagazine.com, www.dlnews.com,
  cryptoslate.com, unchainedcrypto.com) so rss-proxy.js accepts the
  new finance feeds instead of rejecting them as disallowed hosts
- src/App.ts: add crypto-heatmap/defi-tokens/ai-tokens/other-tokens to
  the periodic markets refresh viewport condition so panels on screen
  continue receiving live updates, not just the initial load
- ais-relay seedTokenPanels: capture upstashSet return values and log
  PARTIAL if any Redis write fails, matching seedCryptoSectors pattern
2026-03-20 10:34:20 +04:00

68 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
import { loadEnvFile, loadSharedConfig, CHROME_UA, runSeed, sleep } from './_seed-utils.mjs';
const sectorsConfig = loadSharedConfig('crypto-sectors.json');
loadEnvFile(import.meta.url);
const CANONICAL_KEY = 'market:crypto-sectors:v1';
const CACHE_TTL = 3600;
const SECTORS = sectorsConfig.sectors;
async function fetchWithRateLimitRetry(url, maxAttempts = 5, headers = { Accept: 'application/json', 'User-Agent': CHROME_UA }) {
for (let i = 0; i < maxAttempts; i++) {
const resp = await fetch(url, { headers, signal: AbortSignal.timeout(15_000) });
if (resp.status === 429) {
const wait = Math.min(10_000 * (i + 1), 60_000);
console.warn(` CoinGecko 429 — waiting ${wait / 1000}s (attempt ${i + 1}/${maxAttempts})`);
await sleep(wait);
continue;
}
if (!resp.ok) throw new Error(`CoinGecko HTTP ${resp.status}`);
return resp;
}
throw new Error('CoinGecko rate limit exceeded after retries');
}
async function fetchSectorData() {
const allIds = [...new Set(SECTORS.flatMap(s => s.tokens))];
const apiKey = process.env.COINGECKO_API_KEY;
const baseUrl = apiKey ? 'https://pro-api.coingecko.com/api/v3' : 'https://api.coingecko.com/api/v3';
const url = `${baseUrl}/coins/markets?vs_currency=usd&ids=${allIds.join(',')}&order=market_cap_desc&sparkline=false&price_change_percentage=24h`;
const headers = { Accept: 'application/json', 'User-Agent': CHROME_UA };
if (apiKey) headers['x-cg-pro-api-key'] = apiKey;
const resp = await fetchWithRateLimitRetry(url, 5, headers);
const data = await resp.json();
if (!Array.isArray(data) || data.length === 0) throw new Error('CoinGecko returned no data');
const byId = new Map(data.map(c => [c.id, c.price_change_percentage_24h]));
const sectors = SECTORS.map(sector => {
const changes = sector.tokens
.map(id => byId.get(id))
.filter(v => typeof v === 'number' && isFinite(v));
const change = changes.length > 0 ? changes.reduce((a, b) => a + b, 0) / changes.length : 0;
return { id: sector.id, name: sector.name, change };
});
return { sectors };
}
function validate(data) {
return Array.isArray(data?.sectors) && data.sectors.length > 0;
}
runSeed('market', 'crypto-sectors', CANONICAL_KEY, fetchSectorData, {
validateFn: validate,
ttlSeconds: CACHE_TTL,
sourceVersion: 'coingecko-sectors',
}).catch(err => {
const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : '';
console.error('FATAL:', (err.message || err) + _cause);
process.exit(1);
});