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

@@ -6,6 +6,8 @@ import { ethers } from 'ethers';
import { createHash } from 'crypto';
import { env } from '../config/env';
import { getEvmTokens, getTrxTokens, getSolTokens } from '../lib/token-registry';
import { getPricesBySymbols } from './price-oracle.service';
import { logger } from '../lib/logger';
export type ChainCode = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC';
@@ -30,6 +32,17 @@ export interface FormattedAmount {
raw: string; // smallest units (string-encoded BigInt — без потери точности)
formatted: string; // human-readable, e.g. "0.003"
decimals: number; // decimals chain'а/токена
/**
* USD price per 1 целая единица (e.g. $67432.12 за 1 BTC).
* `null` если symbol не в реестре с `coingeckoId` или upstream price oracle недоступен.
* Источник: CoinGecko free API, cache 5 мин в KeyDB.
*/
usdPrice: number | null;
/**
* Совокупная USD-стоимость holding'а: `Number(formatted) × usdPrice`.
* Округлено до 8 знаков. `null` если `usdPrice === null` или вычисление дало `Infinity/NaN`.
*/
usdValue: number | null;
}
export interface BalanceResult {
@@ -70,7 +83,67 @@ export function formatUnits(raw: string, decimals: number): string {
}
function fmt(raw: string, decimals: number): FormattedAmount {
return { raw, formatted: formatUnits(raw, decimals), decimals };
return {
raw,
formatted: formatUnits(raw, decimals),
decimals,
usdPrice: null, // populated post-build via populatePrices()
usdValue: null,
};
}
/**
* Округление USD значения до 8 знаков после запятой (избавляемся от float-мусора).
* S10 — `Infinity`/`NaN` → `null`.
*/
function roundUsd(n: number): number | null {
if (!Number.isFinite(n)) return null;
return Math.round(n * 1e8) / 1e8;
}
/**
* Мутирует BalanceResult, проставляя `usdPrice` и `usdValue` каждому FormattedAmount.
* Никогда не throws — если price oracle упал, поля остаются `null`.
*/
async function populatePrices(result: BalanceResult): Promise<void> {
try {
const pairs: { chain: ChainCode; symbol: string }[] = [
{ chain: result.chain, symbol: result.chain }, // native (BTC/ETH/BSC/TRX/SOL)
];
if (result.tokens) {
for (const sym of Object.keys(result.tokens)) {
pairs.push({ chain: result.chain, symbol: sym });
}
}
const prices = await getPricesBySymbols(pairs);
// Native
const nativeKey = `${result.chain}:${result.chain}`;
const nativePrice = prices.get(nativeKey) ?? null;
result.native.usdPrice = nativePrice;
if (nativePrice != null) {
const formattedNum = Number(result.native.formatted);
result.native.usdValue = Number.isFinite(formattedNum)
? roundUsd(formattedNum * nativePrice)
: null;
}
// Tokens
if (result.tokens) {
for (const [sym, amt] of Object.entries(result.tokens)) {
const key = `${result.chain}:${sym}`;
const p = prices.get(key) ?? null;
amt.usdPrice = p;
if (p != null) {
const fNum = Number(amt.formatted);
amt.usdValue = Number.isFinite(fNum) ? roundUsd(fNum * p) : null;
}
}
}
} catch (err: any) {
// Не валим запрос — balance вернётся без цен.
logger.warn(`populatePrices(${result.chain}) failed: ${err?.message || 'unknown'}`);
}
}
function fmtTokens(
@@ -86,20 +159,23 @@ function fmtTokens(
export async function getBalance(chain: ChainCode, address: string): Promise<BalanceResult> {
const nativeDecimals = NATIVE_DECIMALS[chain];
let result: BalanceResult;
switch (chain) {
case 'BTC':
return {
result = {
chain, address,
native: fmt(await btcBalance(address), nativeDecimals),
};
break;
case 'TRX': {
const { trx, tokens } = await trxBalance(address);
const decimalsMap = Object.fromEntries(getTrxTokens().map((t) => [t.symbol, t.decimals]));
return {
result = {
chain, address,
native: fmt(trx, nativeDecimals),
tokens: fmtTokens(tokens, decimalsMap),
};
break;
}
case 'BSC':
case 'ETH': {
@@ -111,22 +187,28 @@ export async function getBalance(chain: ChainCode, address: string): Promise<Bal
tokenList.map((t) => ({ symbol: t.symbol, addr: t.contractAddress })),
);
const decimalsMap = Object.fromEntries(tokenList.map((t) => [t.symbol, t.decimals]));
return {
result = {
chain, address,
native: fmt(native, nativeDecimals),
tokens: fmtTokens(tokens, decimalsMap),
};
break;
}
case 'SOL': {
const { native, tokens } = await solBalance(address);
const decimalsMap = Object.fromEntries(getSolTokens().map((t) => [t.symbol, t.decimals]));
return {
result = {
chain, address,
native: fmt(native, nativeDecimals),
tokens: fmtTokens(tokens, decimalsMap),
};
break;
}
}
// Populate USD prices (graceful — never throws, fields stay null on failure).
await populatePrices(result);
return result;
}
async function btcBalance(address: string): Promise<string> {