swaggerready

This commit is contained in:
ZOMBIIIIIII
2026-05-14 01:11:20 +03:00
parent 0661fffb88
commit 53635806d6
9 changed files with 1139 additions and 56 deletions

View File

@@ -0,0 +1,138 @@
/**
* GET /api/prices — USD prices for selected token symbols.
*
* Security:
* S1 — whitelist через `getCoingeckoId`. Любой symbol вне registry → 400.
* S2 — лимит max 50 (symbol, chain) пар. Иначе → 400.
* S5 — общий 502 при failure, без stack trace.
* S7 — auth provided by router middleware.
*/
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';
import type { ChainCode } from '../lib/address-validators';
import { logger } from '../lib/logger';
const MAX_SYMBOLS_PER_REQUEST = 50;
const ALLOWED_CHAINS = new Set<ChainCode>(ALL_CHAINS);
const SYMBOL_RE = /^[A-Z0-9]{1,16}$/;
function isChain(v: unknown): v is ChainCode {
return typeof v === 'string' && ALLOWED_CHAINS.has(v as ChainCode);
}
export const PricesController = {
/**
* GET /api/prices?symbols=BTC,ETH,USDT&chain=ETH
*
* Params:
* - symbols: comma-separated list, max 50. Каждый symbol должен быть в whitelist.
* - chain (опционально): chain для disambiguation (USDT на ETH vs USDT на BSC).
* Если не указан — для каждого symbol fallback порядок: ETH → BSC → SOL → TRX → BTC.
* Native symbol (BTC/ETH/...) всегда matches its chain.
*
* Response 200:
* { success: true, data: { "BTC": { "usd": 67432.12 }, "ETH": { "usd": 3210.45 }, "FOO": { "usd": null } } }
*/
async getPrices(req: Request, res: Response) {
try {
const rawSymbols = String(req.query.symbols || '').trim();
if (!rawSymbols) {
res.status(400).json({ success: false, error: 'symbols query param is required (csv)' });
return;
}
const requestedChain = req.query.chain ? String(req.query.chain).toUpperCase() : null;
if (requestedChain && !isChain(requestedChain)) {
res.status(400).json({ success: false, error: 'Invalid chain parameter' });
return;
}
const symbols = rawSymbols
.split(',')
.map((s) => s.trim().toUpperCase())
.filter((s) => s.length > 0);
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;
}
// Strict symbol shape (S1 belt-and-suspenders).
for (const s of symbols) {
if (!SYMBOL_RE.test(s)) {
res.status(400).json({ success: false, error: `Invalid symbol: ${s}` });
return;
}
}
// Build (chain, symbol) pairs.
// Fallback resolution order при отсутствии явного chain:
// native symbol == chain code → that chain;
// иначе пробуем ETH, BSC, SOL, TRX, BTC по очереди.
const fallbackChains: ChainCode[] = ['ETH', 'BSC', 'SOL', 'TRX', 'BTC'];
const pairs: { chain: ChainCode; symbol: string; key: string }[] = [];
for (const sym of symbols) {
if (requestedChain) {
pairs.push({ chain: requestedChain as ChainCode, symbol: sym, key: sym });
continue;
}
let resolvedChain: ChainCode | null = null;
if (ALLOWED_CHAINS.has(sym as ChainCode)) {
resolvedChain = sym as ChainCode;
} else {
for (const c of fallbackChains) {
if (getCoingeckoId(c, sym)) {
resolvedChain = c;
break;
}
}
}
if (!resolvedChain) {
// Symbol не находится ни в одной chain → 400 (S1: whitelist enforcement).
res.status(400).json({ success: false, error: `Unknown symbol: ${sym}` });
return;
}
pairs.push({ chain: resolvedChain, symbol: sym, key: sym });
}
// Если явный chain задан — повторная проверка whitelist для каждого symbol
// (native symbol для chain'а тоже разрешён).
if (requestedChain) {
for (const p of pairs) {
if (!getCoingeckoId(p.chain, p.symbol)) {
res.status(400).json({
success: false,
error: `Unknown symbol ${p.symbol} for chain ${p.chain}`,
});
return;
}
}
}
const prices = await getPricesBySymbols(
pairs.map((p) => ({ chain: p.chain, symbol: p.symbol })),
);
const data: Record<string, { usd: number | null }> = {};
for (const p of pairs) {
const lookupKey = `${p.chain}:${p.symbol}`;
data[p.key] = { usd: prices.get(lookupKey) ?? null };
}
res.json({ success: true, data });
} catch (err: any) {
logger.error(`getPrices failed: ${err?.stack || err?.message || 'unknown'}`);
res.status(502).json({ success: false, error: 'Upstream price oracle error' });
}
},
};