UNPKG

@shakesco/silent

Version:

Bitcoin Silent Payments

172 lines (141 loc) 4.24 kB
// Bech32 character set for encoding const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; // Generator coefficients for checksum calculation const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; const ENCODING_CONST = { bech32: 1, bech32m: 0x2bc830a3, }; const Bech32Consts = { /// The separator character used in Bech32 encoded strings. separator: "1", /// The length of the checksum part in a Bech32 encoded string. checksumStrLen: 6, }; // Bech32 encoding function function encodeBech32( hrp, data, separator = Bech32Consts.separator, encoding = "bech32m" ) { const checksum = createChecksum(hrp, data, encoding); const combined = [...data, ...checksum]; const encodedData = combined.map((value) => CHARSET[value]).join(""); return `${hrp}${separator}${encodedData}`; } // Convert bytes to base32 function convertToBase32(data) { const result = []; let accumulator = 0; let bits = 0; const maxV = 31; // 5-bit chunks for base32 for (const value of data) { accumulator = (accumulator << 8) | value; bits += 8; while (bits >= 5) { bits -= 5; result.push((accumulator >> bits) & maxV); } } if (bits > 0) { result.push((accumulator << (5 - bits)) & maxV); } return result; } function convertFromBase32(data) { const result = []; let accumulator = 0; let bits = 0; const maxV = 255; // 8-bit chunks for bytes for (const value of data) { accumulator = (accumulator << 5) | value; bits += 5; while (bits >= 8) { bits -= 8; result.push((accumulator >> bits) & maxV); } } return result; } // Create checksum function createChecksum(hrp, data, encoding = "bech32m") { const values = [...expandHrp(hrp), ...data, 0, 0, 0, 0, 0, 0]; const polymod = polyMod(values) ^ ENCODING_CONST[encoding]; return Array.from({ length: 6 }, (_, i) => (polymod >> (5 * (5 - i))) & 31); } // Expand the human-readable part function expandHrp(hrp) { const expand = []; for (let i = 0; i < hrp.length; i++) { expand.push(hrp.charCodeAt(i) >> 5); } expand.push(0); for (let i = 0; i < hrp.length; i++) { expand.push(hrp.charCodeAt(i) & 31); } return expand; } // PolyMod function for checksum calculation function polyMod(values) { 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 ^= GENERATOR[i]; } } } return chk; } function decodeBech32( bechStr, sep = Bech32Consts.separator, checksumLen = Bech32Consts.checksumStrLen, encoding = "bech32m" ) { if (_isStringMixed(bechStr)) { throw new Error("Invalid bech32 format (string is mixed case)"); } bechStr = bechStr.toLowerCase(); const sepPos = bechStr.lastIndexOf(sep); if (sepPos == -1) { throw new Error("Invalid bech32 format (no separator found)"); } const hrp = bechStr.substring(0, sepPos); if ( hrp.length === 0 || hrp .split("") .some((char) => char.charCodeAt(0) < 33 || char.charCodeAt(0) > 126) ) { throw new Error(`Invalid bech32 format (HRP not valid: ${hrp})`); } const dataPart = bechStr.substring(sepPos + 1); if ( dataPart.length < checksumLen + 1 || dataPart.split("").some((char) => !CHARSET.includes(char)) ) { throw new Error("Invalid bech32 format (data part not valid)"); } const intData = dataPart.split("").map((char) => CHARSET.indexOf(char)); if (!veriCheckSum(hrp, intData, encoding)) { throw new Error("Invalid bech32 checksum"); } return [hrp, Array.from(intData.slice(0, intData.length - checksumLen))]; } function veriCheckSum(hrp, data, encoding = "bech32m") { const polymod = polyMod([...expandHrp(hrp), ...data]); return polymod == ENCODING_CONST[encoding]; } function _isStringMixed(str) { return str !== str.toLowerCase() && str !== str.toUpperCase(); } module.exports = { convertToBase32, convertFromBase32, encodeBech32, decodeBech32, };