Files
brk/website_next/wallets/derive/address.js
T
2026-06-17 11:25:42 +02:00

271 lines
6.2 KiB
JavaScript

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<EncodedAddress>}
*/
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<EncodedAddress>}
*/
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<EncodedAddress>}
*/
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<EncodedAddress>}
*/
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<EncodedAddress>}
*/
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<EncodedAddress>}
*/
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");
}