initiukyghv

This commit is contained in:
ZOMBIIIIIII
2026-06-23 22:09:30 +03:00
parent 489d56ad36
commit 32a642b924
3 changed files with 107 additions and 5 deletions

View File

@@ -21,11 +21,43 @@ import { WalletModel } from '../models/wallet.model';
import type { ChainCode } from '../lib/address-validators'; import type { ChainCode } from '../lib/address-validators';
import { proxiedFetch } from '../lib/outbound-proxy'; import { proxiedFetch } from '../lib/outbound-proxy';
import { getTokensForChains } from '../lib/token-registry'; import { getTokensForChains } from '../lib/token-registry';
import { computeAppFee } from '../lib/app-fee';
import { formatSmallestUnits } from '../lib/amount-units';
const router = Router(); const router = Router();
const LIFI_API_URL = 'https://li.quest/v1'; const LIFI_API_URL = 'https://li.quest/v1';
const LIFI_TIMEOUT_MS = 20_000; 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: * LiFi chainIds → наш ChainCode. LiFi использует custom IDs для не-EVM:
* - SOL: 1151111081099710 (КАРДИНАЛЬНО отличается от Relay's 792703809) * - SOL: 1151111081099710 (КАРДИНАЛЬНО отличается от Relay's 792703809)
@@ -213,6 +245,17 @@ async function proxyJumperRequest(req: Request, res: Response, _next: NextFuncti
} }
try { 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); const filtered = filterJumperMetadata(jumperPath, text);
if (filtered) { if (filtered) {
res.json(filtered); res.json(filtered);
@@ -426,7 +469,7 @@ async function handleQuoteBest(req: Request, res: Response): Promise<void> {
// Step 1 — NearIntents only. // Step 1 — NearIntents only.
const nearRes = await tryLiFiQuote({ key: 'allowBridges', value: 'near' }); const nearRes = await tryLiFiQuote({ key: 'allowBridges', value: 'near' });
if (nearRes.ok && nearRes.body && (nearRes.body.estimate || nearRes.body.action)) { 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; return;
} }
@@ -435,7 +478,7 @@ async function handleQuoteBest(req: Request, res: Response): Promise<void> {
// Step 2 — fallback на любой best route. // Step 2 — fallback на любой best route.
const bestRes = await tryLiFiQuote(); const bestRes = await tryLiFiQuote();
if (bestRes.ok && bestRes.body && (bestRes.body.estimate || bestRes.body.action)) { 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; return;
} }

View File

@@ -5,12 +5,40 @@ import { WalletModel } from '../models/wallet.model';
import type { ChainCode } from '../lib/address-validators'; import type { ChainCode } from '../lib/address-validators';
import { indexRelayExecuteResponse } from '../lib/relay-trusted-cache'; import { indexRelayExecuteResponse } from '../lib/relay-trusted-cache';
import { proxiedFetch } from '../lib/outbound-proxy'; 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 router = Router();
const RELAY_API_URL = 'https://api.relay.link'; const RELAY_API_URL = 'https://api.relay.link';
const RELAY_TIMEOUT_MS = 20_000; 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. // chainId → ChainCode. Relay использует EVM chainIds + custom большие для не-EVM.
const RELAY_CHAINID_TO_CHAIN: Record<number, ChainCode> = { const RELAY_CHAINID_TO_CHAIN: Record<number, ChainCode> = {
1: 'ETH', 1: 'ETH',
@@ -235,6 +263,8 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction
app: fees.app ?? null, app: fees.app ?? null,
total: { amountUsd: totalUsd }, total: { amountUsd: totalUsd },
}, },
// Наша app-комиссия 0.7% в токене + USDT (информативно; собирается отдельной /app-fee tx).
appCommission: computeRelayAppCommission(full),
rate: full?.details?.rate ?? null, rate: full?.details?.rate ?? null,
priceImpactPct: full?.details?.totalImpact?.percent ?? null, priceImpactPct: full?.details?.totalImpact?.percent ?? null,
priceImpactUsd: full?.details?.totalImpact?.usd ?? null, priceImpactUsd: full?.details?.totalImpact?.usd ?? null,
@@ -250,6 +280,19 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction
return; 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, иначе обернём // Send raw text если это валидный JSON, иначе обернём
try { try {
res.send(text); res.send(text);

View File

@@ -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": { "rate": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -2862,7 +2878,7 @@
}, },
"responses": { "responses": {
"200": { "200": {
"description": "Quote с steps[], fees, details, breakdown" "description": "Quote с steps[], fees, details, breakdown + appCommission (наша 0.7% в токене+USDT, информативно — считается из details.currencyIn)"
}, },
"502": { "502": {
"description": "Relay upstream error (приложен upstream JSON для деталей)" "description": "Relay upstream error (приложен upstream JSON для деталей)"
@@ -3921,7 +3937,7 @@
], ],
"responses": { "responses": {
"200": { "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": { "400": {
"description": "Missing required params" "description": "Missing required params"