init
This commit is contained in:
@@ -11,6 +11,7 @@ import { Request, Response } from 'express';
|
||||
import { getCoingeckoId } from '../lib/token-registry';
|
||||
import { ALL_CHAINS } from '../services/wallet-generator.service';
|
||||
import { getPricesBySymbols } from '../services/price-oracle.service';
|
||||
// getPricesWithChangeByIds импортируется dynamic'но в getDynamics handler ниже.
|
||||
import type { ChainCode } from '../lib/address-validators';
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
@@ -135,4 +136,87 @@ export const PricesController = {
|
||||
res.status(502).json({ success: false, error: 'Upstream price oracle error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/prices/dynamics?symbols=BTC,ETH,BNB,SOL,TRX
|
||||
*
|
||||
* Возвращает USD-цену + 24h % изменения для списка symbols.
|
||||
* Default symbols (если query не задан): BTC,ETH,BNB,SOL,TRX.
|
||||
* Source: CoinGecko `include_24hr_change=true` (rolling 24h, не anchored).
|
||||
*
|
||||
* Response 200:
|
||||
* { success: true, data: { "BTC": { "usd": 67432.12, "change24h": -1.38 }, ... } }
|
||||
*/
|
||||
async getDynamics(req: Request, res: Response) {
|
||||
try {
|
||||
const rawSymbols = String(req.query.symbols || '').trim();
|
||||
const symbols = rawSymbols
|
||||
? rawSymbols.split(',').map((s) => s.trim().toUpperCase()).filter((s) => s.length > 0)
|
||||
: ['BTC', 'ETH', 'BNB', 'SOL', 'TRX'];
|
||||
|
||||
if (symbols.length === 0) {
|
||||
res.status(400).json({ success: false, error: 'symbols list is empty' });
|
||||
return;
|
||||
}
|
||||
if (symbols.length > MAX_SYMBOLS_PER_REQUEST) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Too many symbols (max ${MAX_SYMBOLS_PER_REQUEST})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (const s of symbols) {
|
||||
if (!SYMBOL_RE.test(s)) {
|
||||
res.status(400).json({ success: false, error: `Invalid symbol: ${s}` });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve каждый symbol в CoinGecko id напрямую.
|
||||
// Native tickers: BTC=bitcoin, ETH=ethereum, BNB=binancecoin, SOL=solana, TRX=tron.
|
||||
// Для non-native: пытаемся getCoingeckoId через chain fallback.
|
||||
const NATIVE_TICKER_TO_COINGECKO: Record<string, string> = {
|
||||
BTC: 'bitcoin',
|
||||
ETH: 'ethereum',
|
||||
BNB: 'binancecoin',
|
||||
SOL: 'solana',
|
||||
TRX: 'tron',
|
||||
};
|
||||
const fallbackChains: ChainCode[] = ['ETH', 'BSC', 'SOL', 'TRX', 'BTC'];
|
||||
|
||||
const symbolToCgId = new Map<string, string>();
|
||||
for (const sym of symbols) {
|
||||
let cgId: string | null = NATIVE_TICKER_TO_COINGECKO[sym] ?? null;
|
||||
if (!cgId) {
|
||||
for (const c of fallbackChains) {
|
||||
const id = getCoingeckoId(c, sym);
|
||||
if (id) { cgId = id; break; }
|
||||
}
|
||||
}
|
||||
if (!cgId) {
|
||||
res.status(400).json({ success: false, error: `Unknown symbol: ${sym}` });
|
||||
return;
|
||||
}
|
||||
symbolToCgId.set(sym, cgId);
|
||||
}
|
||||
|
||||
const { getPricesWithChangeByIds } = await import('../services/price-oracle.service');
|
||||
const rich = await getPricesWithChangeByIds(Array.from(new Set(symbolToCgId.values())));
|
||||
|
||||
const data: Record<string, { usd: number | null; change24h: number | null }> = {};
|
||||
for (const sym of symbols) {
|
||||
const cgId = symbolToCgId.get(sym)!;
|
||||
const v = rich[cgId];
|
||||
data[sym] = {
|
||||
usd: v?.usd ?? null,
|
||||
change24h: v?.change24h ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
res.json({ success: true, data });
|
||||
} catch (err: any) {
|
||||
logger.error(`getDynamics failed: ${err?.stack || err?.message || 'unknown'}`);
|
||||
res.status(502).json({ success: false, error: 'Upstream price oracle error' });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user