mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* fix: three panel issues — Tech Readiness toggle, Crypto top 10, FIRMS key check 1. #1132 — Add tech-readiness to FULL_PANELS so it appears in the Settings toggle list for Full/Geopolitical variant users. 2. #979 — Expand crypto panel from 4 coins to top 10 by market cap (BTC, ETH, USDT, BNB, SOL, XRP, USDC, ADA, DOGE, TRX) across client config, server metadata, CoinPaprika fallback map, and seed script. 3. #997 — Check isFeatureAvailable('nasaFirms') before loading FIRMS data. When the API key is missing, show a clear "not configured" message instead of the generic "No fire data available". Closes #1132, closes #979, closes #997 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace stablecoins with AVAX/LINK, remove duplicate key, revert FIRMS change - Replace USDT/USDC (stablecoins pegged ~$1) with AVAX and LINK - Remove duplicate 'usd-coin' key in COINPAPRIKA_ID_MAP - Add CoinPaprika fallback IDs for avalanche-2 and chainlink - Revert FIRMS API key gating (handled differently now) - Add sync comments across the 3 crypto config locations * fix: update AIS relay + seed CoinPaprika fallback for all 10 coins The AIS relay (primary seeder) still had the old 4-coin list. The seed script's CoinPaprika fallback map was also missing the new coins. Both now have all 10 entries. * refactor: DRY crypto config into shared/crypto.json Single source of truth for crypto IDs, metadata, and CoinPaprika fallback mappings. All 4 consumers now import from shared/crypto.json: - src/config/markets.ts (client) - server/worldmonitor/market/v1/_shared.ts (server) - scripts/seed-crypto-quotes.mjs (seed script) - scripts/ais-relay.cjs (primary relay seeder) Adding a new coin now requires editing only shared/crypto.json. * chore: fix pre-existing markdown lint errors in README.md Add blank lines between headings and lists per MD022/MD032 rules. * fix: correct CoinPaprika XRP mapping and add crypto config test - Fix xrp-ripple → xrp-xrp (current CoinPaprika id) - Add tests/crypto-config.test.mjs: validates every coin has meta, coinpaprika mapping, unique symbols, no stablecoins, and valid id format — bad fallback ids now fail fast * test: validate CoinPaprika ids against live API The regex-only check wouldn't have caught the xrp-ripple typo. New test fetches /v1/coins from CoinPaprika and asserts every configured id exists. Gracefully skips if API is unreachable. * fix(test): handle network failures in CoinPaprika API validation Wrap fetch in try-catch so DNS failures, timeouts, and rate limits skip gracefully instead of failing the test suite. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Elie Habib <elie.habib@gmail.com>
This commit is contained in:
committed by
GitHub
parent
2d1163153d
commit
7b9426299d
@@ -1296,9 +1296,10 @@ async function seedEtfFlows() {
|
||||
}
|
||||
|
||||
// Crypto Quotes — CoinGecko → CoinPaprika fallback
|
||||
const CRYPTO_IDS = ['bitcoin', 'ethereum', 'solana', 'ripple'];
|
||||
const CRYPTO_META = { bitcoin: { name: 'Bitcoin', symbol: 'BTC' }, ethereum: { name: 'Ethereum', symbol: 'ETH' }, solana: { name: 'Solana', symbol: 'SOL' }, ripple: { name: 'XRP', symbol: 'XRP' } };
|
||||
const CRYPTO_PAPRIKA_MAP = { bitcoin: 'btc-bitcoin', ethereum: 'eth-ethereum', solana: 'sol-solana', ripple: 'xrp-ripple' };
|
||||
const _cryptoCfg = require('../shared/crypto.json');
|
||||
const CRYPTO_IDS = _cryptoCfg.ids;
|
||||
const CRYPTO_META = _cryptoCfg.meta;
|
||||
const CRYPTO_PAPRIKA_MAP = _cryptoCfg.coinpaprika;
|
||||
const CRYPTO_SEED_TTL = 3600; // 1h
|
||||
|
||||
async function fetchCryptoCoinPaprika() {
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import { loadEnvFile, CHROME_UA, runSeed, sleep } from './_seed-utils.mjs';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const cryptoConfig = require('../shared/crypto.json');
|
||||
|
||||
loadEnvFile(import.meta.url);
|
||||
|
||||
const CANONICAL_KEY = 'market:crypto:v1';
|
||||
const CACHE_TTL = 3600; // 1 hour
|
||||
|
||||
const CRYPTO_IDS = ['bitcoin', 'ethereum', 'solana', 'ripple'];
|
||||
const CRYPTO_META = {
|
||||
bitcoin: { name: 'Bitcoin', symbol: 'BTC' },
|
||||
ethereum: { name: 'Ethereum', symbol: 'ETH' },
|
||||
solana: { name: 'Solana', symbol: 'SOL' },
|
||||
ripple: { name: 'XRP', symbol: 'XRP' },
|
||||
};
|
||||
const CRYPTO_IDS = cryptoConfig.ids;
|
||||
const CRYPTO_META = cryptoConfig.meta;
|
||||
|
||||
async function fetchWithRateLimitRetry(url, maxAttempts = 5, headers = { Accept: 'application/json', 'User-Agent': CHROME_UA }) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
@@ -33,12 +32,7 @@ async function fetchWithRateLimitRetry(url, maxAttempts = 5, headers = { Accept:
|
||||
throw new Error('CoinGecko rate limit exceeded after retries');
|
||||
}
|
||||
|
||||
const COINPAPRIKA_ID_MAP = {
|
||||
bitcoin: 'btc-bitcoin',
|
||||
ethereum: 'eth-ethereum',
|
||||
solana: 'sol-solana',
|
||||
ripple: 'xrp-ripple',
|
||||
};
|
||||
const COINPAPRIKA_ID_MAP = cryptoConfig.coinpaprika;
|
||||
|
||||
async function fetchFromCoinGecko() {
|
||||
const ids = CRYPTO_IDS.join(',');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Shared helpers, types, and constants for the market service handler RPCs.
|
||||
*/
|
||||
import { CHROME_UA, yahooGate } from '../../../_shared/constants';
|
||||
import cryptoConfig from '../../../../shared/crypto.json';
|
||||
|
||||
// ========================================================================
|
||||
// Relay helpers (Railway proxy for Yahoo when Vercel IPs are rate-limited)
|
||||
@@ -69,13 +70,7 @@ export const YAHOO_ONLY_SYMBOLS = new Set([
|
||||
'GC=F', 'CL=F', 'NG=F', 'SI=F', 'HG=F',
|
||||
]);
|
||||
|
||||
// Known crypto IDs and their metadata
|
||||
export const CRYPTO_META: Record<string, { name: string; symbol: string }> = {
|
||||
bitcoin: { name: 'Bitcoin', symbol: 'BTC' },
|
||||
ethereum: { name: 'Ethereum', symbol: 'ETH' },
|
||||
solana: { name: 'Solana', symbol: 'SOL' },
|
||||
ripple: { name: 'XRP', symbol: 'XRP' },
|
||||
};
|
||||
export const CRYPTO_META: Record<string, { name: string; symbol: string }> = cryptoConfig.meta;
|
||||
|
||||
// ========================================================================
|
||||
// Types
|
||||
@@ -273,12 +268,9 @@ export async function fetchCoinGeckoMarkets(
|
||||
// CoinPaprika fallback fetcher
|
||||
// ========================================================================
|
||||
|
||||
// CoinGecko ID → CoinPaprika ID mapping
|
||||
// CoinGecko ID → CoinPaprika ID mapping (shared ids + stablecoin-specific)
|
||||
const COINPAPRIKA_ID_MAP: Record<string, string> = {
|
||||
bitcoin: 'btc-bitcoin',
|
||||
ethereum: 'eth-ethereum',
|
||||
solana: 'sol-solana',
|
||||
ripple: 'xrp-ripple',
|
||||
...cryptoConfig.coinpaprika,
|
||||
tether: 'usdt-tether',
|
||||
'usd-coin': 'usdc-usd-coin',
|
||||
dai: 'dai-dai',
|
||||
|
||||
31
shared/crypto.json
Normal file
31
shared/crypto.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"ids": [
|
||||
"bitcoin", "ethereum", "binancecoin", "solana",
|
||||
"ripple", "cardano", "dogecoin", "tron",
|
||||
"avalanche-2", "chainlink"
|
||||
],
|
||||
"meta": {
|
||||
"bitcoin": { "name": "Bitcoin", "symbol": "BTC" },
|
||||
"ethereum": { "name": "Ethereum", "symbol": "ETH" },
|
||||
"binancecoin": { "name": "BNB", "symbol": "BNB" },
|
||||
"solana": { "name": "Solana", "symbol": "SOL" },
|
||||
"ripple": { "name": "XRP", "symbol": "XRP" },
|
||||
"cardano": { "name": "Cardano", "symbol": "ADA" },
|
||||
"dogecoin": { "name": "Dogecoin", "symbol": "DOGE" },
|
||||
"tron": { "name": "TRON", "symbol": "TRX" },
|
||||
"avalanche-2": { "name": "Avalanche", "symbol": "AVAX" },
|
||||
"chainlink": { "name": "Chainlink", "symbol": "LINK" }
|
||||
},
|
||||
"coinpaprika": {
|
||||
"bitcoin": "btc-bitcoin",
|
||||
"ethereum": "eth-ethereum",
|
||||
"binancecoin": "bnb-binance-coin",
|
||||
"solana": "sol-solana",
|
||||
"ripple": "xrp-xrp",
|
||||
"cardano": "ada-cardano",
|
||||
"dogecoin": "doge-dogecoin",
|
||||
"tron": "trx-tron",
|
||||
"avalanche-2": "avax-avalanche",
|
||||
"chainlink": "link-chainlink"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Sector, Commodity, MarketSymbol } from '@/types';
|
||||
import cryptoConfig from '../../shared/crypto.json';
|
||||
|
||||
export const SECTORS: Sector[] = [
|
||||
{ symbol: 'XLK', name: 'Tech' },
|
||||
@@ -55,11 +56,5 @@ export const MARKET_SYMBOLS: MarketSymbol[] = [
|
||||
{ symbol: 'BAC', name: 'BofA', display: 'BAC' },
|
||||
];
|
||||
|
||||
export const CRYPTO_IDS = ['bitcoin', 'ethereum', 'solana', 'ripple'] as const;
|
||||
|
||||
export const CRYPTO_MAP: Record<string, { name: string; symbol: string }> = {
|
||||
bitcoin: { name: 'Bitcoin', symbol: 'BTC' },
|
||||
ethereum: { name: 'Ethereum', symbol: 'ETH' },
|
||||
solana: { name: 'Solana', symbol: 'SOL' },
|
||||
ripple: { name: 'XRP', symbol: 'XRP' },
|
||||
};
|
||||
export const CRYPTO_IDS = cryptoConfig.ids as readonly string[];
|
||||
export const CRYPTO_MAP: Record<string, { name: string; symbol: string }> = cryptoConfig.meta;
|
||||
|
||||
@@ -58,6 +58,7 @@ const FULL_PANELS: Record<string, PanelConfig> = {
|
||||
'oref-sirens': { name: 'Israel Sirens', enabled: true, priority: 2, ...(_desktop && { premium: 'locked' as const }) },
|
||||
'telegram-intel': { name: 'Telegram Intel', enabled: true, priority: 2, ...(_desktop && { premium: 'locked' as const }) },
|
||||
'airline-intel': { name: 'Airline Intelligence', enabled: true, priority: 2 },
|
||||
'tech-readiness': { name: 'Tech Readiness Index', enabled: true, priority: 2 },
|
||||
'world-clock': { name: 'World Clock', enabled: true, priority: 2 },
|
||||
};
|
||||
|
||||
|
||||
62
tests/crypto-config.test.mjs
Normal file
62
tests/crypto-config.test.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const crypto = require('../shared/crypto.json');
|
||||
|
||||
describe('shared/crypto.json integrity', () => {
|
||||
it('every id in ids has a meta entry', () => {
|
||||
for (const id of crypto.ids) {
|
||||
assert.ok(crypto.meta[id], `missing meta for "${id}"`);
|
||||
assert.ok(crypto.meta[id].name, `missing meta.name for "${id}"`);
|
||||
assert.ok(crypto.meta[id].symbol, `missing meta.symbol for "${id}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it('every id in ids has a coinpaprika mapping', () => {
|
||||
for (const id of crypto.ids) {
|
||||
assert.ok(crypto.coinpaprika[id], `missing coinpaprika mapping for "${id}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it('coinpaprika ids follow the symbol-name pattern', () => {
|
||||
for (const [geckoId, paprikaId] of Object.entries(crypto.coinpaprika)) {
|
||||
assert.match(paprikaId, /^[a-z0-9]+-[a-z0-9-]+$/, `bad coinpaprika id format for "${geckoId}": "${paprikaId}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it('coinpaprika ids exist on CoinPaprika API', async () => {
|
||||
let coins;
|
||||
try {
|
||||
const resp = await fetch('https://api.coinpaprika.com/v1/coins', {
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!resp.ok) { console.log(` skipping: CoinPaprika API returned ${resp.status}`); return; }
|
||||
coins = await resp.json();
|
||||
} catch (err) {
|
||||
console.log(` skipping: CoinPaprika unreachable (${err.code || err.message})`);
|
||||
return;
|
||||
}
|
||||
const validIds = new Set(coins.map((c) => c.id));
|
||||
const invalid = [];
|
||||
for (const [geckoId, paprikaId] of Object.entries(crypto.coinpaprika)) {
|
||||
if (!validIds.has(paprikaId)) invalid.push(`${geckoId} → ${paprikaId}`);
|
||||
}
|
||||
assert.equal(invalid.length, 0, `invalid CoinPaprika ids:\n ${invalid.join('\n ')}`);
|
||||
});
|
||||
|
||||
it('symbols are unique', () => {
|
||||
const symbols = Object.values(crypto.meta).map((m) => m.symbol);
|
||||
assert.equal(new Set(symbols).size, symbols.length, `duplicate symbols: ${symbols}`);
|
||||
});
|
||||
|
||||
it('no stablecoins in the top-coins list', () => {
|
||||
const stableSymbols = new Set(['USDT', 'USDC', 'DAI', 'FDUSD', 'USDE', 'TUSD', 'BUSD']);
|
||||
for (const id of crypto.ids) {
|
||||
const sym = crypto.meta[id]?.symbol;
|
||||
assert.ok(!stableSymbols.has(sym), `stablecoin "${sym}" (${id}) should not be in top-coins list`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user