import { encodeBase58Check } from "./base58.js"; import { concatBytes } from "./bytes.js"; import { hash160, sha256 } from "./hash.js"; import { addXOnlyPublicKeyTweak, getXOnlyPublicKey, } from "./secp256k1.js"; const bech32Alphabet = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; const bech32Generator = /** @type {const} */ ([ 0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3, ]); const BECH32_CHECKSUM = 1; const BECH32M_CHECKSUM = 0x2bc830a3; const p2pkhVersions = /** @type {const} */ ({ mainnet: 0x00, testnet: 0x6f, }); const p2shVersions = /** @type {const} */ ({ mainnet: 0x05, testnet: 0xc4, }); const bech32Prefixes = /** @type {const} */ ({ mainnet: "bc", testnet: "tb", }); /** * @typedef {"mainnet" | "testnet"} BitcoinNetwork * @typedef {"p2pkh" | "p2sh_p2wpkh" | "v0_p2wpkh" | "v1_p2tr" | "v0_p2wsh_sortedmulti"} AddressScript * @typedef {Object} EncodedAddress * @property {string} address * @property {Uint8Array} payload */ /** * @param {number} version * @param {Uint8Array} payload */ function encodeVersionedBase58(version, payload) { return encodeBase58Check(concatBytes([Uint8Array.of(version), payload])); } /** * @param {string} prefix */ function expandBech32Prefix(prefix) { const values = /** @type {number[]} */ ([]); for (const character of prefix) { values.push(character.charCodeAt(0) >> 5); } values.push(0); for (const character of prefix) { values.push(character.charCodeAt(0) & 31); } return values; } /** * @param {number[]} values */ function bech32Polymod(values) { let checksum = 1; for (const value of values) { const top = checksum >>> 25; checksum = ((checksum & 0x1ffffff) << 5) ^ value; for (let i = 0; i < bech32Generator.length; i += 1) { if ((top >>> i) & 1) { checksum ^= bech32Generator[i]; } } } return checksum; } /** * @param {string} prefix * @param {number[]} values * @param {number} checksumConstant */ function createBech32Checksum(prefix, values, checksumConstant) { const polymod = bech32Polymod([ ...expandBech32Prefix(prefix), ...values, 0, 0, 0, 0, 0, 0, ]); const checksum = /** @type {number[]} */ ([]); const combined = polymod ^ checksumConstant; for (let i = 0; i < 6; i += 1) { checksum.push((combined >>> (5 * (5 - i))) & 31); } return checksum; } /** * @param {Uint8Array} bytes * @param {number} fromBits * @param {number} toBits */ function convertBits(bytes, fromBits, toBits) { const maxValue = (1 << toBits) - 1; const values = /** @type {number[]} */ ([]); let accumulator = 0; let bits = 0; for (const byte of bytes) { accumulator = (accumulator << fromBits) | byte; bits += fromBits; while (bits >= toBits) { bits -= toBits; values.push((accumulator >>> bits) & maxValue); } } if (bits > 0) { values.push((accumulator << (toBits - bits)) & maxValue); } return values; } /** * @param {string} prefix * @param {number[]} values * @param {number} checksumConstant */ function encodeBech32(prefix, values, checksumConstant) { const checksum = createBech32Checksum(prefix, values, checksumConstant); const characters = [...values, ...checksum].map((value) => { return bech32Alphabet[value]; }); return `${prefix}1${characters.join("")}`; } /** * @param {string} tag * @param {Uint8Array} bytes */ async function taggedHash(tag, bytes) { const tagHash = await sha256(new TextEncoder().encode(tag)); return sha256(concatBytes([tagHash, tagHash, bytes])); } /** * @param {Uint8Array} publicKey * @param {BitcoinNetwork} network * @returns {Promise} */ async function encodeP2pkhAddressData(publicKey, network) { const payload = await hash160(publicKey); return { address: await encodeVersionedBase58(p2pkhVersions[network], payload), payload, }; } /** * @param {Uint8Array} publicKey * @param {BitcoinNetwork} network * @returns {Promise} */ async function encodeP2shP2wpkhAddressData(publicKey, network) { const publicKeyHash = await hash160(publicKey); const redeemScript = concatBytes([Uint8Array.of(0x00, 0x14), publicKeyHash]); const payload = await hash160(redeemScript); return { address: await encodeVersionedBase58(p2shVersions[network], payload), payload, }; } /** * @param {Uint8Array} publicKey * @param {BitcoinNetwork} network * @returns {Promise} */ async function encodeP2wpkhAddressData(publicKey, network) { const payload = await hash160(publicKey); const values = [0, ...convertBits(payload, 8, 5)]; return { address: encodeBech32(bech32Prefixes[network], values, BECH32_CHECKSUM), payload, }; } /** * @param {Uint8Array} witnessScript * @param {BitcoinNetwork} network * @returns {Promise} */ export async function encodeP2wshAddressData(witnessScript, network) { const payload = await sha256(witnessScript); const values = [0, ...convertBits(payload, 8, 5)]; return { address: encodeBech32(bech32Prefixes[network], values, BECH32_CHECKSUM), payload, }; } /** * @param {Uint8Array} publicKey * @param {BitcoinNetwork} network * @returns {Promise} */ async function encodeP2trAddressData(publicKey, network) { const internalKey = getXOnlyPublicKey(publicKey); const tweak = await taggedHash("TapTweak", internalKey); const payload = addXOnlyPublicKeyTweak(publicKey, tweak); const values = [1, ...convertBits(payload, 8, 5)]; return { address: encodeBech32(bech32Prefixes[network], values, BECH32M_CHECKSUM), payload, }; } /** * @param {Uint8Array} publicKey * @param {AddressScript} script * @param {BitcoinNetwork} network * @returns {Promise} */ export async function encodePublicKeyAddressData(publicKey, script, network) { if (script === "p2pkh") { return encodeP2pkhAddressData(publicKey, network); } if (script === "p2sh_p2wpkh") { return encodeP2shP2wpkhAddressData(publicKey, network); } if (script === "v0_p2wpkh") { return encodeP2wpkhAddressData(publicKey, network); } if (script === "v1_p2tr") { return encodeP2trAddressData(publicKey, network); } throw new Error("Expected a single-key address script"); }