From 32a642b924266903711a94d45a4cdd078980b2b1 Mon Sep 17 00:00:00 2001 From: ZOMBIIIIIII <120676065+Metaton241@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:09:30 +0300 Subject: [PATCH] initiukyghv --- apps/api/src/routes/jumper-proxy.routes.ts | 47 +++++++++++++++++++++- apps/api/src/routes/relay-proxy.routes.ts | 45 ++++++++++++++++++++- apps/api/swagger.json | 20 ++++++++- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/apps/api/src/routes/jumper-proxy.routes.ts b/apps/api/src/routes/jumper-proxy.routes.ts index 11379f2..48bae41 100644 --- a/apps/api/src/routes/jumper-proxy.routes.ts +++ b/apps/api/src/routes/jumper-proxy.routes.ts @@ -21,11 +21,43 @@ import { WalletModel } from '../models/wallet.model'; import type { ChainCode } from '../lib/address-validators'; import { proxiedFetch } from '../lib/outbound-proxy'; import { getTokensForChains } from '../lib/token-registry'; +import { computeAppFee } from '../lib/app-fee'; +import { formatSmallestUnits } from '../lib/amount-units'; const router = Router(); const LIFI_API_URL = 'https://li.quest/v1'; const LIFI_TIMEOUT_MS = 20_000; +/** + * Вычисляет нашу app-комиссию 0.7% (в токене + USDT) из LiFi-ответа (action.fromToken + fromAmount, + * estimate.fromAmountUSD). Чисто информативно (показать в quote) — сбор не трогаем (0.7% берётся + * атомарно в /bridge/execute), поэтому LiFi fee-параметры НЕ инжектим (иначе двойное списание). + */ +function computeJumperAppCommission(body: any): { + ratePercent: number; + inToken: { symbol: string | null; amount: string | null }; + usd: number | null; +} | null { + const act = body?.action; + const est = body?.estimate; + const fromToken = act?.fromToken; + const fromAmount = act?.fromAmount ?? est?.fromAmount; + if (!fromToken || fromAmount == null) return null; + let amountToken: string | null = null; + try { + const dec = Number(fromToken.decimals ?? 0); + const feeSmallest = computeAppFee(String(fromAmount)); // fromAmount × 0.7% + amountToken = formatSmallestUnits(feeSmallest.toString(), dec); + } catch { amountToken = null; } + const fromUsd = Number(est?.fromAmountUSD); + const usd = Number.isFinite(fromUsd) ? Math.round(fromUsd * 0.007 * 1e6) / 1e6 : null; + return { + ratePercent: 0.7, + inToken: { symbol: fromToken.symbol ?? null, amount: amountToken }, + usd, + }; +} + /** * LiFi chainIds → наш ChainCode. LiFi использует custom IDs для не-EVM: * - SOL: 1151111081099710 (КАРДИНАЛЬНО отличается от Relay's 792703809) @@ -213,6 +245,17 @@ async function proxyJumperRequest(req: Request, res: Response, _next: NextFuncti } try { + // /quote — доинжектим вычисляемую appCommission (наша 0.7% в токене + USDT) для видимости. + if (jumperPath === '/quote') { + try { + const obj = JSON.parse(text); + obj.appCommission = computeJumperAppCommission(obj); + res.send(JSON.stringify(obj)); + } catch { + res.send(text); // не JSON — passthrough + } + return; + } const filtered = filterJumperMetadata(jumperPath, text); if (filtered) { res.json(filtered); @@ -426,7 +469,7 @@ async function handleQuoteBest(req: Request, res: Response): Promise { // Step 1 — NearIntents only. const nearRes = await tryLiFiQuote({ key: 'allowBridges', value: 'near' }); if (nearRes.ok && nearRes.body && (nearRes.body.estimate || nearRes.body.action)) { - res.status(200).json({ ...nearRes.body, _source: 'near' }); + res.status(200).json({ ...nearRes.body, _source: 'near', appCommission: computeJumperAppCommission(nearRes.body) }); return; } @@ -435,7 +478,7 @@ async function handleQuoteBest(req: Request, res: Response): Promise { // Step 2 — fallback на любой best route. const bestRes = await tryLiFiQuote(); if (bestRes.ok && bestRes.body && (bestRes.body.estimate || bestRes.body.action)) { - res.status(200).json({ ...bestRes.body, _source: 'best' }); + res.status(200).json({ ...bestRes.body, _source: 'best', appCommission: computeJumperAppCommission(bestRes.body) }); return; } diff --git a/apps/api/src/routes/relay-proxy.routes.ts b/apps/api/src/routes/relay-proxy.routes.ts index 7552d09..c8fc3f0 100644 --- a/apps/api/src/routes/relay-proxy.routes.ts +++ b/apps/api/src/routes/relay-proxy.routes.ts @@ -5,12 +5,40 @@ import { WalletModel } from '../models/wallet.model'; import type { ChainCode } from '../lib/address-validators'; import { indexRelayExecuteResponse } from '../lib/relay-trusted-cache'; import { proxiedFetch } from '../lib/outbound-proxy'; -import { getDecimalsByContract, parseHumanAmount } from '../lib/amount-units'; +import { getDecimalsByContract, parseHumanAmount, formatSmallestUnits } from '../lib/amount-units'; +import { computeAppFee } from '../lib/app-fee'; const router = Router(); const RELAY_API_URL = 'https://api.relay.link'; const RELAY_TIMEOUT_MS = 20_000; +/** + * Вычисляет нашу app-комиссию 0.7% (в токене + USDT) из Relay-ответа `details.currencyIn`. + * Чисто информативно (показать в quote/cost-estimate) — сбор комиссии не трогаем (отдельная /app-fee tx), + * поэтому НЕ инжектим Relay appFees (иначе было бы двойное списание). + */ +function computeRelayAppCommission(full: any): { + ratePercent: number; + inToken: { symbol: string | null; amount: string | null }; + usd: number | null; +} | null { + const cIn = full?.details?.currencyIn; + if (!cIn || cIn.amount == null) return null; + let amountToken: string | null = null; + try { + const dec = Number(cIn.currency?.decimals ?? 0); + const feeSmallest = computeAppFee(String(cIn.amount)); // amount × 0.7% + amountToken = formatSmallestUnits(feeSmallest.toString(), dec); + } catch { amountToken = null; } + const inUsd = Number(cIn.amountUsd); + const usd = Number.isFinite(inUsd) ? Math.round(inUsd * 0.007 * 1e6) / 1e6 : null; + return { + ratePercent: 0.7, + inToken: { symbol: cIn.currency?.symbol ?? null, amount: amountToken }, + usd, + }; +} + // chainId → ChainCode. Relay использует EVM chainIds + custom большие для не-EVM. const RELAY_CHAINID_TO_CHAIN: Record = { 1: 'ETH', @@ -235,6 +263,8 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction app: fees.app ?? null, total: { amountUsd: totalUsd }, }, + // Наша app-комиссия 0.7% в токене + USDT (информативно; собирается отдельной /app-fee tx). + appCommission: computeRelayAppCommission(full), rate: full?.details?.rate ?? null, priceImpactPct: full?.details?.totalImpact?.percent ?? null, priceImpactUsd: full?.details?.totalImpact?.usd ?? null, @@ -250,6 +280,19 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction return; } + // /quote — доинжектим вычисляемую appCommission (наша 0.7% в токене + USDT) для видимости в Swagger. + // Остальное (steps[], fees, details) проходит без изменений. Сбор комиссии не меняется. + if (relayPath === '/quote') { + try { + const obj = JSON.parse(text); + obj.appCommission = computeRelayAppCommission(obj); + res.send(JSON.stringify(obj)); + } catch { + res.send(text); // не JSON — passthrough как есть + } + return; + } + // Send raw text если это валидный JSON, иначе обернём try { res.send(text); diff --git a/apps/api/swagger.json b/apps/api/swagger.json index 006ee02..457ba30 100644 --- a/apps/api/swagger.json +++ b/apps/api/swagger.json @@ -885,6 +885,22 @@ } } }, + "appCommission": { + "type": "object", + "nullable": true, + "description": "Наша app-комиссия 0.7% (в токене + USDT). Информативно; собирается отдельной /app-fee tx. Считается из details.currencyIn. Есть и в /relay/quote.", + "properties": { + "ratePercent": { "type": "number", "example": 0.7 }, + "inToken": { + "type": "object", + "properties": { + "symbol": { "type": "string", "nullable": true, "example": "USDT" }, + "amount": { "type": "string", "nullable": true, "example": "0.7", "description": "0.7% во входном токене" } + } + }, + "usd": { "type": "number", "nullable": true, "example": 0.7, "description": "та же 0.7% в USDT (≈USD)" } + } + }, "rate": { "type": "string", "nullable": true @@ -2862,7 +2878,7 @@ }, "responses": { "200": { - "description": "Quote с steps[], fees, details, breakdown" + "description": "Quote с steps[], fees, details, breakdown + appCommission (наша 0.7% в токене+USDT, информативно — считается из details.currencyIn)" }, "502": { "description": "Relay upstream error (приложен upstream JSON для деталей)" @@ -3921,7 +3937,7 @@ ], "responses": { "200": { - "description": "LiFi quote + _source field (near|best)" + "description": "LiFi quote + _source (near|best) + appCommission (наша 0.7% в токене + USDT; информативно, считается из action.fromToken/estimate.fromAmountUSD; сбор — атомарно в /bridge/execute). Пример appCommission: { ratePercent: 0.7, inToken: { symbol: \"USDT\", amount: \"0.7\" }, usd: 0.7 }" }, "400": { "description": "Missing required params"