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
252 lines (213 loc) • 6.48 kB
JavaScript
/**
* @fileoverview Bech32/Bech32m encoding for Bitcoin SegWit and Taproot addresses
* @version 1.0.0
* @author yfbsei
* @license ISC
*/
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
const BECH32_CONST = 1;
const BECH32M_CONST = 0x2bc830a3;
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
/**
* Compute Bech32 polymod checksum
* @param {number[]} values - Values to checksum
* @returns {number} Checksum value
*/
function polymod(values) {
let chk = 1;
for (const v of values) {
const top = chk >> 25;
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (let i = 0; i < 5; i++) {
if ((top >> i) & 1) {
chk ^= GENERATOR[i];
}
}
}
return chk;
}
function hrpExpand(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;
}
function verifyChecksum(hrp, data, spec) {
const constant = spec === 'bech32m' ? BECH32M_CONST : BECH32_CONST;
return polymod(hrpExpand(hrp).concat(data)) === constant;
}
function createChecksum(hrp, data, spec) {
const constant = spec === 'bech32m' ? BECH32M_CONST : BECH32_CONST;
const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
const mod = polymod(values) ^ constant;
const result = [];
for (let i = 0; i < 6; i++) {
result.push((mod >> (5 * (5 - i))) & 31);
}
return result;
}
/**
* Encode data as Bech32/Bech32m
* @param {string} hrp - Human-readable part
* @param {number[]} data - 5-bit values to encode
* @param {string} [spec='bech32'] - Encoding spec ('bech32' or 'bech32m')
* @returns {string} Encoded address
*/
function encode(hrp, data, spec = 'bech32') {
const checksum = createChecksum(hrp, data, spec);
const combined = data.concat(checksum);
let result = hrp + '1';
for (const d of combined) {
result += CHARSET[d];
}
return result;
}
/**
* Decode a Bech32/Bech32m string
* @param {string} str - Bech32 string to decode
* @returns {Object} Decoded {hrp, data, spec}
* @throws {Error} If string invalid or checksum fails
*/
function decode(str) {
if (str.length < 8 || str.length > 90) {
throw new Error('Invalid bech32 string length');
}
const lowered = str.toLowerCase();
const uppered = str.toUpperCase();
if (str !== lowered && str !== uppered) {
throw new Error('Mixed case in bech32 string');
}
const bech = lowered;
const pos = bech.lastIndexOf('1');
if (pos < 1 || pos + 7 > bech.length) {
throw new Error('Invalid separator position');
}
const hrp = bech.slice(0, pos);
const data = [];
for (let i = pos + 1; i < bech.length; i++) {
const idx = CHARSET.indexOf(bech[i]);
if (idx === -1) {
throw new Error(`Invalid character: ${bech[i]}`);
}
data.push(idx);
}
let spec = 'bech32';
if (!verifyChecksum(hrp, data, 'bech32')) {
if (verifyChecksum(hrp, data, 'bech32m')) {
spec = 'bech32m';
} else {
throw new Error('Invalid checksum');
}
}
return { hrp, data: data.slice(0, -6), spec };
}
/**
* Convert between bit groupings
* @param {number[]} data - Input values
* @param {number} fromBits - Source bits per value
* @param {number} toBits - Target bits per value
* @param {boolean} [pad=true] - Add padding if needed
* @returns {number[]} Converted values
*/
function convertBits(data, fromBits, toBits, pad = true) {
let acc = 0;
let bits = 0;
const result = [];
const maxv = (1 << toBits) - 1;
for (const value of data) {
if (value < 0 || value >> fromBits !== 0) {
throw new Error('Invalid value for bit conversion');
}
acc = (acc << fromBits) | value;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
result.push((acc >> bits) & maxv);
}
}
if (pad) {
if (bits > 0) {
result.push((acc << (toBits - bits)) & maxv);
}
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) !== 0) {
throw new Error('Invalid padding');
}
return result;
}
/**
* Encode a SegWit address
* @param {string} hrp - Human-readable part ('bc' or 'tb')
* @param {number} version - Witness version (0-16)
* @param {Buffer} program - Witness program
* @returns {string} Bech32/Bech32m address
*/
function encodeSegwit(hrp, version, program) {
if (version < 0 || version > 16) {
throw new Error('Invalid witness version');
}
if (program.length < 2 || program.length > 40) {
throw new Error('Invalid witness program length');
}
if (version === 0 && program.length !== 20 && program.length !== 32) {
throw new Error('Invalid witness program length for v0');
}
const spec = version === 0 ? 'bech32' : 'bech32m';
const data = [version].concat(convertBits(Array.from(program), 8, 5));
return encode(hrp, data, spec);
}
/**
* Decode a SegWit address
* @param {string} hrp - Expected human-readable part
* @param {string} addr - Bech32 address to decode
* @returns {Object} Decoded {version, program}
* @throws {Error} If address invalid
*/
function decodeSegwit(hrp, addr) {
const { hrp: decodedHrp, data, spec } = decode(addr);
if (decodedHrp !== hrp) {
throw new Error('HRP mismatch');
}
if (data.length < 1) {
throw new Error('Empty data');
}
const version = data[0];
if (version > 16) {
throw new Error('Invalid witness version');
}
if (version === 0 && spec !== 'bech32') {
throw new Error('Version 0 must use bech32');
}
if (version !== 0 && spec !== 'bech32m') {
throw new Error('Version 1+ must use bech32m');
}
const program = Buffer.from(convertBits(data.slice(1), 5, 8, false));
if (program.length < 2 || program.length > 40) {
throw new Error('Invalid program length');
}
if (version === 0 && program.length !== 20 && program.length !== 32) {
throw new Error('Invalid v0 program length');
}
return { version, program };
}
export {
encode,
decode,
encodeSegwit,
decodeSegwit,
convertBits,
CHARSET,
BECH32_CONST,
BECH32M_CONST
};
export default {
encode,
decode,
encodeSegwit,
decodeSegwit,
convertBits
};