UNPKG

j-bitcoin

Version:

Comprehensive JavaScript cryptocurrency wallet library for Bitcoin (BTC), Bitcoin Cash (BCH), and Bitcoin SV (BSV) with custodial and non-custodial wallet support, threshold signatures, and multiple address formats

211 lines (192 loc) 9.09 kB
/** * @fileoverview Bech32 and Bech32m address encoding implementation for Bitcoin * * This module implements the Bech32 address format (BIP173) and Bech32m (BIP350) * for encoding Bitcoin SegWit addresses. It supports P2WPKH (Pay to Witness PubKey Hash) * address generation and arbitrary data encoding with customizable prefixes. * * @see {@link https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki|BIP173 - Base32 address format for native v0-16 witness outputs} * @see {@link https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki|BIP350 - Bech32m format for v1+ witness addresses} * @author yfbsei * @version 1.0.0 */ import CASH_ADDR from '../BCH/cash_addr.js'; import base32_encode from '../../utilities/Base32.js'; /** * Bech32 and Bech32m address encoding utilities for Bitcoin SegWit addresses * * Provides comprehensive support for encoding witness programs into human-readable * addresses with error detection capabilities. Supports both legacy Bech32 (for v0 witnesses) * and Bech32m (for v1+ witnesses) encoding schemes. * * @namespace BECH32 * @example * // Convert legacy address to P2WPKH * const legacyAddr = "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"; * const segwitAddr = BECH32.to_P2WPKH(legacyAddr); * // Returns: "bc1qhkfq3zahaqkkzx5mjnamwjsfpw3tvke7v6aaph" * * // Encode arbitrary data with custom prefix * const encoded = BECH32.data_to_bech32("hello", "48656c6c6f20576f726c64", "bech32"); * // Returns: "hello1dpjkcmr0vpmkxettv9xjqn50p2u" */ const BECH32 = { /** * Encodes data into Bech32 or Bech32m format with specified prefix * * The encoding process follows the Bech32 specification: * 1. Expands the Human Readable Part (HRP) into 5-bit groups * 2. Concatenates expanded HRP + data + 6 zero bytes * 3. Computes polynomial checksum using the Bech32 generator * 4. XORs with encoding constant (1 for Bech32, 0x2bc830a3 for Bech32m) * 5. Converts checksum to 5-bit representation and appends to data * 6. Encodes the complete payload using Base32 alphabet * * @param {string} [prefix="bc"] - Human Readable Part (e.g., "bc" for mainnet, "tb" for testnet) * @param {Uint8Array|Buffer} [data] - 5-bit encoded data to include in address * @param {string} [encoding='bech32'] - Encoding type: 'bech32' for v0 witnesses, 'bech32m' for v1+ * @returns {string} Complete Bech32-encoded address * @example * // Encode witness program for P2WPKH * const witnessProgram = new Uint8Array([0, ...hashBytes]); // version 0 + hash * const address = BECH32.encode("bc", witnessProgram, "bech32"); */ encode(prefix = "bc", data = Uint8Array || Buffer, encoding = 'bech32') { // Prepare data for checksum calculation: expanded HRP + data + 6 zero bytes let checksum = Buffer.concat([ this.expandHRP(prefix), // [high bits of HRP] + [0] + [low bits of HRP] data, // [data] Buffer.alloc(6) // [0,0,0,0,0,0] ]); // Calculate polynomial checksum and apply encoding constant checksum = this.polymod(checksum) ^ (encoding === 'bech32' ? 1 : 0x2bc830a3); // Combine data with checksum (excluding first 2 bytes from BCH template) const payload = Buffer.concat([ data, // Original data CASH_ADDR.checksum_5bit(checksum).subarray(2) // 6-byte checksum ]); // Return complete address: HRP + "1" + Base32(data+checksum) return prefix + "1" + base32_encode(payload); }, /** * Computes the Bech32 polynomial checksum using the generator polynomial * * Implements the Bech32 checksum algorithm with the generator polynomial: * G(x) = x^5 + x^3 + x + 1 (represented as 0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3) * * The algorithm processes each 5-bit value, maintaining a 30-bit checksum state * and applying the generator polynomial when the top bit is set. * * @param {Buffer|Uint8Array} values - Array of 5-bit values to process * @returns {number} 30-bit polynomial checksum result * @see {@link https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#checksum|BIP173 Checksum Algorithm} */ polymod(values) { const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; let chk = 1; for (let v of values) { let b = (chk >> 25); // Extract top 5 bits chk = (chk & 0x1ffffff) << 5 ^ v; // Shift and add new value // Apply generator polynomial for each set bit for (let i = 0; i < 5; i++) { chk ^= ((b >> i) & 1) ? GEN[i] : 0; } } return chk; }, /** * Expands the Human Readable Part (HRP) into the format required for checksum calculation * * The expansion converts the HRP into two parts: * 1. High 3 bits of each character * 2. A zero separator * 3. Low 5 bits of each character * * This expansion ensures that the HRP is properly incorporated into the checksum * while maintaining the 5-bit alignment required by the Bech32 algorithm. * * @param {string} [prefix="bc"] - Human Readable Part to expand * @returns {Buffer} Expanded HRP ready for checksum calculation * @example * const expanded = BECH32.expandHRP("bc"); * // Returns Buffer with: [3, 3] + [0] + [2, 3] (high bits + separator + low bits) */ expandHRP(prefix = "bc") { return Buffer.concat([ // Upper 3 bits of each character (characters >> 5) new Uint8Array(prefix.length).map((_, i) => prefix[i].charCodeAt() >> 5), Buffer.from([0]), // Separator // Lower 5 bits of each character (characters & 31) new Uint8Array(prefix.length).map((_, i) => prefix[i].charCodeAt() & 31) ]); }, /** * Converts a legacy Bitcoin address to a P2WPKH (Pay to Witness PubKey Hash) Bech32 address * * The conversion process: * 1. Decodes the legacy Base58Check address to extract the hash160 * 2. Determines the appropriate network prefix (bc/tb) from the version byte * 3. Converts the 20-byte hash from 8-bit to 5-bit representation * 4. Prepends witness version 0 to create the witness program * 5. Encodes using Bech32 (not Bech32m, as version 0 uses original Bech32) * * @param {string} [witness_program="legacy address"] - Legacy P2PKH address to convert * @returns {string} Bech32-encoded P2WPKH address * @throws {Error} If the legacy address is invalid or has wrong format * @example * // Mainnet conversion * const legacy = "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"; * const segwit = BECH32.to_P2WPKH(legacy); * // Returns: "bc1qhkfq3zahaqkkzx5mjnamwjsfpw3tvke7v6aaph" * * // Testnet conversion * const testLegacy = "mgRpP3zP1hmxyoeYJgfbcmN3c2Qsurw48D"; * const testSegwit = BECH32.to_P2WPKH(testLegacy); * // Returns: "tb1qp8lhpx0jmcusxnq6cyktwp8rfpaunccntw8kty" */ to_P2WPKH(witness_program = "legacy address") { // Decode legacy address to get network prefix and hash160 let [bch_prefix, hash] = CASH_ADDR.decode_legacy_address(witness_program); // Map BCH prefix to BTC prefix const btc_prefix = bch_prefix === "bitcoincash" ? 'bc' : 'tb'; // Convert hash from hex string to buffer, then to 5-bit representation hash = Buffer.from(hash, 'hex'); hash = CASH_ADDR.convertBits(hash, 8, 5); // Convert to 5 bits per group // Create witness program: version 0 + converted hash const data = Buffer.concat([Buffer.from([0]), hash]); // Encode using Bech32 (version 0 uses Bech32, not Bech32m) return this.encode(btc_prefix, data, 'bech32'); }, /** * Encodes arbitrary hex data into a Bech32 address with custom prefix * * This function provides a general-purpose interface for encoding any data * into the Bech32 format. It handles the conversion from 8-bit bytes to * 5-bit groups and validates the total length constraints. * * @param {string} [prefix="Jamallo"] - Custom Human Readable Part for the address * @param {string} [data="hex"] - Hex-encoded data to include in the address * @param {string} [encoding='bech32'] - Encoding type: 'bech32' or 'bech32m' * @returns {string} Bech32-encoded address with custom prefix and data * @throws {Error} If the total address length would exceed 90 characters * @example * // Encode custom data * const customAddr = BECH32.data_to_bech32("myapp", "48656c6c6f", "bech32"); * // Returns: "myapp1dpjkcmr0vx8nrwl" * * // Using Bech32m encoding * const modernAddr = BECH32.data_to_bech32("test", "deadbeef", "bech32m"); */ data_to_bech32(prefix = "Jamallo", data = "hex", encoding = 'bech32') { // Convert hex string to buffer and then to 5-bit representation const hex_to_buffer = Buffer.from(data, 'hex'); data = CASH_ADDR.convertBits(hex_to_buffer, 8, 5); // Validate total length: 2*prefix + 1 separator + data + 6 checksum ≤ 90 const len = 2 * prefix.length + 1 + data.length + 6; if (len - prefix.length > 90) { throw new Error("prefix or data is too long, total max length is 90"); } return this.encode(prefix, data, encoding); } } export default BECH32;