From 2beebb49689a2969b2cd7dc7cf4f85d41470ff2d Mon Sep 17 00:00:00 2001 From: ZOMBIIIIIII <120676065+Metaton241@users.noreply.github.com> Date: Fri, 26 Jun 2026 18:46:21 +0300 Subject: [PATCH] inithipk; --- apps/api/src/lib/outbound-proxy.ts | 20 ++++-- apps/api/src/routes/jumper-proxy.routes.ts | 8 ++- apps/api/src/routes/relay-proxy.routes.ts | 4 +- apps/api/swagger.json | 72 +++++++++++++++++++++- 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/apps/api/src/lib/outbound-proxy.ts b/apps/api/src/lib/outbound-proxy.ts index 2ef8659..a3e94ba 100644 --- a/apps/api/src/lib/outbound-proxy.ts +++ b/apps/api/src/lib/outbound-proxy.ts @@ -71,10 +71,22 @@ export async function proxiedFetch( } // undici.fetch поддерживает `dispatcher` для per-call routing через ProxyAgent. // Возвращаемый тип Response совместим с native — приводим через unknown для TS. - return undiciFetch(input as any, { - ...(init as any), - dispatcher: agent, - }) as unknown as Response; + try { + return (await undiciFetch(input as any, { + ...(init as any), + dispatcher: agent, + })) as unknown as Response; + } catch (err: any) { + // Таймаут/отмена (AbortSignal) — это НЕ отказ прокси, fallback не нужен (сохраняем 504-семантику). + if (err?.name === 'AbortError') throw err; + // Прокси недоступен (connect ECONNREFUSED / timeout до прокси) → самовосстановление: пробуем + // прямой выход. Relay/Jumper/Jupiter — публичные API, прямой fetch их достаёт, если у сервера + // есть direct egress. Срабатывает ТОЛЬКО при throw (HTTP-ошибка ОТ прокси не бросает — это + // обычный Response с !ok). Идемпотентно: при живом прокси сюда не заходим, поведение прежнее. + const reason = err?.cause?.code ?? err?.code ?? err?.name ?? 'unknown'; + logger.warn(`Outbound proxy failed (${reason}) — fallback на прямой fetch`); + return fetch(input as any, init as any); + } } /** diff --git a/apps/api/src/routes/jumper-proxy.routes.ts b/apps/api/src/routes/jumper-proxy.routes.ts index 48bae41..9b9926f 100644 --- a/apps/api/src/routes/jumper-proxy.routes.ts +++ b/apps/api/src/routes/jumper-proxy.routes.ts @@ -272,7 +272,9 @@ async function proxyJumperRequest(req: Request, res: Response, _next: NextFuncti return; } logger.error(`Jumper proxy failed: ${error?.stack || error?.message}`); - res.status(502).json({ success: false, error: 'Jumper proxy error' }); + // reason = код/имя сетевой ошибки (ECONNREFUSED/ENOTFOUND/UND_ERR_*) без хоста/IP/кредов прокси. + const reason = error?.cause?.code ?? error?.code ?? error?.name ?? 'unknown'; + res.status(502).json({ success: false, error: 'Jumper proxy error', reason }); } } @@ -494,6 +496,8 @@ async function handleQuoteBest(req: Request, res: Response): Promise { return; } logger.error(`handleQuoteBest failed: ${error?.stack || error?.message}`); - res.status(502).json({ success: false, error: 'Quote-best failed' }); + // reason = код/имя сетевой ошибки (ECONNREFUSED/ENOTFOUND/UND_ERR_*) без хоста/IP/кредов прокси. + const reason = error?.cause?.code ?? error?.code ?? error?.name ?? 'unknown'; + res.status(502).json({ success: false, error: 'Quote-best failed', reason }); } } diff --git a/apps/api/src/routes/relay-proxy.routes.ts b/apps/api/src/routes/relay-proxy.routes.ts index c8fc3f0..d172145 100644 --- a/apps/api/src/routes/relay-proxy.routes.ts +++ b/apps/api/src/routes/relay-proxy.routes.ts @@ -320,6 +320,8 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction return; } logger.error(`Relay proxy failed: ${error?.stack || error?.message}`); - res.status(502).json({ success: false, error: 'Relay proxy error' }); + // reason = код/имя сетевой ошибки (ECONNREFUSED/ENOTFOUND/UND_ERR_*) без хоста/IP/кредов прокси. + const reason = error?.cause?.code ?? error?.code ?? error?.name ?? 'unknown'; + res.status(502).json({ success: false, error: 'Relay proxy error', reason }); } } diff --git a/apps/api/swagger.json b/apps/api/swagger.json index 457ba30..78e61d2 100644 --- a/apps/api/swagger.json +++ b/apps/api/swagger.json @@ -3937,7 +3937,51 @@ ], "responses": { "200": { - "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 }" + "description": "LiFi quote (passthrough) + _source (near|best) + appCommission (наша 0.7%, информативно; сбор — атомарно в /bridge/execute)", + "content": { "application/json": { "schema": { + "type": "object", + "additionalProperties": true, + "description": "Сырой LiFi-quote + наши поля. Ниже задокументированы ключевые.", + "properties": { + "_source": { "type": "string", "enum": ["near", "best"], "description": "near = NearIntents, best = любой best route" }, + "tool": { "type": "string", "example": "near" }, + "toolDetails": { "type": "object", "additionalProperties": true }, + "estimate": { + "type": "object", + "additionalProperties": true, + "properties": { + "fromAmount": { "type": "string" }, + "fromAmountUSD": { "type": "string", "example": "10.03" }, + "toAmount": { "type": "string", "description": "сколько получишь (smallest units)" }, + "toAmountMin": { "type": "string", "description": "минимум с учётом slippage → передай как acceptedMinOut в /bridge/execute" }, + "toAmountUSD": { "type": "string" }, + "executionDuration": { "type": "number", "description": "оценка, сек" }, + "gasCosts": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, + "feeCosts": { "type": "array", "items": { "type": "object", "additionalProperties": true } } + } + }, + "action": { + "type": "object", + "additionalProperties": true, + "properties": { + "fromToken": { "type": "object", "additionalProperties": true, "properties": { "symbol": { "type": "string" }, "decimals": { "type": "integer" }, "address": { "type": "string" } } }, + "toToken": { "type": "object", "additionalProperties": true }, + "fromAmount": { "type": "string" } + } + }, + "transactionRequest": { "type": "object", "nullable": true, "additionalProperties": true, "description": "Готовая EVM-tx (to/data/value/chainId) — для ручной подписи через /wallets/{chain}/sign-raw-evm-tx" }, + "appCommission": { + "type": "object", + "nullable": true, + "description": "Наша комиссия 0.7% (в токене + USDT). Информативно; реально списывается в /bridge/execute. Считается из action.fromToken + action.fromAmount + estimate.fromAmountUSD.", + "properties": { + "ratePercent": { "type": "number", "example": 0.7 }, + "inToken": { "type": "object", "properties": { "symbol": { "type": "string", "example": "USDC" }, "amount": { "type": "string", "example": "0.07", "description": "0.7% во входном токене" } } }, + "usd": { "type": "number", "nullable": true, "example": 0.07, "description": "та же 0.7% в USDT (≈USD)" } + } + } + } + } } } }, "400": { "description": "Missing required params" @@ -3954,6 +3998,32 @@ } } }, + "/jumper/quote": { + "get": { + "summary": "LiFi single quote (passthrough; без NearIntents-приоритета)", + "description": "Прямой проброс на LiFi `/quote`. В отличие от `/jumper/quote-best` НЕ пробует NearIntents первым — отдаёт LiFi best route как есть. Ответ — сырой LiFi-quote + наше поле **appCommission** (наша 0.7% в токене + USDT, информативно; сбор — в /bridge/execute). Параметры те же, что у quote-best. Обычно используй quote-best.", + "tags": ["Bridge (Jumper / LiFi)"], + "parameters": [ + { "name": "fromChain", "in": "query", "required": true, "schema": { "type": "integer" }, "description": "LiFi chainId: ETH=1, BSC=56, SOL=1151111081099710, TRX=728126428, BTC=20000000000001" }, + { "name": "toChain", "in": "query", "required": true, "schema": { "type": "integer" } }, + { "name": "fromToken", "in": "query", "required": true, "schema": { "type": "string" }, "description": "contract / mint / native sentinel" }, + { "name": "toToken", "in": "query", "required": true, "schema": { "type": "string" } }, + { "name": "fromAmount", "in": "query", "required": true, "schema": { "type": "string" }, "description": "smallest units" }, + { "name": "fromAddress", "in": "query", "required": true, "schema": { "type": "string" }, "description": "должен совпадать с кошельком юзера для fromChain (JWT-bind)" }, + { "name": "toAddress", "in": "query", "required": false, "schema": { "type": "string" } }, + { "name": "slippage", "in": "query", "required": false, "schema": { "type": "number", "example": 0.005 } } + ], + "responses": { + "200": { + "description": "LiFi quote (passthrough) + appCommission (наша 0.7% в токене + USDT). Поля — как у /jumper/quote-best (estimate, action, transactionRequest, appCommission)." + }, + "400": { "description": "Missing required params" }, + "403": { "description": "fromAddress mismatch user wallet" }, + "502": { "description": "LiFi upstream / no route" }, + "504": { "description": "LiFi timeout" } + } + } + }, "/bridge/execute": { "post": { "summary": "One-click bridge execute (sign + broadcast)",