44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
/**
|
||
* JWT ↔ wallet-address binding.
|
||
*
|
||
* Защита от drain-вектора: проксируемые endpoint'ы (swap-build, relay-quote) принимают
|
||
* `userAddress`/`recipient` как body param. Если не привязать к JWT-юзеру —
|
||
* authenticated user A может set `userAddress=<user B's addr>` и:
|
||
* - 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<string> {
|
||
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;
|
||
}
|