initrftsebfvgyhloutersvbhustdr
This commit is contained in:
@@ -10,6 +10,7 @@ import { authMiddleware } from './middleware/auth';
|
|||||||
import { csrfMiddleware } from './middleware/csrf';
|
import { csrfMiddleware } from './middleware/csrf';
|
||||||
import { globalLimiter, mutateLimiter, sensitiveLimiter, mnemonicRevealLimiter } from './middleware/rate-limit';
|
import { globalLimiter, mutateLimiter, sensitiveLimiter, mnemonicRevealLimiter } from './middleware/rate-limit';
|
||||||
import { errorHandler } from './middleware/error-handler';
|
import { errorHandler } from './middleware/error-handler';
|
||||||
|
import { generateCsrfToken } from './services/csrf.service';
|
||||||
import walletRoutes from './routes/wallet.routes';
|
import walletRoutes from './routes/wallet.routes';
|
||||||
import relayProxyRoutes from './routes/relay-proxy.routes';
|
import relayProxyRoutes from './routes/relay-proxy.routes';
|
||||||
import jumperProxyRoutes from './routes/jumper-proxy.routes';
|
import jumperProxyRoutes from './routes/jumper-proxy.routes';
|
||||||
@@ -41,6 +42,7 @@ app.use(
|
|||||||
origin: corsIsWildcard ? '*' : (corsOrigins.length > 0 ? corsOrigins : false),
|
origin: corsIsWildcard ? '*' : (corsOrigins.length > 0 ? corsOrigins : false),
|
||||||
// Wildcard incompatible с credentials per browser spec — force false при wildcard.
|
// Wildcard incompatible с credentials per browser spec — force false при wildcard.
|
||||||
credentials: corsIsWildcard ? false : env.cors.allowCredentials,
|
credentials: corsIsWildcard ? false : env.cors.allowCredentials,
|
||||||
|
exposedHeaders: ['X-CSRF-Token', 'X-Trace-ID'],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(express.json({ limit: '64kb' })); // защита от больших payload-DoS
|
app.use(express.json({ limit: '64kb' })); // защита от больших payload-DoS
|
||||||
@@ -65,6 +67,30 @@ app.get('/api/health', async (_req, res) => {
|
|||||||
// ── Глобальный rate limit на /api/* — ДО docs чтобы не было unauthenticated DoS на swagger.json
|
// ── Глобальный rate limit на /api/* — ДО docs чтобы не было unauthenticated DoS на swagger.json
|
||||||
app.use('/api', globalLimiter);
|
app.use('/api', globalLimiter);
|
||||||
|
|
||||||
|
app.get('/api/csrf', (_req, res) => {
|
||||||
|
if (!env.vault.csrfPath) {
|
||||||
|
res.json({ success: true, data: { enabled: false, csrfToken: null } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { token, maxAgeSec } = generateCsrfToken();
|
||||||
|
const crossSiteCookie = !corsIsWildcard && env.cors.allowCredentials;
|
||||||
|
res.cookie('csrf_token', token, {
|
||||||
|
httpOnly: false,
|
||||||
|
secure: crossSiteCookie,
|
||||||
|
sameSite: crossSiteCookie ? 'none' : 'lax',
|
||||||
|
maxAge: maxAgeSec * 1000,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
|
res.setHeader('X-CSRF-Token', token);
|
||||||
|
res.json({ success: true, data: { enabled: true, csrfToken: token, maxAgeSec } });
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(503).json({ success: false, error: err.message || 'CSRF token unavailable' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// H1 — Swagger gated. В production требуется basic-auth ИЛИ NODE_ENV != production.
|
// H1 — Swagger gated. В production требуется basic-auth ИЛИ NODE_ENV != production.
|
||||||
// JSON endpoint ОБЯЗАТЕЛЬНО до app.use('/api/docs', ...) — иначе swagger-ui-express
|
// JSON endpoint ОБЯЗАТЕЛЬНО до app.use('/api/docs', ...) — иначе swagger-ui-express
|
||||||
// перехватывает все /api/docs/* и возвращает HTML вместо JSON.
|
// перехватывает все /api/docs/* и возвращает HTML вместо JSON.
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ const ALLOWED_TRC_FUNCTIONS = new Set<string>([
|
|||||||
'allowance(address,address)',
|
'allowance(address,address)',
|
||||||
'swapExactETHForTokens(uint256,address[],address,uint256)',
|
'swapExactETHForTokens(uint256,address[],address,uint256)',
|
||||||
'swapExactTokensForETH(uint256,uint256,address[],address,uint256)',
|
'swapExactTokensForETH(uint256,uint256,address[],address,uint256)',
|
||||||
|
'swapExactTokensForETHSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256)',
|
||||||
'swapNativeWithFee(bytes)',
|
'swapNativeWithFee(bytes)',
|
||||||
'swapTokenWithFee(address,uint256,bytes)',
|
'swapTokenWithFee(address,uint256,bytes)',
|
||||||
'getAmountsOut(uint256,address[])',
|
'getAmountsOut(uint256,address[])',
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export function isCsrfConfigured(): boolean {
|
|||||||
return current !== null;
|
return current !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function b64urlEncode(buf: Buffer): string {
|
||||||
|
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pre-fetch CSRF config из Vault — НЕ мутирует глобал, возвращает новый объект.
|
* Pre-fetch CSRF config из Vault — НЕ мутирует глобал, возвращает новый объект.
|
||||||
*/
|
*/
|
||||||
@@ -95,11 +99,38 @@ function decodeTimestamp(encoded: string): number {
|
|||||||
return ts + ITSDANGEROUS_EPOCH;
|
return ts + ITSDANGEROUS_EPOCH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeTimestamp(unixSeconds: number): string {
|
||||||
|
let ts = unixSeconds - ITSDANGEROUS_EPOCH;
|
||||||
|
const bytes: number[] = [];
|
||||||
|
do {
|
||||||
|
bytes.unshift(ts & 0xff);
|
||||||
|
ts = Math.floor(ts / 256);
|
||||||
|
} while (ts > 0);
|
||||||
|
return b64urlEncode(Buffer.from(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
export interface CsrfVerifyResult {
|
export interface CsrfVerifyResult {
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateCsrfToken(): { token: string; maxAgeSec: number } {
|
||||||
|
if (!current) {
|
||||||
|
throw new Error('CSRF secret not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = b64urlEncode(crypto.randomBytes(32));
|
||||||
|
const timestamp = encodeTimestamp(Math.floor(Date.now() / 1000));
|
||||||
|
const payloadTs = `${payload}.${timestamp}`;
|
||||||
|
const derived = deriveKey(current.secret, current.salt, current.digest);
|
||||||
|
const signature = b64urlEncode(crypto.createHmac(current.digest, derived).update(payloadTs).digest());
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: `${payloadTs}.${signature}`,
|
||||||
|
maxAgeSec: current.maxAgeSec,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function verifyCsrfToken(token: string): CsrfVerifyResult {
|
export function verifyCsrfToken(token: string): CsrfVerifyResult {
|
||||||
if (!current) return { valid: false, reason: 'CSRF secret not loaded' };
|
if (!current) return { valid: false, reason: 'CSRF secret not loaded' };
|
||||||
if (!token || typeof token !== 'string') return { valid: false, reason: 'Empty token' };
|
if (!token || typeof token !== 'string') return { valid: false, reason: 'Empty token' };
|
||||||
|
|||||||
@@ -526,7 +526,7 @@ const TRX_BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrs
|
|||||||
// FeeSwapRouter работает с supporting-fee variant — это уже доказано в production traffic.
|
// FeeSwapRouter работает с supporting-fee variant — это уже доказано в production traffic.
|
||||||
const SEL_APPROVE = '095ea7b3';
|
const SEL_APPROVE = '095ea7b3';
|
||||||
const SEL_SWAP_EXACT_ETH_FOR_TOKENS = 'b6f9de95';
|
const SEL_SWAP_EXACT_ETH_FOR_TOKENS = 'b6f9de95';
|
||||||
const SEL_SWAP_EXACT_TOKENS_FOR_ETH = '18cbafe5';
|
const SEL_SWAP_EXACT_TOKENS_FOR_ETH_SUPPORTING_FEE = '791ac947';
|
||||||
const SEL_SWAP_NATIVE_WITH_FEE = '152dad1d';
|
const SEL_SWAP_NATIVE_WITH_FEE = '152dad1d';
|
||||||
const SEL_SWAP_TOKEN_WITH_FEE = 'e8d1f203';
|
const SEL_SWAP_TOKEN_WITH_FEE = 'e8d1f203';
|
||||||
|
|
||||||
@@ -617,8 +617,8 @@ function buildSwapExactETHForTokensCalldata(
|
|||||||
encAddr(to) + encU256(deadline) + pathLen + pathElements;
|
encAddr(to) + encU256(deadline) + pathLen + pathElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
// function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)
|
// function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)
|
||||||
function buildSwapExactTokensForETHCalldata(
|
function buildSwapExactTokensForETHSupportingFeeCalldata(
|
||||||
amountIn: bigint,
|
amountIn: bigint,
|
||||||
amountOutMin: bigint,
|
amountOutMin: bigint,
|
||||||
path: string[],
|
path: string[],
|
||||||
@@ -628,7 +628,7 @@ function buildSwapExactTokensForETHCalldata(
|
|||||||
const offsetToPath = encU256(160n); // 5 × 32 bytes
|
const offsetToPath = encU256(160n); // 5 × 32 bytes
|
||||||
const pathLen = encU256(BigInt(path.length));
|
const pathLen = encU256(BigInt(path.length));
|
||||||
const pathElements = path.map(encAddr).join('');
|
const pathElements = path.map(encAddr).join('');
|
||||||
return SEL_SWAP_EXACT_TOKENS_FOR_ETH + encU256(amountIn) + encU256(amountOutMin) +
|
return SEL_SWAP_EXACT_TOKENS_FOR_ETH_SUPPORTING_FEE + encU256(amountIn) + encU256(amountOutMin) +
|
||||||
offsetToPath + encAddr(to) + encU256(deadline) + pathLen + pathElements;
|
offsetToPath + encAddr(to) + encU256(deadline) + pathLen + pathElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,7 +936,7 @@ export async function quoteTrx(p: QuoteTrxParams): Promise<SwapQuoteRaw> {
|
|||||||
* Поддерживает только TRX↔USDT.
|
* Поддерживает только TRX↔USDT.
|
||||||
*
|
*
|
||||||
* TRX → USDT (1 tx): swapNativeWithFee wraps swapExactETHForTokens.
|
* TRX → USDT (1 tx): swapNativeWithFee wraps swapExactETHForTokens.
|
||||||
* USDT → TRX (1-2 tx): optional approve(infinite) + swapTokenWithFee wraps swapExactTokensForETH.
|
* USDT → TRX (1-2 tx): optional approve(infinite) + swapTokenWithFee wraps supporting-fee swapExactTokensForETH.
|
||||||
*
|
*
|
||||||
* Slippage: если `lockedMinOut` задан → используется. Иначе re-quote on-chain.
|
* Slippage: если `lockedMinOut` задан → используется. Иначе re-quote on-chain.
|
||||||
*/
|
*/
|
||||||
@@ -1059,7 +1059,7 @@ export async function executeTrx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: build swapTokenWithFee(USDT, amount, calldata).
|
// Step 2: build swapTokenWithFee(USDT, amount, calldata).
|
||||||
const sunswapCalldata = buildSwapExactTokensForETHCalldata(
|
const sunswapCalldata = buildSwapExactTokensForETHSupportingFeeCalldata(
|
||||||
swapAmount, amountOutMin, path, fromTronAddr, deadline,
|
swapAmount, amountOutMin, path, fromTronAddr, deadline,
|
||||||
);
|
);
|
||||||
const tokenInEnc = encAddr(USDT_CONTRACT);
|
const tokenInEnc = encAddr(USDT_CONTRACT);
|
||||||
|
|||||||
Reference in New Issue
Block a user