initkjbnkhj
This commit is contained in:
@@ -1057,12 +1057,18 @@ export const WalletController = {
|
||||
amount: raw.networkFee.amount,
|
||||
amountFormatted: fmtUnits(raw.networkFee.amount, networkFeeAssetDecimals),
|
||||
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: raw.appFee ? {
|
||||
asset: raw.appFee.asset,
|
||||
amount: raw.appFee.amount,
|
||||
amountFormatted: fmtUnits(raw.appFee.amount, raw.fromDecimals),
|
||||
// USDT-эквивалент 0.7% (комиссия уже в токене через amountFormatted).
|
||||
amountUsd: toUsd(raw.appFee.amount, raw.fromDecimals, fromPriceUsd),
|
||||
recipient: raw.appFee.recipient,
|
||||
} : null,
|
||||
// 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';
|
||||
}
|
||||
|
||||
// USD enrichment fee asset
|
||||
// USD enrichment: сетевой fee-asset + входной токен (для эквивалента в валюте свапа + USDT нашей 0.7%)
|
||||
const inputSymbol = raw.appFee?.asset ?? null; // символ входного токена (= asset нашей 0.7%)
|
||||
const priceMap = await getPricesBySymbols([
|
||||
{ chain, symbol: feeAssetSymbolForPrice },
|
||||
...(inputSymbol ? [{ chain, symbol: inputSymbol }] : []),
|
||||
]).catch(() => new Map<string, number | 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
|
||||
: raw.networkFee.asset === 'SOL' ? 9
|
||||
: 18;
|
||||
const amountFormatted = formatSmallestUnits(raw.networkFee.amount, feeAssetDecimals);
|
||||
let amountUsd: number | null = null;
|
||||
if (feePriceUsd != null) {
|
||||
try {
|
||||
const big = BigInt(raw.networkFee.amount);
|
||||
const divisor = 10n ** BigInt(feeAssetDecimals);
|
||||
const whole = Number(big / divisor);
|
||||
const frac = Number(big % divisor) / Number(divisor);
|
||||
amountUsd = (whole + frac) * feePriceUsd;
|
||||
} catch { amountUsd = null; }
|
||||
}
|
||||
const amountUsd = usdFromRaw(raw.networkFee.amount, feeAssetDecimals, feePriceUsd);
|
||||
// Сетевая комиссия в эквиваленте входного токена (валюты свапа).
|
||||
const networkInInputToken = (amountUsd != null && inputPriceUsd != null && inputPriceUsd > 0)
|
||||
? trimNum(amountUsd / inputPriceUsd) : null;
|
||||
// Наша 0.7% в USDT (в токене — formatSmallestUnits ниже).
|
||||
const appFeeUsd = raw.appFee ? usdFromRaw(raw.appFee.amount, raw.fromDecimals, inputPriceUsd) : null;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -1453,7 +1467,15 @@ export const WalletController = {
|
||||
amount: raw.networkFee.amount,
|
||||
amountFormatted,
|
||||
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 },
|
||||
route: raw.route,
|
||||
approveRequired: raw.approveRequired,
|
||||
|
||||
@@ -293,8 +293,10 @@ export function getCoingeckoId(chain: ChainCode, symbol: string): string | null
|
||||
const upper = String(symbol || '').toUpperCase();
|
||||
if (!upper) return null;
|
||||
|
||||
// Native — symbol === chain code (BTC, ETH, ...).
|
||||
if (upper === chain) return NATIVE_COINGECKO_IDS[chain] ?? null;
|
||||
// Native — symbol === chain code (BTC/ETH/SOL/TRX) ИЛИ символ нативной монеты.
|
||||
// На 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') {
|
||||
const t = getEvmTokens(chain).find((x) => x.symbol.toUpperCase() === upper);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '@solana/web3.js';
|
||||
import { getAppFeeWallet, computeAppFee } from '../lib/app-fee';
|
||||
import { formatSmallestUnits } from '../lib/amount-units';
|
||||
import { getPricesBySymbols } from './price-oracle.service';
|
||||
import { signAndBroadcastSolBuilt } from './wallet-signer.service';
|
||||
import { StakingError } from './staking.service';
|
||||
import { resolveStakeValidator } from '../lib/sol-validator';
|
||||
@@ -47,6 +48,7 @@ export interface SolStakeQuote {
|
||||
amountLamportsHuman: string;
|
||||
appFeeLamports: string;
|
||||
appFeeLamportsHuman: string;
|
||||
appFeeUsd: number | null; // наша 0.7% в USDT (≈USD); null если цена SOL недоступна
|
||||
stakeLamports: string;
|
||||
stakeLamportsHuman: string;
|
||||
validator: string;
|
||||
@@ -62,11 +64,17 @@ export async function quoteSolStake(amountLamports: string): Promise<SolStakeQuo
|
||||
const stake = BigInt(amountLamports) - fee;
|
||||
if (stake <= 0n) throw badRequest('amount too small after 0.7% fee');
|
||||
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 {
|
||||
amountLamports,
|
||||
amountLamportsHuman: sol(amountLamports),
|
||||
appFeeLamports: fee.toString(),
|
||||
appFeeLamportsHuman: sol(fee.toString()),
|
||||
appFeeLamportsHuman: appFeeHuman,
|
||||
appFeeUsd,
|
||||
stakeLamports: stake.toString(),
|
||||
stakeLamportsHuman: sol(stake.toString()),
|
||||
validator,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '../lib/defi-contracts';
|
||||
import { computeAppFee } from '../lib/app-fee';
|
||||
import { formatSmallestUnits } from '../lib/amount-units';
|
||||
import { getPricesBySymbols } from './price-oracle.service';
|
||||
import { signAndBroadcastEvmFeeTx, signAndBroadcastRawEvm } from './wallet-signer.service';
|
||||
import { signAndBroadcastEvmApprove } from './wallet-signer-bridge';
|
||||
|
||||
@@ -52,6 +53,7 @@ export interface LidoQuote {
|
||||
amountWeiHuman: string;
|
||||
appFeeWei: string;
|
||||
appFeeWeiHuman: string;
|
||||
appFeeUsd: number | null; // наша 0.7% в USDT (≈USD); null если цена ETH недоступна
|
||||
stakeAmountWei: string;
|
||||
stakeAmountWeiHuman: string;
|
||||
expectedStEthWei: string;
|
||||
@@ -68,11 +70,17 @@ export async function quoteLidoStake(amountWei: string): Promise<LidoQuote> {
|
||||
const stakeAmount = BigInt(amountWei) - fee;
|
||||
if (stakeAmount <= 0n) throw badRequest('amount too small after 0.7% fee');
|
||||
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 {
|
||||
amountWei,
|
||||
amountWeiHuman: eth(amountWei),
|
||||
appFeeWei: fee.toString(),
|
||||
appFeeWeiHuman: eth(fee.toString()),
|
||||
appFeeWeiHuman: appFeeHuman,
|
||||
appFeeUsd,
|
||||
stakeAmountWei: stakeAmount.toString(),
|
||||
stakeAmountWeiHuman: eth(stakeAmount.toString()),
|
||||
// Lido минтит stETH ~1:1 к внесённому ETH (после fee)
|
||||
|
||||
@@ -56,6 +56,12 @@ function badRequest(msg: string): StakingError {
|
||||
|
||||
const MINT_GAS_LIMIT = '600000';
|
||||
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 MAX_SLIPPAGE_BPS = 500n; // 5% — верхняя граница (дефолт 1%)
|
||||
const SLIPPAGE_DENOM = 10000n;
|
||||
@@ -220,6 +226,13 @@ export interface AddQuote {
|
||||
appFee1Usd: number | null;
|
||||
appFeeTotalUsd: number | null;
|
||||
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) ──
|
||||
balance?: {
|
||||
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);
|
||||
} 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 fee1Usd = price1 != null ? roundUsd(Number(fee1Human) * price1) : 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,
|
||||
appFeeTotalUsd,
|
||||
appFeeTotalEth,
|
||||
networkFee: {
|
||||
estGasUnits: estGas.toString(),
|
||||
gasPriceGwei,
|
||||
amountEthHuman: networkFeeEthHuman,
|
||||
amountUsd: networkFeeUsd,
|
||||
},
|
||||
...(balance ? { balance } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -670,56 +670,30 @@
|
||||
"network": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"asset": {
|
||||
"type": "string",
|
||||
"example": "BNB"
|
||||
},
|
||||
"amount": {
|
||||
"type": "string",
|
||||
"example": "65000000000000"
|
||||
},
|
||||
"amountFormatted": {
|
||||
"type": "string",
|
||||
"example": "0.000065"
|
||||
},
|
||||
"amountUsd": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"example": 0.04
|
||||
}
|
||||
"asset": { "type": "string", "example": "BNB" },
|
||||
"amount": { "type": "string", "example": "65000000000000" },
|
||||
"amountFormatted": { "type": "string", "example": "0.000065", "description": "Сетевая комиссия в native-монете" },
|
||||
"amountUsd": { "type": "number", "nullable": true, "example": 0.04 },
|
||||
"amountInInputToken": { "type": "string", "nullable": true, "example": "0.04", "description": "Та же сетевая комиссия в эквиваленте валюты свапа (входного токена)" },
|
||||
"inputTokenSymbol": { "type": "string", "example": "USDT", "description": "Символ входного токена (валюты свапа)" }
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amountUsd": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"example": 0.04
|
||||
}
|
||||
"amountUsd": { "type": "number", "nullable": true, "example": 0.04 }
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"asset": {
|
||||
"type": "string",
|
||||
"example": "BNB"
|
||||
},
|
||||
"amount": {
|
||||
"type": "string",
|
||||
"example": "70000000000000"
|
||||
},
|
||||
"amountFormatted": {
|
||||
"type": "string",
|
||||
"example": "0.00007"
|
||||
},
|
||||
"recipient": {
|
||||
"type": "string",
|
||||
"example": "0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718"
|
||||
}
|
||||
"asset": { "type": "string", "example": "USDT", "description": "Токен комиссии (= входной токен)" },
|
||||
"amount": { "type": "string", "example": "70000000000000" },
|
||||
"amountFormatted": { "type": "string", "example": "0.07", "description": "Комиссия 0.7% в токене" },
|
||||
"amountUsd": { "type": "number", "nullable": true, "example": 0.07, "description": "Та же 0.7% в USDT-эквиваленте" },
|
||||
"recipient": { "type": "string", "example": "0xeb9fbf0d137ef5ea7b9959044c2ed44ec1206c68" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -818,21 +792,25 @@
|
||||
},
|
||||
"fee": {
|
||||
"type": "object",
|
||||
"description": "Сетевая (gas) комиссия",
|
||||
"properties": {
|
||||
"asset": {
|
||||
"type": "string",
|
||||
"example": "BNB"
|
||||
},
|
||||
"amount": {
|
||||
"type": "string"
|
||||
},
|
||||
"amountFormatted": {
|
||||
"type": "string"
|
||||
},
|
||||
"amountUsd": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
}
|
||||
"asset": { "type": "string", "example": "BNB" },
|
||||
"amount": { "type": "string" },
|
||||
"amountFormatted": { "type": "string", "description": "в native-монете" },
|
||||
"amountUsd": { "type": "number", "nullable": true },
|
||||
"amountInInputToken": { "type": "string", "nullable": true, "description": "та же сетевая комиссия в эквиваленте валюты свапа (входного токена)" },
|
||||
"inputTokenSymbol": { "type": "string", "nullable": true, "example": "USDT" }
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -1154,7 +1132,13 @@
|
||||
"type": "string"
|
||||
},
|
||||
"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": {
|
||||
"type": "string"
|
||||
@@ -1463,7 +1447,17 @@
|
||||
"appFee0Usd": { "type": "number", "nullable": true },
|
||||
"appFee1Usd": { "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": {
|
||||
"type": "object",
|
||||
"description": "есть только если у юзера есть ETH-кошелёк",
|
||||
|
||||
Reference in New Issue
Block a user