initkjbnkhj

This commit is contained in:
ZOMBIIIIIII
2026-06-23 21:30:54 +03:00
parent 6f01f4af34
commit 489d56ad36
6 changed files with 142 additions and 70 deletions

View File

@@ -1057,12 +1057,18 @@ export const WalletController = {
amount: raw.networkFee.amount, amount: raw.networkFee.amount,
amountFormatted: fmtUnits(raw.networkFee.amount, networkFeeAssetDecimals), amountFormatted: fmtUnits(raw.networkFee.amount, networkFeeAssetDecimals),
amountUsd: networkFeeUsd, amountUsd: networkFeeUsd,
// Сетевая комиссия в эквиваленте валюты свапа (входного токена) + символ.
amountInInputToken: (networkFeeUsd != null && fromPriceUsd != null && fromPriceUsd > 0)
? trimNum(networkFeeUsd / fromPriceUsd) : null,
inputTokenSymbol: fromSymbol,
}, },
// App fee 0.7% (BSC only) — сервер шлёт это через off-chain double-tx ПЕРЕД swap. // App fee 0.7% (BSC only) — сервер шлёт это через off-chain double-tx ПЕРЕД swap.
app: raw.appFee ? { app: raw.appFee ? {
asset: raw.appFee.asset, asset: raw.appFee.asset,
amount: raw.appFee.amount, amount: raw.appFee.amount,
amountFormatted: fmtUnits(raw.appFee.amount, raw.fromDecimals), amountFormatted: fmtUnits(raw.appFee.amount, raw.fromDecimals),
// USDT-эквивалент 0.7% (комиссия уже в токене через amountFormatted).
amountUsd: toUsd(raw.appFee.amount, raw.fromDecimals, fromPriceUsd),
recipient: raw.appFee.recipient, recipient: raw.appFee.recipient,
} : null, } : null,
// dex fee включён в expectedOut (Pancake 0.25%, SunSwap 0.3%+0.7% fee router, Jupiter platform varied). // dex fee включён в expectedOut (Pancake 0.25%, SunSwap 0.3%+0.7% fee router, Jupiter platform varied).
@@ -1423,26 +1429,34 @@ export const WalletController = {
feeAssetSymbolForPrice = 'SOL'; feeAssetSymbolForPrice = 'SOL';
} }
// USD enrichment fee asset // USD enrichment: сетевой fee-asset + входной токен (для эквивалента в валюте свапа + USDT нашей 0.7%)
const inputSymbol = raw.appFee?.asset ?? null; // символ входного токена (= asset нашей 0.7%)
const priceMap = await getPricesBySymbols([ const priceMap = await getPricesBySymbols([
{ chain, symbol: feeAssetSymbolForPrice }, { chain, symbol: feeAssetSymbolForPrice },
...(inputSymbol ? [{ chain, symbol: inputSymbol }] : []),
]).catch(() => new Map<string, number | null>()); ]).catch(() => new Map<string, number | null>());
const feePriceUsd = priceMap.get(`${chain}:${feeAssetSymbolForPrice}`) ?? null; const feePriceUsd = priceMap.get(`${chain}:${feeAssetSymbolForPrice}`) ?? null;
const inputPriceUsd = inputSymbol ? (priceMap.get(`${chain}:${inputSymbol}`) ?? null) : null;
const usdFromRaw = (rawAmt: string, dec: number, price: number | null): number | null => {
if (price == null) return null;
try {
const big = BigInt(rawAmt);
const divisor = 10n ** BigInt(dec);
return (Number(big / divisor) + Number(big % divisor) / Number(divisor)) * price;
} catch { return null; }
};
const feeAssetDecimals = raw.networkFee.asset === 'TRX' ? 6 const feeAssetDecimals = raw.networkFee.asset === 'TRX' ? 6
: raw.networkFee.asset === 'SOL' ? 9 : raw.networkFee.asset === 'SOL' ? 9
: 18; : 18;
const amountFormatted = formatSmallestUnits(raw.networkFee.amount, feeAssetDecimals); const amountFormatted = formatSmallestUnits(raw.networkFee.amount, feeAssetDecimals);
let amountUsd: number | null = null; const amountUsd = usdFromRaw(raw.networkFee.amount, feeAssetDecimals, feePriceUsd);
if (feePriceUsd != null) { // Сетевая комиссия в эквиваленте входного токена (валюты свапа).
try { const networkInInputToken = (amountUsd != null && inputPriceUsd != null && inputPriceUsd > 0)
const big = BigInt(raw.networkFee.amount); ? trimNum(amountUsd / inputPriceUsd) : null;
const divisor = 10n ** BigInt(feeAssetDecimals); // Наша 0.7% в USDT (в токене — formatSmallestUnits ниже).
const whole = Number(big / divisor); const appFeeUsd = raw.appFee ? usdFromRaw(raw.appFee.amount, raw.fromDecimals, inputPriceUsd) : null;
const frac = Number(big % divisor) / Number(divisor);
amountUsd = (whole + frac) * feePriceUsd;
} catch { amountUsd = null; }
}
res.json({ res.json({
success: true, success: true,
@@ -1453,7 +1467,15 @@ export const WalletController = {
amount: raw.networkFee.amount, amount: raw.networkFee.amount,
amountFormatted, amountFormatted,
amountUsd, amountUsd,
amountInInputToken: networkInInputToken,
inputTokenSymbol: inputSymbol,
}, },
appFee: raw.appFee ? {
asset: raw.appFee.asset,
amount: raw.appFee.amount,
amountFormatted: formatSmallestUnits(raw.appFee.amount, raw.fromDecimals),
amountUsd: appFeeUsd,
} : null,
total: { amountUsd }, total: { amountUsd },
route: raw.route, route: raw.route,
approveRequired: raw.approveRequired, approveRequired: raw.approveRequired,

View File

@@ -293,8 +293,10 @@ export function getCoingeckoId(chain: ChainCode, symbol: string): string | null
const upper = String(symbol || '').toUpperCase(); const upper = String(symbol || '').toUpperCase();
if (!upper) return null; if (!upper) return null;
// Native — symbol === chain code (BTC, ETH, ...). // Native — symbol === chain code (BTC/ETH/SOL/TRX) ИЛИ символ нативной монеты.
if (upper === chain) return NATIVE_COINGECKO_IDS[chain] ?? null; // На BSC нативка = BNB (символ ≠ chain code 'BSC') — без этой ветки цена BNB не резолвилась
// (→ сетевая/0.7% комиссия в BNB приходила без USD). NATIVE_SYMBOLS[BSC]='BNB'.
if (upper === chain || upper === NATIVE_SYMBOLS[chain]) return NATIVE_COINGECKO_IDS[chain] ?? null;
if (chain === 'ETH' || chain === 'BSC') { if (chain === 'ETH' || chain === 'BSC') {
const t = getEvmTokens(chain).find((x) => x.symbol.toUpperCase() === upper); const t = getEvmTokens(chain).find((x) => x.symbol.toUpperCase() === upper);

View File

@@ -22,6 +22,7 @@ import {
} from '@solana/web3.js'; } from '@solana/web3.js';
import { getAppFeeWallet, computeAppFee } from '../lib/app-fee'; import { getAppFeeWallet, computeAppFee } from '../lib/app-fee';
import { formatSmallestUnits } from '../lib/amount-units'; import { formatSmallestUnits } from '../lib/amount-units';
import { getPricesBySymbols } from './price-oracle.service';
import { signAndBroadcastSolBuilt } from './wallet-signer.service'; import { signAndBroadcastSolBuilt } from './wallet-signer.service';
import { StakingError } from './staking.service'; import { StakingError } from './staking.service';
import { resolveStakeValidator } from '../lib/sol-validator'; import { resolveStakeValidator } from '../lib/sol-validator';
@@ -47,6 +48,7 @@ export interface SolStakeQuote {
amountLamportsHuman: string; amountLamportsHuman: string;
appFeeLamports: string; appFeeLamports: string;
appFeeLamportsHuman: string; appFeeLamportsHuman: string;
appFeeUsd: number | null; // наша 0.7% в USDT (≈USD); null если цена SOL недоступна
stakeLamports: string; stakeLamports: string;
stakeLamportsHuman: string; stakeLamportsHuman: string;
validator: string; validator: string;
@@ -62,11 +64,17 @@ export async function quoteSolStake(amountLamports: string): Promise<SolStakeQuo
const stake = BigInt(amountLamports) - fee; const stake = BigInt(amountLamports) - fee;
if (stake <= 0n) throw badRequest('amount too small after 0.7% fee'); if (stake <= 0n) throw badRequest('amount too small after 0.7% fee');
const validator = await resolveStakeValidator(getConn()); const validator = await resolveStakeValidator(getConn());
// USDT-эквивалент комиссии (≈USD). Цена SOL через whitelist; null если недоступна.
let solPrice: number | null = null;
try { solPrice = (await getPricesBySymbols([{ chain: 'SOL', symbol: 'SOL' }])).get('SOL:SOL') ?? null; } catch { /* optional */ }
const appFeeHuman = sol(fee.toString());
const appFeeUsd = solPrice != null ? Math.round(Number(appFeeHuman) * solPrice * 1e6) / 1e6 : null;
return { return {
amountLamports, amountLamports,
amountLamportsHuman: sol(amountLamports), amountLamportsHuman: sol(amountLamports),
appFeeLamports: fee.toString(), appFeeLamports: fee.toString(),
appFeeLamportsHuman: sol(fee.toString()), appFeeLamportsHuman: appFeeHuman,
appFeeUsd,
stakeLamports: stake.toString(), stakeLamports: stake.toString(),
stakeLamportsHuman: sol(stake.toString()), stakeLamportsHuman: sol(stake.toString()),
validator, validator,

View File

@@ -23,6 +23,7 @@ import {
} from '../lib/defi-contracts'; } from '../lib/defi-contracts';
import { computeAppFee } from '../lib/app-fee'; import { computeAppFee } from '../lib/app-fee';
import { formatSmallestUnits } from '../lib/amount-units'; import { formatSmallestUnits } from '../lib/amount-units';
import { getPricesBySymbols } from './price-oracle.service';
import { signAndBroadcastEvmFeeTx, signAndBroadcastRawEvm } from './wallet-signer.service'; import { signAndBroadcastEvmFeeTx, signAndBroadcastRawEvm } from './wallet-signer.service';
import { signAndBroadcastEvmApprove } from './wallet-signer-bridge'; import { signAndBroadcastEvmApprove } from './wallet-signer-bridge';
@@ -52,6 +53,7 @@ export interface LidoQuote {
amountWeiHuman: string; amountWeiHuman: string;
appFeeWei: string; appFeeWei: string;
appFeeWeiHuman: string; appFeeWeiHuman: string;
appFeeUsd: number | null; // наша 0.7% в USDT (≈USD); null если цена ETH недоступна
stakeAmountWei: string; stakeAmountWei: string;
stakeAmountWeiHuman: string; stakeAmountWeiHuman: string;
expectedStEthWei: string; expectedStEthWei: string;
@@ -68,11 +70,17 @@ export async function quoteLidoStake(amountWei: string): Promise<LidoQuote> {
const stakeAmount = BigInt(amountWei) - fee; const stakeAmount = BigInt(amountWei) - fee;
if (stakeAmount <= 0n) throw badRequest('amount too small after 0.7% fee'); if (stakeAmount <= 0n) throw badRequest('amount too small after 0.7% fee');
const apr = await fetchLidoApr(); const apr = await fetchLidoApr();
// USDT-эквивалент комиссии (≈USD). Цена ETH через whitelist; null если недоступна.
let ethPrice: number | null = null;
try { ethPrice = (await getPricesBySymbols([{ chain: 'ETH', symbol: 'ETH' }])).get('ETH:ETH') ?? null; } catch { /* optional */ }
const appFeeHuman = eth(fee.toString());
const appFeeUsd = ethPrice != null ? Math.round(Number(appFeeHuman) * ethPrice * 1e6) / 1e6 : null;
return { return {
amountWei, amountWei,
amountWeiHuman: eth(amountWei), amountWeiHuman: eth(amountWei),
appFeeWei: fee.toString(), appFeeWei: fee.toString(),
appFeeWeiHuman: eth(fee.toString()), appFeeWeiHuman: appFeeHuman,
appFeeUsd,
stakeAmountWei: stakeAmount.toString(), stakeAmountWei: stakeAmount.toString(),
stakeAmountWeiHuman: eth(stakeAmount.toString()), stakeAmountWeiHuman: eth(stakeAmount.toString()),
// Lido минтит stETH ~1:1 к внесённому ETH (после fee) // Lido минтит stETH ~1:1 к внесённому ETH (после fee)

View File

@@ -56,6 +56,12 @@ function badRequest(msg: string): StakingError {
const MINT_GAS_LIMIT = '600000'; const MINT_GAS_LIMIT = '600000';
const REMOVE_GAS_LIMIT = '500000'; const REMOVE_GAS_LIMIT = '500000';
// Оценочные gas-лимиты шагов LP-депозита (для networkFee в quote). Соответствуют реальным:
// mint=MINT_GAS_LIMIT, approve≈APPROVE_GAS_LIMIT, fee≈ERC20 transfer, wrap≈WETH.deposit.
const EST_GAS_MINT = 600_000n;
const EST_GAS_APPROVE = 80_000n;
const EST_GAS_FEE = 65_000n;
const EST_GAS_WRAP = 60_000n;
const DEFAULT_SLIPPAGE_BPS = 100n; // 1% const DEFAULT_SLIPPAGE_BPS = 100n; // 1%
const MAX_SLIPPAGE_BPS = 500n; // 5% — верхняя граница (дефолт 1%) const MAX_SLIPPAGE_BPS = 500n; // 5% — верхняя граница (дефолт 1%)
const SLIPPAGE_DENOM = 10000n; const SLIPPAGE_DENOM = 10000n;
@@ -220,6 +226,13 @@ export interface AddQuote {
appFee1Usd: number | null; appFee1Usd: number | null;
appFeeTotalUsd: number | null; appFeeTotalUsd: number | null;
appFeeTotalEth: string | null; // комиссия в монете сети (ETH-эквивалент) appFeeTotalEth: string | null; // комиссия в монете сети (ETH-эквивалент)
// ── Оценка сетевой (gas) комиссии всего LP-депозита (wrap?+fee×ноги+approve×ноги+mint) ──
networkFee: {
estGasUnits: string;
gasPriceGwei: string | null;
amountEthHuman: string | null; // оценка газа в ETH
amountUsd: number | null; // та же оценка в USD
};
// ── Проверка баланса (только если передан ownerAddress) ── // ── Проверка баланса (только если передан ownerAddress) ──
balance?: { balance?: {
token0: LegBalance; token0: LegBalance;
@@ -328,6 +341,25 @@ export async function quoteAdd(p: AddParams): Promise<AddQuote> {
price1 = isWeth(pool.token1) ? ethPrice : (priceMap.get(`ETH:${pool.sym1}`) ?? null); price1 = isWeth(pool.token1) ? ethPrice : (priceMap.get(`ETH:${pool.sym1}`) ?? null);
} catch { /* prices optional */ } } catch { /* prices optional */ }
// ── Оценка сетевой (gas) комиссии: суммируем gas-лимиты шагов, которые реально пойдут ──
const t0IsWethN = isWeth(pool.token0);
const t1IsWethN = isWeth(pool.token1);
let estGas = EST_GAS_MINT; // mint всегда
if (in0 > 0n) estGas += EST_GAS_APPROVE + EST_GAS_FEE; // нога token0: approve + fee
if (in1 > 0n) estGas += EST_GAS_APPROVE + EST_GAS_FEE; // нога token1
if (p.useNativeEth && (t0IsWethN || t1IsWethN)) estGas += EST_GAS_WRAP; // авто-wrap ETH→WETH
let gasPriceWei = 0n;
try {
const fd = await provider.getFeeData();
const gp = fd.maxFeePerGas ?? fd.gasPrice ?? null;
if (gp) gasPriceWei = BigInt(gp.toString());
} catch { /* gas price optional */ }
const networkFeeWei = estGas * gasPriceWei;
const networkFeeEthHuman = gasPriceWei > 0n ? formatSmallestUnits(networkFeeWei.toString(), 18) : null;
const networkFeeUsd = (networkFeeEthHuman != null && ethPrice != null)
? roundUsd(Number(networkFeeEthHuman) * ethPrice) : null;
const gasPriceGwei = gasPriceWei > 0n ? formatSmallestUnits(gasPriceWei.toString(), 9) : null;
const fee0Usd = price0 != null ? roundUsd(Number(fee0Human) * price0) : null; const fee0Usd = price0 != null ? roundUsd(Number(fee0Human) * price0) : null;
const fee1Usd = price1 != null ? roundUsd(Number(fee1Human) * price1) : null; const fee1Usd = price1 != null ? roundUsd(Number(fee1Human) * price1) : null;
const usdParts = [fee0Usd, fee1Usd].filter((v): v is number => v != null); const usdParts = [fee0Usd, fee1Usd].filter((v): v is number => v != null);
@@ -405,6 +437,12 @@ export async function quoteAdd(p: AddParams): Promise<AddQuote> {
appFee1Usd: fee1Usd, appFee1Usd: fee1Usd,
appFeeTotalUsd, appFeeTotalUsd,
appFeeTotalEth, appFeeTotalEth,
networkFee: {
estGasUnits: estGas.toString(),
gasPriceGwei,
amountEthHuman: networkFeeEthHuman,
amountUsd: networkFeeUsd,
},
...(balance ? { balance } : {}), ...(balance ? { balance } : {}),
}; };
} }

View File

@@ -670,56 +670,30 @@
"network": { "network": {
"type": "object", "type": "object",
"properties": { "properties": {
"asset": { "asset": { "type": "string", "example": "BNB" },
"type": "string", "amount": { "type": "string", "example": "65000000000000" },
"example": "BNB" "amountFormatted": { "type": "string", "example": "0.000065", "description": "Сетевая комиссия в native-монете" },
}, "amountUsd": { "type": "number", "nullable": true, "example": 0.04 },
"amount": { "amountInInputToken": { "type": "string", "nullable": true, "example": "0.04", "description": "Та же сетевая комиссия в эквиваленте валюты свапа (входного токена)" },
"type": "string", "inputTokenSymbol": { "type": "string", "example": "USDT", "description": "Символ входного токена (валюты свапа)" }
"example": "65000000000000"
},
"amountFormatted": {
"type": "string",
"example": "0.000065"
},
"amountUsd": {
"type": "number",
"nullable": true,
"example": 0.04
}
} }
}, },
"total": { "total": {
"type": "object", "type": "object",
"properties": { "properties": {
"amountUsd": { "amountUsd": { "type": "number", "nullable": true, "example": 0.04 }
"type": "number",
"nullable": true,
"example": 0.04
}
} }
}, },
"app": { "app": {
"type": "object", "type": "object",
"nullable": true, "nullable": true,
"description": "App fee 0.7% (BSC only). Server sends this to BSC_FEE_WALLET via separate tx BEFORE swap.", "description": "Наша комиссия 0.7%. Списывается отдельной tx ПЕРЕД свапом.",
"properties": { "properties": {
"asset": { "asset": { "type": "string", "example": "USDT", "description": "Токен комиссии (= входной токен)" },
"type": "string", "amount": { "type": "string", "example": "70000000000000" },
"example": "BNB" "amountFormatted": { "type": "string", "example": "0.07", "description": "Комиссия 0.7% в токене" },
}, "amountUsd": { "type": "number", "nullable": true, "example": 0.07, "description": "Та же 0.7% в USDT-эквиваленте" },
"amount": { "recipient": { "type": "string", "example": "0xeb9fbf0d137ef5ea7b9959044c2ed44ec1206c68" }
"type": "string",
"example": "70000000000000"
},
"amountFormatted": {
"type": "string",
"example": "0.00007"
},
"recipient": {
"type": "string",
"example": "0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718"
}
} }
} }
} }
@@ -818,21 +792,25 @@
}, },
"fee": { "fee": {
"type": "object", "type": "object",
"description": "Сетевая (gas) комиссия",
"properties": { "properties": {
"asset": { "asset": { "type": "string", "example": "BNB" },
"type": "string", "amount": { "type": "string" },
"example": "BNB" "amountFormatted": { "type": "string", "description": "в native-монете" },
}, "amountUsd": { "type": "number", "nullable": true },
"amount": { "amountInInputToken": { "type": "string", "nullable": true, "description": "та же сетевая комиссия в эквиваленте валюты свапа (входного токена)" },
"type": "string" "inputTokenSymbol": { "type": "string", "nullable": true, "example": "USDT" }
},
"amountFormatted": {
"type": "string"
},
"amountUsd": {
"type": "number",
"nullable": true
} }
},
"appFee": {
"type": "object",
"nullable": true,
"description": "Наша комиссия 0.7% в токене + USDT",
"properties": {
"asset": { "type": "string", "example": "USDT" },
"amount": { "type": "string" },
"amountFormatted": { "type": "string", "description": "0.7% в токене" },
"amountUsd": { "type": "number", "nullable": true, "description": "та же 0.7% в USDT" }
} }
}, },
"total": { "total": {
@@ -1154,7 +1132,13 @@
"type": "string" "type": "string"
}, },
"appFeeWeiHuman": { "appFeeWeiHuman": {
"type": "string" "type": "string",
"description": "наша 0.7% в токене (ETH). SOL-ответ: appFeeLamportsHuman"
},
"appFeeUsd": {
"type": "number",
"nullable": true,
"description": "наша 0.7% в USDT (≈USD). Есть и в SOL-ответе (appFeeUsd). null если цена недоступна"
}, },
"stakeAmountWei": { "stakeAmountWei": {
"type": "string" "type": "string"
@@ -1463,7 +1447,17 @@
"appFee0Usd": { "type": "number", "nullable": true }, "appFee0Usd": { "type": "number", "nullable": true },
"appFee1Usd": { "type": "number", "nullable": true }, "appFee1Usd": { "type": "number", "nullable": true },
"appFeeTotalUsd": { "type": "number", "nullable": true }, "appFeeTotalUsd": { "type": "number", "nullable": true },
"appFeeTotalEth": { "type": "string", "nullable": true, "description": "суммарная комиссия в монете сети (ETH)" }, "appFeeTotalEth": { "type": "string", "nullable": true, "description": "суммарная наша 0.7% в монете сети (ETH)" },
"networkFee": {
"type": "object",
"description": "Оценка сетевой (gas) комиссии всего LP-депозита (wrap?+fee×ноги+approve×ноги+mint) при текущем gasPrice",
"properties": {
"estGasUnits": { "type": "string", "example": "950000" },
"gasPriceGwei": { "type": "string", "nullable": true, "example": "5.2" },
"amountEthHuman": { "type": "string", "nullable": true, "example": "0.00494", "description": "оценка газа в ETH" },
"amountUsd": { "type": "number", "nullable": true, "example": 12.3, "description": "та же оценка в USD" }
}
},
"balance": { "balance": {
"type": "object", "type": "object",
"description": "есть только если у юзера есть ETH-кошелёк", "description": "есть только если у юзера есть ETH-кошелёк",