inithipk;
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<void> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Reference in New Issue
Block a user