/**
* FILE: lib/address-validator.cjs
* STATUS: ACTIVE
* RUNTIME: Bare
* MODULE: CommonJS
*
* PURPOSE:
* Address validation with basic format checking
*
* USED BY:
* - wallet-server.cjs (transaction and contact validation)
*
* KEY EXPORTS:
* - validateAddress(address, chain, network) - Generic validator
* - validateBitcoinAddress(address, network) - Bitcoin with Base58Check/Bech32
* - validateEthereumAddress(address) - Ethereum format check only
*
* VALIDATION:
* - Bitcoin: Full checksum validation (Base58Check and Bech32/Bech32m)
* - Ethereum: Format check only (EIP-55 checksum disabled - Pear module resolution issues)
* - Returns: { valid: boolean, reason?: string, warning?: string }
*
* NOTE: EIP-55 checksum validation disabled due to Pear module resolution bug with @noble/hashes
*/
const crypto = require('bare-crypto')
/**
* Bitcoin Base58 alphabet (no 0, O, I, l to avoid confusion)
*/
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
/**
* Decode Base58 string to buffer
* @param {string} str - Base58 encoded string
* @returns {Buffer} Decoded bytes
*/
function base58Decode(str) {
const bytes = []
for (let i = 0; i < str.length; i++) {
const char = str[i]
let value = BASE58_ALPHABET.indexOf(char)
if (value === -1) {
throw new Error('Invalid Base58 character')
}
for (let j = 0; j < bytes.length; j++) {
value += bytes[j] * 58
bytes[j] = value & 0xff
value >>= 8
}
while (value > 0) {
bytes.push(value & 0xff)
value >>= 8
}
}
// Add leading zeros
for (let i = 0; i < str.length && str[i] === '1'; i++) {
bytes.push(0)
}
return Buffer.from(bytes.reverse())
}
/**
* Verify Base58Check checksum
* @param {Buffer} payload - Decoded Base58 bytes
* @returns {boolean} True if checksum valid
*/
function verifyBase58Check(payload) {
if (payload.length < 5) return false
const data = payload.slice(0, -4)
const checksum = payload.slice(-4)
// Double SHA-256 hash
const hash1 = crypto.createHash('sha256').update(data).digest()
const hash2 = crypto.createHash('sha256').update(hash1).digest()
// Compare first 4 bytes
return checksum.equals(hash2.slice(0, 4))
}
/**
* Bech32 character set
*/
const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
/**
* Bech32 polymod for checksum
*/
function bech32Polymod(values) {
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
let chk = 1
for (const value of values) {
const top = chk >> 25
chk = ((chk & 0x1ffffff) << 5) ^ value
for (let i = 0; i < 5; i++) {
if ((top >> i) & 1) {
chk ^= GEN[i]
}
}
}
return chk
}
/**
* Expand HRP (Human Readable Part) for Bech32
*/
function bech32HrpExpand(hrp) {
const result = []
for (let i = 0; i < hrp.length; i++) {
result.push(hrp.charCodeAt(i) >> 5)
}
result.push(0)
for (let i = 0; i < hrp.length; i++) {
result.push(hrp.charCodeAt(i) & 31)
}
return result
}
/**
* Verify Bech32 checksum (SegWit v0)
*/
function verifyBech32(hrp, data) {
return bech32Polymod(bech32HrpExpand(hrp).concat(data)) === 1
}
/**
* Verify Bech32m checksum (SegWit v1+)
*/
function verifyBech32m(hrp, data) {
return bech32Polymod(bech32HrpExpand(hrp).concat(data)) === 0x2bc830a3
}
/**
* Decode Bech32 address
*/
function bech32Decode(address) {
address = address.toLowerCase()
const pos = address.lastIndexOf('1')
if (pos < 1 || pos + 7 > address.length || address.length > 90) {
throw new Error('Invalid Bech32 address')
}
const hrp = address.slice(0, pos)
const data = []
for (let i = pos + 1; i < address.length; i++) {
const d = BECH32_CHARSET.indexOf(address[i])
if (d === -1) {
throw new Error('Invalid Bech32 character')
}
data.push(d)
}
return { hrp, data }
}
/**
* Validate Bitcoin address with checksum
* @param {string} address - Bitcoin address
* @param {string} network - 'testnet' or 'mainnet'
* @returns {Object} { valid: boolean, reason?: string }
*/
function validateBitcoinAddress(address, network = 'testnet') {
// P2PKH (1... or m...) and P2SH (3... or 2...)
if (address.match(/^[123mn][a-zA-HJ-NP-Z0-9]{25,34}$/)) {
try {
const decoded = base58Decode(address)
if (!verifyBase58Check(decoded)) {
return { valid: false, reason: 'Invalid Base58Check checksum' }
}
const version = decoded[0]
// Check version byte matches network
const validVersions = network === 'testnet' ? [0x6f, 0xc4] : [0x00, 0x05]
if (!validVersions.includes(version)) {
return { valid: false, reason: `Invalid network for address format (expected ${network})` }
}
return { valid: true }
} catch (err) {
return { valid: false, reason: `Base58 decode error: ${err.message}` }
}
}
// Bech32 (bc1... or tb1...)
if (address.match(/^(bc1|tb1)[a-z0-9]{39,87}$/)) {
try {
const prefix = network === 'testnet' ? 'tb' : 'bc'
if (!address.startsWith(prefix)) {
return { valid: false, reason: `Invalid network prefix (expected ${prefix}1...)` }
}
const { hrp, data } = bech32Decode(address)
if (hrp !== prefix) {
return { valid: false, reason: 'Network mismatch in HRP' }
}
// Try Bech32 (v0) first, then Bech32m (v1+)
const validBech32 = verifyBech32(hrp, data)
const validBech32m = verifyBech32m(hrp, data)
if (!validBech32 && !validBech32m) {
return { valid: false, reason: 'Invalid Bech32/Bech32m checksum' }
}
return { valid: true }
} catch (err) {
return { valid: false, reason: `Bech32 decode error: ${err.message}` }
}
}
return { valid: false, reason: 'Unrecognized Bitcoin address format' }
}
/**
* Validate Ethereum address (format only, NO EIP-55 checksum)
* @param {string} address - Ethereum address (0x...)
* @returns {Object} { valid: boolean, reason?: string, warning?: string }
*/
function validateEthereumAddress(address) {
// Must start with 0x and be 40 hex chars
if (!address.match(/^0x[a-fA-F0-9]{40}$/)) {
return { valid: false, reason: 'Invalid Ethereum address format (expected 0x + 40 hex chars)' }
}
// EIP-55 checksum validation DISABLED
// bare-crypto doesn't support keccak256, and installing @noble/hashes breaks Pear module resolution
// Accepting all properly formatted addresses regardless of checksum
return { valid: true, warning: 'EIP-55 checksum validation disabled (Pear module resolution limitation)' }
}
/**
* Generic address validator (dispatches to chain-specific validator)
* @param {string} address - Blockchain address
* @param {string} chain - 'bitcoin' or 'ethereum'
* @param {string} network - 'testnet' or 'mainnet'
* @returns {Object} { valid: boolean, reason?: string, warning?: string }
*/
function validateAddress(address, chain, network = 'testnet') {
if (!address || typeof address !== 'string') {
return { valid: false, reason: 'Address is required' }
}
if (chain === 'bitcoin') {
return validateBitcoinAddress(address, network)
} else if (chain === 'ethereum') {
return validateEthereumAddress(address)
} else {
return { valid: false, reason: `Unsupported chain: ${chain}` }
}
}
module.exports = {
validateAddress,
validateBitcoinAddress,
validateEthereumAddress
}