import { validateSync } from 'class-validator'; import { MoneroNetwork } from '../../types/MoneroNetwork'; import { IsMoneroStandardAddress } from './isMoneroStandardAddress'; class TestDto { @IsMoneroStandardAddress() destinationAddress: string; } const pad = (prefix: string) => `${prefix}${'A'.repeat(95 - prefix.length)}`; const MAINNET_STANDARD_ADDRESS = '4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2684Rge'; const validateAddress = (value: string) => { const dto = Object.assign(new TestDto(), { destinationAddress: value }); return validateSync(dto); }; const accepts = (value: string) => { expect(validateAddress(value)).toHaveLength(0); }; const rejects = (value: string) => { expect(validateAddress(value).length).toBeGreaterThan(0); }; describe('IsMoneroStandardAddress', () => { const originalNetwork = process.env.MONERO_NETWORK; afterEach(() => { if (originalNetwork === undefined) { delete process.env.MONERO_NETWORK; } else { process.env.MONERO_NETWORK = originalNetwork; } }); describe('mainnet', () => { beforeEach(() => { process.env.MONERO_NETWORK = MoneroNetwork.Mainnet; }); it('accepts a real mainnet standard address', () => { accepts(MAINNET_STANDARD_ADDRESS); }); it('accepts mainnet standard addresses', () => { accepts(pad('41')); accepts(pad('49')); accepts(pad('4A')); accepts(pad('4B')); }); it('accepts mainnet subaddresses', () => { accepts(pad('82')); accepts(pad('89')); accepts(pad('8A')); accepts(pad('8B')); accepts(pad('8C')); }); it('rejects stagenet addresses', () => { rejects(pad('51')); rejects(pad('72')); }); it('rejects testnet addresses', () => { rejects(pad('91')); rejects(pad('BY')); }); }); describe('stagenet', () => { beforeEach(() => { process.env.MONERO_NETWORK = MoneroNetwork.Stagenet; }); it('accepts stagenet standard addresses', () => { accepts(pad('51')); accepts(pad('59')); accepts(pad('5A')); accepts(pad('5B')); }); it('accepts stagenet subaddresses', () => { accepts(pad('72')); accepts(pad('79')); accepts(pad('7A')); accepts(pad('7B')); }); it('rejects mainnet addresses', () => { rejects(MAINNET_STANDARD_ADDRESS); rejects(pad('4A')); rejects(pad('82')); }); it('rejects testnet addresses', () => { rejects(pad('91')); rejects(pad('BY')); }); }); describe('shared validation rules', () => { beforeEach(() => { process.env.MONERO_NETWORK = MoneroNetwork.Mainnet; }); it('accepts lowercase base58 characters in the body', () => { accepts('4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgYeYTRj5UzqtReoS44qo9mtmXCqY45DJ852K5Jv2684Rge'); }); it('trims surrounding whitespace on mainnet', () => { accepts(` ${pad('4A')} `); }); it('trims surrounding whitespace on stagenet', () => { process.env.MONERO_NETWORK = MoneroNetwork.Stagenet; accepts(`\n${pad('5B')}\t`); }); it.each(['0', '1', '2', '3', '6', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G'])( 'rejects addresses starting with %s', prefix => { rejects(pad(`${prefix}A`)); } ); it.each(['0', 'C'])('rejects mainnet standard addresses with second character %s', second => { rejects(pad(`4${second}`)); }); it.each(['0', '1'])('rejects mainnet subaddresses with second character %s', second => { rejects(pad(`8${second}`)); }); it.each(['0', 'C'])('rejects stagenet standard addresses with second character %s', second => { process.env.MONERO_NETWORK = MoneroNetwork.Stagenet; rejects(pad(`5${second}`)); }); it.each(['0', '1', 'C'])('rejects stagenet subaddresses with second character %s', second => { process.env.MONERO_NETWORK = MoneroNetwork.Stagenet; rejects(pad(`7${second}`)); }); it('rejects empty input', () => { rejects(''); }); it('rejects whitespace-only input', () => { rejects(' '); }); it('rejects addresses that are too short', () => { rejects(pad('4A').slice(0, 94)); }); it('rejects addresses that are too long', () => { rejects(`${pad('4A')}A`); }); it('rejects integrated addresses (106 chars, also start with 4 on mainnet)', () => { rejects(`${MAINNET_STANDARD_ADDRESS}${'A'.repeat(11)}`); }); it.each(['0', 'O', 'I', 'l'])('rejects addresses containing %s', invalidChar => { const address = `${pad('4A').slice(0, 10)}${invalidChar}${pad('4A').slice(11)}`; rejects(address); }); it('returns a network-specific error message', () => { const [error] = validateAddress('invalid'); expect(error.constraints?.isMoneroStandardAddress).toBe('Enter a valid mainnet Monero address.'); }); }); });