feat: security audit fixes

This commit is contained in:
ZOMBIIIIIII
2026-05-13 00:17:32 +03:00
parent e87d178d71
commit 1498ed3431
31 changed files with 2198 additions and 339 deletions

View File

@@ -4,6 +4,7 @@
*/
import { ethers } from 'ethers';
import { env } from '../config/env';
import { getEvmTokens, getTrxTokens, getSolTokens } from '../lib/token-registry';
export type ChainCode = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC';
@@ -16,10 +17,6 @@ const BSC_RPC = 'https://bsc-dataseed.binance.org';
const ETH_RPC = 'https://ethereum-rpc.publicnode.com';
const SOL_RPC = 'https://api.mainnet-beta.solana.com';
const USDT_TRC20 = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
const USDT_BEP20 = '0x55d398326f99059fF775485246999027B3197955';
const USDT_ERC20 = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
@@ -28,31 +25,106 @@ const ERC20_ABI = [
// ─────────────────────── BALANCE ───────────────────────
export interface FormattedAmount {
raw: string; // smallest units (string-encoded BigInt — без потери точности)
formatted: string; // human-readable, e.g. "0.003"
decimals: number; // decimals chain'а/токена
}
export interface BalanceResult {
chain: ChainCode;
address: string;
native: string; // в smallest units (satoshi/wei/lamports/sun)
tokens?: Record<string, string>; // например { USDT: "12345678" }
native: FormattedAmount;
tokens?: Record<string, FormattedAmount>;
}
// Native decimals per chain
const NATIVE_DECIMALS: Record<ChainCode, number> = {
ETH: 18, // wei
BSC: 18, // wei (BNB)
BTC: 8, // satoshi
TRX: 6, // sun
SOL: 9, // lamports
};
/**
* Convert raw bigint string in smallest units → human-readable decimal string.
* Без потери точности (string-based, не Number/Float).
*
* formatUnits("3000000000000000", 18) → "0.003"
* formatUnits("1500000", 6) → "1.5"
* formatUnits("123456", 0) → "123456"
*/
export function formatUnits(raw: string, decimals: number): string {
if (!/^-?\d+$/.test(raw)) return '0';
if (decimals === 0) return raw;
const negative = raw.startsWith('-');
const abs = negative ? raw.slice(1) : raw;
const padded = abs.padStart(decimals + 1, '0');
const whole = padded.slice(0, padded.length - decimals);
const frac = padded.slice(-decimals).replace(/0+$/, '');
const result = frac ? `${whole}.${frac}` : whole;
return negative ? `-${result}` : result;
}
function fmt(raw: string, decimals: number): FormattedAmount {
return { raw, formatted: formatUnits(raw, decimals), decimals };
}
function fmtTokens(
raw: Record<string, string>,
decimalsLookup: Record<string, number>,
): Record<string, FormattedAmount> {
const out: Record<string, FormattedAmount> = {};
for (const [sym, val] of Object.entries(raw)) {
out[sym] = fmt(val, decimalsLookup[sym] ?? 0);
}
return out;
}
export async function getBalance(chain: ChainCode, address: string): Promise<BalanceResult> {
const nativeDecimals = NATIVE_DECIMALS[chain];
switch (chain) {
case 'BTC':
return { chain, address, native: await btcBalance(address) };
return {
chain, address,
native: fmt(await btcBalance(address), nativeDecimals),
};
case 'TRX': {
const { trx, usdt } = await trxBalance(address);
return { chain, address, native: trx, tokens: { USDT: usdt } };
}
case 'BSC': {
const { native, tokens } = await evmBalance(BSC_RPC, address, [{ symbol: 'USDT', addr: USDT_BEP20 }]);
return { chain, address, native, tokens };
const { trx, tokens } = await trxBalance(address);
const decimalsMap = Object.fromEntries(getTrxTokens().map((t) => [t.symbol, t.decimals]));
return {
chain, address,
native: fmt(trx, nativeDecimals),
tokens: fmtTokens(tokens, decimalsMap),
};
}
case 'BSC':
case 'ETH': {
const { native, tokens } = await evmBalance(ETH_RPC, address, [{ symbol: 'USDT', addr: USDT_ERC20 }]);
return { chain, address, native, tokens };
const tokenList = getEvmTokens(chain);
const rpc = chain === 'BSC' ? BSC_RPC : ETH_RPC;
const { native, tokens } = await evmBalance(
rpc,
address,
tokenList.map((t) => ({ symbol: t.symbol, addr: t.contractAddress })),
);
const decimalsMap = Object.fromEntries(tokenList.map((t) => [t.symbol, t.decimals]));
return {
chain, address,
native: fmt(native, nativeDecimals),
tokens: fmtTokens(tokens, decimalsMap),
};
}
case 'SOL': {
const { native, tokens } = await solBalance(address);
const decimalsMap = Object.fromEntries(getSolTokens().map((t) => [t.symbol, t.decimals]));
return {
chain, address,
native: fmt(native, nativeDecimals),
tokens: fmtTokens(tokens, decimalsMap),
};
}
case 'SOL':
return { chain, address, native: await solBalance(address) };
}
}
@@ -63,29 +135,40 @@ async function btcBalance(address: string): Promise<string> {
return sat.toString();
}
async function trxBalance(address: string): Promise<{ trx: string; usdt: string }> {
async function trxBalance(address: string): Promise<{ trx: string; tokens: Record<string, string> }> {
const headers: Record<string, string> = { Accept: 'application/json' };
if (env.tronApiKey) headers['TRON-PRO-API-KEY'] = env.tronApiKey;
const accRes = await fetchJson(`${TRONGRID}/v1/accounts/${address}`, { headers });
const trx = accRes.data?.[0]?.balance ? String(accRes.data[0].balance) : '0';
// USDT TRC20 balance
const usdtRes = await fetchJson(`${TRONGRID}/wallet/triggerconstantcontract`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
owner_address: address,
contract_address: USDT_TRC20,
function_selector: 'balanceOf(address)',
parameter: tronAddressToHex(address).padStart(64, '0'),
visible: true,
}),
});
const usdtHex = usdtRes.constant_result?.[0];
const usdt = usdtHex && !/^0+$/.test(usdtHex) ? BigInt('0x' + usdtHex).toString() : '0';
// Parallel TRC20 balanceOf для всех зарегистрированных токенов
const tokens: Record<string, string> = {};
const addrHex = tronAddressToHex(address).padStart(64, '0');
return { trx, usdt };
await Promise.all(
getTrxTokens().map(async ({ symbol, contractAddress }) => {
try {
const res = await fetchJson(`${TRONGRID}/wallet/triggerconstantcontract`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
owner_address: address,
contract_address: contractAddress,
function_selector: 'balanceOf(address)',
parameter: addrHex,
visible: true,
}),
});
const hex = res.constant_result?.[0];
tokens[symbol] = hex && !/^0+$/.test(hex) ? BigInt('0x' + hex).toString() : '0';
} catch {
tokens[symbol] = '0';
}
}),
);
return { trx, tokens };
}
async function evmBalance(
@@ -112,8 +195,9 @@ async function evmBalance(
return { native: native.toString(), tokens: tokenBalances };
}
async function solBalance(address: string): Promise<string> {
const res = await fetchJson(SOL_RPC, {
async function solBalance(address: string): Promise<{ native: string; tokens: Record<string, string> }> {
// 1) Native SOL balance
const nativeRes = await fetchJson(SOL_RPC, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -123,7 +207,48 @@ async function solBalance(address: string): Promise<string> {
params: [address],
}),
});
return String(res.result?.value ?? 0);
const native = String(nativeRes.result?.value ?? 0);
// 2) Все SPL token accounts юзера одним запросом
const tokens: Record<string, string> = {};
const knownMints = new Map(getSolTokens().map((t) => [t.mint, t.symbol]));
// Сразу инициализируем все известные символы нулём (чтобы output был consistent)
for (const [, sym] of knownMints) tokens[sym] = '0';
try {
const splRes = await fetchJson(SOL_RPC, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getTokenAccountsByOwner',
params: [
address,
{ programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' }, // SPL Token program
{ encoding: 'jsonParsed' },
],
}),
});
const accounts = splRes.result?.value || [];
for (const acc of accounts) {
const info = acc?.account?.data?.parsed?.info;
const mint = info?.mint;
const amount = info?.tokenAmount?.amount;
if (mint && amount && knownMints.has(mint)) {
const symbol = knownMints.get(mint)!;
// Суммируем если несколько token accounts для одного mint
const prev = BigInt(tokens[symbol] || '0');
tokens[symbol] = (prev + BigInt(amount)).toString();
}
}
} catch {
// SOL RPC недоступен — оставляем нули
}
return { native, tokens };
}
// ─────────────────────── TRANSACTIONS ───────────────────────