const crypto = require('crypto') const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' function bech32Polymod(values) { const generators = [ 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 ^= generators[i] } } } return chk >>> 0 } function bech32HrpExpand(hrp) { const values = [] for (const char of hrp) { values.push(char.charCodeAt(0) >> 5) } values.push(0) for (const char of hrp) { values.push(char.charCodeAt(0) & 31) } return values } function bech32VerifyChecksum(hrp, data) { const polymod = bech32Polymod([ ...bech32HrpExpand(hrp), ...data, ]) return polymod === 1 || polymod === 0x2bc830a3 } function bech32Decode(address) { if (typeof address !== 'string') { throw new Error('Bitcoin address must be a string') } if (address.length < 8 || address.length > 90) { throw new Error('Invalid Bitcoin address length') } const hasLower = address !== address.toUpperCase() const hasUpper = address !== address.toLowerCase() if (hasLower && hasUpper) { throw new Error( 'Bitcoin address must not mix uppercase and lowercase', ) } const normalized = address.toLowerCase() const separator = normalized.lastIndexOf('1') if (separator < 1 || separator + 7 > normalized.length) { throw new Error('Invalid Bech32 separator') } const hrp = normalized.slice(0, separator) const dataPart = normalized.slice(separator + 1) const data = [] for (const char of dataPart) { const value = BECH32_CHARSET.indexOf(char) if (value === -1) { throw new Error('Invalid Bech32 character') } data.push(value) } if (!bech32VerifyChecksum(hrp, data)) { throw new Error('Invalid Bitcoin address checksum') } return { hrp, data: data.slice(0, -6), spec: bech32Polymod([ ...bech32HrpExpand(hrp), ...data, ]) === 1 ? 'bech32' : 'bech32m', } } function convertBits(data, fromBits, toBits, pad) { let accumulator = 0 let bits = 0 const result = [] const maxValue = (1 << toBits) - 1 for (const value of data) { if (value < 0 || value >> fromBits !== 0) { throw new Error('Invalid bit conversion input') } accumulator = (accumulator << fromBits) | value bits += fromBits while (bits >= toBits) { bits -= toBits result.push( (accumulator >> bits) & maxValue, ) } } if (pad) { if (bits > 0) { result.push( (accumulator << (toBits - bits)) & maxValue, ) } } else { if (bits >= fromBits) { throw new Error('Invalid padding') } if ( ((accumulator << (toBits - bits)) & maxValue) !== 0 ) { throw new Error('Non-zero padding') } } return result } function addressToScriptPubKey(address) { const decoded = bech32Decode(address) if (decoded.hrp !== 'bc') { throw new Error( 'Only mainnet Bitcoin addresses are supported', ) } if (decoded.data.length < 1) { throw new Error('Invalid SegWit address') } const witnessVersion = decoded.data[0] if (witnessVersion > 16) { throw new Error('Invalid SegWit witness version') } if ( witnessVersion === 0 && decoded.spec !== 'bech32' ) { throw new Error( 'Witness version 0 must use Bech32', ) } if ( witnessVersion !== 0 && decoded.spec !== 'bech32m' ) { throw new Error( 'Witness version 1+ must use Bech32m', ) } const program = convertBits( decoded.data.slice(1), 5, 8, false, ) if (program.length < 2 || program.length > 40) { throw new Error('Invalid witness program length') } if ( witnessVersion === 0 && program.length !== 20 && program.length !== 32 ) { throw new Error( 'Invalid witness version 0 program length', ) } const versionOpcode = witnessVersion === 0 ? 0x00 : 0x50 + witnessVersion return Buffer.from([ versionOpcode, program.length, ...program, ]) } function scriptPubKeyToScripthash(scriptPubKey) { const hash = crypto .createHash('sha256') .update(scriptPubKey) .digest() return Buffer.from(hash) .reverse() .toString('hex') } function addressToScripthash(address) { return scriptPubKeyToScripthash( addressToScriptPubKey(address), ) } module.exports = { addressToScriptPubKey, scriptPubKeyToScripthash, addressToScripthash, }