88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
/**
|
||
* Chain-specific address validators с CHECKSUM проверкой.
|
||
* Принципиально: regex/length недостаточно — TRX/BTC используют base58check,
|
||
* один испорченный символ может пройти regex, но кошелёк по такому адресу
|
||
* не восстановим → funds permanently lost.
|
||
*/
|
||
import { ethers } from 'ethers';
|
||
import { createHash } from 'crypto';
|
||
import bs58 from 'bs58';
|
||
import * as bitcoin from 'bitcoinjs-lib';
|
||
import { PublicKey } from '@solana/web3.js';
|
||
|
||
const TRX_RE = /^T[1-9A-HJ-NP-Za-km-z]{33}$/;
|
||
|
||
export type ChainCode = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC';
|
||
|
||
export function isValidAddress(chain: ChainCode, address: string): boolean {
|
||
// Любой блокчейн-адрес помещается в ~64 chars. 256 был оверкилл и open vector
|
||
// для DoS (тратим CPU на bs58.decode 200-char garbage).
|
||
if (typeof address !== 'string' || address.length === 0 || address.length > 64) return false;
|
||
|
||
switch (chain) {
|
||
case 'BTC':
|
||
return isValidBtcAddress(address);
|
||
case 'TRX':
|
||
return isValidTrxAddress(address);
|
||
case 'ETH':
|
||
case 'BSC':
|
||
return ethers.utils.isAddress(address);
|
||
case 'SOL':
|
||
return isValidSolAddress(address);
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ── BTC: bitcoinjs-lib проверяет version byte + checksum (P2PKH/P2SH/bech32) ──
|
||
function isValidBtcAddress(address: string): boolean {
|
||
try {
|
||
bitcoin.address.toOutputScript(address, bitcoin.networks.bitcoin);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ── TRX: base58check + первый байт 0x41 ──
|
||
function isValidTrxAddress(address: string): boolean {
|
||
if (!TRX_RE.test(address)) return false;
|
||
let decoded: Uint8Array;
|
||
try {
|
||
decoded = bs58.decode(address);
|
||
} catch {
|
||
return false;
|
||
}
|
||
if (decoded.length !== 25) return false; // 1 prefix + 20 payload + 4 checksum
|
||
if (decoded[0] !== 0x41) return false; // TRX mainnet prefix
|
||
const payload = decoded.subarray(0, 21);
|
||
const checksum = decoded.subarray(21);
|
||
const h1 = createHash('sha256').update(payload).digest();
|
||
const h2 = createHash('sha256').update(h1).digest();
|
||
for (let i = 0; i < 4; i++) {
|
||
if (h2[i] !== checksum[i]) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ── SOL: реальное base58-декодирование через PublicKey ──
|
||
function isValidSolAddress(address: string): boolean {
|
||
try {
|
||
const pk = new PublicKey(address);
|
||
// PublicKey принимает 32-байтовое значение; isOnCurve дополнительный sanity
|
||
return pk.toBytes().length === 32;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export function isValidAmount(amount: string): boolean {
|
||
if (typeof amount !== 'string' || amount.length === 0 || amount.length > 78) return false;
|
||
if (!/^\d+$/.test(amount)) return false;
|
||
try {
|
||
return BigInt(amount) > 0n;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|