/** * JWT ↔ wallet-address binding. * * Защита от drain-вектора: проксируемые endpoint'ы (swap-build, relay-quote) принимают * `userAddress`/`recipient` как body param. Если не привязать к JWT-юзеру — * authenticated user A может set `userAddress=` и: * - swap output идёт к B (бесплатный slippage steal) * - reentrancy: B's address — malicious contract, callback дрянит * - bridge `recipient=attacker` → victim signs → funds to attacker * * Этот хелпер находит wallet user'а по chain и проверяет match. */ import { WalletModel } from '../models/wallet.model'; import type { ChainCode } from './address-validators'; /** * Throws if address ≠ user's wallet для данного chain. * Returns the canonical (DB-stored) address для использования вместо user input. * * Сравнение case-insensitive только для EVM (где checksum mixed-case = same address). * Для BTC/TRX/SOL — strict equality (base58/bech32 case-sensitive). */ export async function assertUserOwnsAddress( userId: string, chain: ChainCode, candidateAddress: string, ): Promise { const wallet = await WalletModel.findByUserAndChain(userId, chain); if (!wallet) { throw new Error(`No ${chain} wallet for user — POST /wallets/create first`); } const isEvm = chain === 'ETH' || chain === 'BSC'; const dbAddr = wallet.address; const candidate = String(candidateAddress ?? '').trim(); const match = isEvm ? candidate.toLowerCase() === dbAddr.toLowerCase() : candidate === dbAddr; if (!match) { throw new Error(`Address ${candidate} does not match user's ${chain} wallet ${dbAddr}`); } return dbAddr; }