initjnjnj
This commit is contained in:
@@ -5,6 +5,7 @@ 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';
|
||||
|
||||
const router = Router();
|
||||
const RELAY_API_URL = 'https://api.relay.link';
|
||||
@@ -21,7 +22,9 @@ const RELAY_CHAINID_TO_CHAIN: Record<number, ChainCode> = {
|
||||
// Без него `req.path` после `/execute/` свободен → `/execute/../admin` обходит startsWith-check.
|
||||
// Relay API: `POST /quote` (раньше был `GET /quote/v2` — upstream сменили в 2025).
|
||||
const ALLOWED_GET_PATHS = new Set(['/intents/status/v3']);
|
||||
const ALLOWED_POST_PATHS = new Set(['/quote']);
|
||||
// `/cost-estimate` — LOCAL alias (not a Relay endpoint). Internally calls Relay /quote и
|
||||
// фильтрует response — отдаёт только fees + details (без steps[]).
|
||||
const ALLOWED_POST_PATHS = new Set(['/quote', '/cost-estimate']);
|
||||
const ALLOWED_EXECUTE_ACTIONS = new Set([
|
||||
'swap',
|
||||
'bridge',
|
||||
@@ -55,10 +58,14 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect local-only /cost-estimate endpoint — internally forwarded к Relay /quote,
|
||||
// response trimmed (без steps[]).
|
||||
const isCostEstimate = req.method === 'POST' && relayPath === '/cost-estimate';
|
||||
|
||||
// C16 — bind body.user / body.recipient to JWT user's wallet.
|
||||
// Без этого authenticated user может set recipient=attacker → Relay строит quote →
|
||||
// victim signs → bridge funds к attacker'у.
|
||||
if (req.method === 'POST' && (relayPath === '/quote' || relayPath.startsWith('/execute/'))) {
|
||||
if (req.method === 'POST' && (relayPath === '/quote' || relayPath === '/cost-estimate' || relayPath.startsWith('/execute/'))) {
|
||||
const userId = (req as any).auth?.userId;
|
||||
if (!userId) {
|
||||
res.status(401).json({ success: false, error: 'auth required' });
|
||||
@@ -114,7 +121,45 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction
|
||||
}
|
||||
}
|
||||
|
||||
const relayUrl = new URL(`${RELAY_API_URL}${relayPath}`);
|
||||
// ADDITIVE: amountHuman preprocessing для /quote, /cost-estimate, /execute/*.
|
||||
// Если body содержит amountHuman → разрешаем через originCurrency contract → decimals.
|
||||
// Старое поле `amount` (smallest units) продолжает работать unchanged.
|
||||
if (req.method === 'POST' &&
|
||||
(relayPath === '/quote' || relayPath === '/cost-estimate' || relayPath.startsWith('/execute/'))) {
|
||||
const body = req.body ?? {};
|
||||
const hasAmount = body.amount !== undefined && body.amount !== null && body.amount !== '';
|
||||
const hasAmountHuman = body.amountHuman !== undefined && body.amountHuman !== null && body.amountHuman !== '';
|
||||
if (hasAmount && hasAmountHuman) {
|
||||
res.status(400).json({ success: false, error: 'Use either "amount" or "amountHuman", not both' });
|
||||
return;
|
||||
}
|
||||
if (hasAmountHuman) {
|
||||
const originCurrency = String(body.originCurrency ?? '');
|
||||
const originChainId = Number(body.originChainId);
|
||||
const originChain = RELAY_CHAINID_TO_CHAIN[originChainId];
|
||||
if (!originChain) {
|
||||
res.status(400).json({ success: false, error: `originChainId ${originChainId} not in allowlist (needed для amountHuman → decimals)` });
|
||||
return;
|
||||
}
|
||||
const dec = getDecimalsByContract(originChain, originCurrency);
|
||||
if (dec == null) {
|
||||
res.status(400).json({ success: false, error: `Unknown originCurrency "${originCurrency}" — supply "amount" (smallest units) directly` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resolved = parseHumanAmount(String(body.amountHuman), dec);
|
||||
req.body.amount = resolved;
|
||||
delete req.body.amountHuman;
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ success: false, error: err.message });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map local /cost-estimate → real Relay /quote endpoint.
|
||||
const upstreamPath = isCostEstimate ? '/quote' : relayPath;
|
||||
const relayUrl = new URL(`${RELAY_API_URL}${upstreamPath}`);
|
||||
|
||||
Object.entries(req.query).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
@@ -169,6 +214,42 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction
|
||||
return;
|
||||
}
|
||||
|
||||
// /cost-estimate — trim response (только fees + details, без steps[]).
|
||||
if (isCostEstimate) {
|
||||
let trimmed: any;
|
||||
try {
|
||||
const full = JSON.parse(text);
|
||||
const fees = full?.fees ?? {};
|
||||
let totalUsd: number | null = 0;
|
||||
for (const k of ['gas', 'relayer', 'app']) {
|
||||
const u = Number(fees?.[k]?.amountUsd);
|
||||
if (Number.isFinite(u)) totalUsd += u;
|
||||
else { totalUsd = null; break; }
|
||||
}
|
||||
trimmed = {
|
||||
success: true,
|
||||
data: {
|
||||
fees: {
|
||||
gas: fees.gas ?? null,
|
||||
relayer: fees.relayer ?? null,
|
||||
app: fees.app ?? null,
|
||||
total: { amountUsd: totalUsd },
|
||||
},
|
||||
rate: full?.details?.rate ?? null,
|
||||
priceImpactPct: full?.details?.totalImpact?.percent ?? null,
|
||||
priceImpactUsd: full?.details?.totalImpact?.usd ?? null,
|
||||
timeEstimate: full?.details?.timeEstimate ?? null,
|
||||
currencyIn: full?.details?.currencyIn ?? null,
|
||||
currencyOut: full?.details?.currencyOut ?? null,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
trimmed = { success: false, error: 'Relay returned non-JSON for /cost-estimate' };
|
||||
}
|
||||
res.json(trimmed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send raw text если это валидный JSON, иначе обернём
|
||||
try {
|
||||
res.send(text);
|
||||
|
||||
@@ -13,11 +13,15 @@ router.get('/portfolio', WalletController.getPortfolio);
|
||||
router.get('/:chain/balance', WalletController.getChainBalance);
|
||||
router.get('/:chain/transactions', WalletController.getChainTransactions);
|
||||
router.get('/:chain/gas-suggestions', WalletController.getGasSuggestions);
|
||||
// IMPORTANT: more specific paths ДОЛЖНЫ быть зарегистрированы РАНЬШЕ — Express сматчит first.
|
||||
// /:chain/send/cost-estimate ПЕРЕД /:chain/send
|
||||
// /:chain/swap/quote ПЕРЕД /:chain/swap
|
||||
// /:chain/swap/cost-estimate ПЕРЕД /:chain/swap
|
||||
router.post('/:chain/send/cost-estimate', WalletController.estimateSendCost);
|
||||
router.post('/:chain/send', WalletController.sendFromChain);
|
||||
router.post('/:chain/sign-raw-evm-tx', WalletController.signRawEvmTx);
|
||||
// IMPORTANT: /:chain/swap/quote ДОЛЖЕН быть ПЕРЕД /:chain/swap чтобы Express
|
||||
// сматчил specific route раньше general'ного.
|
||||
router.post('/:chain/swap/quote', WalletController.quoteSwap);
|
||||
router.post('/:chain/swap/cost-estimate', WalletController.estimateSwapCost);
|
||||
router.post('/:chain/swap', WalletController.swapOnChain);
|
||||
router.post('/SOL/sign-and-broadcast-tx', WalletController.signSolanaTx);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user