UNPKG

j-bitcoin

Version:

Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features

193 lines (169 loc) 6.3 kB
/** * @fileoverview BIP173/BIP350 Bech32 address encoding for Bitcoin * @version 1.0.0 * @author yfbsei * @license ISC */ import { createHash } from 'node:crypto'; import { encodeSegwit, decodeSegwit, convertBits } from '../encoding/base32.js'; import rmd160 from '../core/crypto/hash/ripemd160.js'; /** * Bech32 human-readable part prefixes by network * @constant {Object.<string, string>} */ const BECH32_PREFIXES = { main: 'bc', test: 'tb' }; /** * Compute HASH160 (SHA256 + RIPEMD160) of data * @param {Buffer} data - Input data to hash * @returns {Buffer} 20-byte HASH160 result */ function hash160(data) { const sha256 = createHash('sha256').update(data).digest(); return rmd160(sha256); } /** * BIP173/BIP350 Bech32 address encoding utilities * @namespace BECH32 * @description Provides encoding/decoding for native SegWit (bech32) and * Taproot (bech32m) Bitcoin addresses. */ const BECH32 = { /** * Encode a public key to a P2WPKH (Pay-to-Witness-Public-Key-Hash) address * @param {string} publicKeyHex - Compressed or uncompressed public key as hex * @param {string} [network='main'] - Network type ('main' or 'test') * @returns {string} Bech32-encoded P2WPKH address * @throws {Error} If public key length is invalid or network is unknown * @example * const address = BECH32.to_P2WPKH(compressedPubKeyHex, 'main'); * // Returns: 'bc1q...' */ to_P2WPKH(publicKeyHex, network = 'main') { const publicKey = Buffer.from(publicKeyHex, 'hex'); if (publicKey.length !== 33 && publicKey.length !== 65) { throw new Error('Invalid public key length'); } const prefix = BECH32_PREFIXES[network]; if (!prefix) { throw new Error(`Invalid network: ${network}`); } const hash = hash160(publicKey); return encodeSegwit(prefix, 0, hash); }, /** * Encode a script hash to a P2WSH (Pay-to-Witness-Script-Hash) address * @param {string|Buffer} scriptHash - 32-byte SHA256 hash of the witness script * @param {string} [network='main'] - Network type ('main' or 'test') * @returns {string} Bech32-encoded P2WSH address * @throws {Error} If hash length is not 32 bytes or network is unknown * @example * const address = BECH32.to_P2WSH(sha256Hash, 'main'); * // Returns: 'bc1q...' (62 characters) */ to_P2WSH(scriptHash, network = 'main') { const hash = Buffer.isBuffer(scriptHash) ? scriptHash : Buffer.from(scriptHash, 'hex'); if (hash.length !== 32) { throw new Error('P2WSH requires 32-byte SHA256 hash'); } const prefix = BECH32_PREFIXES[network]; if (!prefix) { throw new Error(`Invalid network: ${network}`); } return encodeSegwit(prefix, 0, hash); }, /** * Encode an x-only public key to a P2TR (Pay-to-Taproot) address * @param {string|Buffer} xOnlyPublicKey - 32-byte x-only public key * @param {string} [network='main'] - Network type ('main' or 'test') * @returns {string} Bech32m-encoded P2TR address * @throws {Error} If public key is not 32 bytes or network is unknown * @example * const address = BECH32.to_P2TR(xOnlyPubKey, 'main'); * // Returns: 'bc1p...' */ to_P2TR(xOnlyPublicKey, network = 'main') { const pubkey = Buffer.isBuffer(xOnlyPublicKey) ? xOnlyPublicKey : Buffer.from(xOnlyPublicKey, 'hex'); if (pubkey.length !== 32) { throw new Error('Taproot requires 32-byte x-only public key'); } const prefix = BECH32_PREFIXES[network]; if (!prefix) { throw new Error(`Invalid network: ${network}`); } return encodeSegwit(prefix, 1, pubkey); }, /** * Decode a Bech32/Bech32m address * @param {string} address - Bech32-encoded Bitcoin address * @returns {Object} Decoded address information * @returns {number} returns.version - Witness version (0-16) * @returns {Buffer} returns.program - Witness program * @returns {string} returns.network - Network type ('main' or 'test') * @returns {string} returns.type - Address type ('p2wpkh', 'p2wsh', 'p2tr', 'unknown') * @throws {Error} If address prefix is invalid * @example * const decoded = BECH32.decode('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'); * // Returns: { version: 0, program: Buffer, network: 'main', type: 'p2wpkh' } */ decode(address) { let hrp, result; if (address.startsWith('bc1') || address.startsWith('BC1')) { hrp = 'bc'; } else if (address.startsWith('tb1') || address.startsWith('TB1')) { hrp = 'tb'; } else { throw new Error('Invalid bech32 address prefix'); } result = decodeSegwit(hrp, address.toLowerCase()); return { version: result.version, program: result.program, network: hrp === 'bc' ? 'main' : 'test', type: this.getAddressType(result.version, result.program.length) }; }, /** * Determine address type from witness version and program length * @param {number} version - Witness version (0-16) * @param {number} programLength - Length of witness program in bytes * @returns {string} Address type ('p2wpkh', 'p2wsh', 'p2tr', 'unknown') */ getAddressType(version, programLength) { if (version === 0) { if (programLength === 20) return 'p2wpkh'; if (programLength === 32) return 'p2wsh'; } if (version === 1 && programLength === 32) { return 'p2tr'; } return 'unknown'; }, /** * Validate a Bech32/Bech32m address * @param {string} address - Address to validate * @returns {boolean} True if address is valid * @example * BECH32.validate('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'); // true */ validate(address) { try { this.decode(address); return true; } catch { return false; } }, /** * Get the Bech32 prefix for a network * @param {string} [network='main'] - Network type * @returns {string} Human-readable part prefix ('bc' or 'tb') */ getPrefix(network = 'main') { return BECH32_PREFIXES[network] || BECH32_PREFIXES.main; } }; export { BECH32, hash160, BECH32_PREFIXES }; export default BECH32;