mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-26 02:08:10 -07:00
global: private xpub support part 2
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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");
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { bytesEqual, bytesToBigInt, concatBytes, createBytes } from "./bytes.js";
|
||||
import { checksum as createChecksum } from "./hash.js";
|
||||
|
||||
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
const base = 58n;
|
||||
|
||||
/**
|
||||
* @param {string} character
|
||||
*/
|
||||
function readBase58Character(character) {
|
||||
const index = alphabet.indexOf(character);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error(`Invalid Base58 character: ${character}`);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function countLeadingZeros(text) {
|
||||
let count = 0;
|
||||
|
||||
while (text[count] === "1") {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
function countLeadingZeroBytes(bytes) {
|
||||
let count = 0;
|
||||
|
||||
while (bytes[count] === 0) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function decodeBase58(text) {
|
||||
let value = 0n;
|
||||
|
||||
for (const character of text) {
|
||||
value = value * base + BigInt(readBase58Character(character));
|
||||
}
|
||||
|
||||
const leadingZeros = countLeadingZeros(text);
|
||||
const decoded = /** @type {number[]} */ ([]);
|
||||
|
||||
while (value > 0n) {
|
||||
decoded.push(Number(value & 0xffn));
|
||||
value >>= 8n;
|
||||
}
|
||||
|
||||
decoded.reverse();
|
||||
|
||||
const bytes = createBytes(leadingZeros + decoded.length);
|
||||
bytes.set(decoded, leadingZeros);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
function encodeBase58(bytes) {
|
||||
let value = bytesToBigInt(bytes);
|
||||
let text = "";
|
||||
|
||||
while (value > 0n) {
|
||||
const index = Number(value % base);
|
||||
text = alphabet[index] + text;
|
||||
value /= base;
|
||||
}
|
||||
|
||||
return "1".repeat(countLeadingZeroBytes(bytes)) + text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
export async function decodeBase58Check(text) {
|
||||
const bytes = decodeBase58(text);
|
||||
|
||||
if (bytes.length < 4) {
|
||||
throw new Error("Invalid Base58Check payload");
|
||||
}
|
||||
|
||||
const payload = bytes.slice(0, -4);
|
||||
const expected = await createChecksum(payload);
|
||||
const actual = bytes.slice(-4);
|
||||
|
||||
if (!bytesEqual(actual, expected)) {
|
||||
throw new Error("Invalid Base58Check checksum");
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} payload
|
||||
*/
|
||||
export async function encodeBase58Check(payload) {
|
||||
return encodeBase58(concatBytes([payload, await createChecksum(payload)]));
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { concatBytes, writeUint32 } from "./bytes.js";
|
||||
import { hmacSha512 } from "./hash.js";
|
||||
import { parseExtendedPublicKey } from "./key.js";
|
||||
import { addPublicKeyTweak } from "./secp256k1.js";
|
||||
|
||||
const HARDENED_INDEX = 0x80000000;
|
||||
|
||||
/**
|
||||
* @typedef {import("./key.js").ExtendedPublicKey} ExtendedPublicKey
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DerivedPublicKey
|
||||
* @property {number} index
|
||||
* @property {Uint8Array} publicKey
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {ExtendedPublicKey} key
|
||||
* @param {number} index
|
||||
* @returns {Promise<ExtendedPublicKey>}
|
||||
*/
|
||||
async function derivePublicChild(key, index) {
|
||||
if (!Number.isSafeInteger(index) || index < 0 || index >= HARDENED_INDEX) {
|
||||
throw new Error("Expected a non-hardened child index");
|
||||
}
|
||||
|
||||
const data = concatBytes([key.publicKey, writeUint32(index)]);
|
||||
const digest = await hmacSha512(key.chainCode, data);
|
||||
const tweak = digest.slice(0, 32);
|
||||
const chainCode = digest.slice(32);
|
||||
|
||||
return {
|
||||
text: key.text,
|
||||
depth: key.depth + 1,
|
||||
childNumber: index,
|
||||
parentFingerprint: key.parentFingerprint,
|
||||
chainCode,
|
||||
publicKey: addPublicKeyTweak(key.publicKey, tweak),
|
||||
version: key.version,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExtendedPublicKey} key
|
||||
* @param {readonly number[]} path
|
||||
*/
|
||||
async function derivePublicPath(key, path) {
|
||||
let child = key;
|
||||
|
||||
for (const index of path) {
|
||||
child = await derivePublicChild(child, index);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ExtendedPublicKey} key
|
||||
* @param {number} start
|
||||
* @param {number} count
|
||||
* @param {readonly number[]} [path]
|
||||
* @returns {Promise<DerivedPublicKey[]>}
|
||||
*/
|
||||
export async function derivePublicKeys(key, start, count, path = []) {
|
||||
const parent = await derivePublicPath(key, path);
|
||||
const children = /** @type {DerivedPublicKey[]} */ ([]);
|
||||
|
||||
for (let offset = 0; offset < count; offset += 1) {
|
||||
const index = start + offset;
|
||||
const child = await derivePublicChild(parent, index);
|
||||
|
||||
children.push({ index, publicKey: child.publicKey });
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
export async function parseXpub(text) {
|
||||
return parseExtendedPublicKey(text);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @param {number} length
|
||||
*/
|
||||
export function createBytes(length) {
|
||||
return new Uint8Array(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array[]} parts
|
||||
*/
|
||||
export function concatBytes(parts) {
|
||||
const length = parts.reduce((total, part) => total + part.length, 0);
|
||||
const bytes = createBytes(length);
|
||||
let offset = 0;
|
||||
|
||||
for (const part of parts) {
|
||||
bytes.set(part, offset);
|
||||
offset += part.length;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
* @param {number} start
|
||||
*/
|
||||
export function readUint32(bytes, start) {
|
||||
return (
|
||||
bytes[start] * 0x1000000 +
|
||||
bytes[start + 1] * 0x10000 +
|
||||
bytes[start + 2] * 0x100 +
|
||||
bytes[start + 3]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
export function writeUint32(value) {
|
||||
const bytes = createBytes(4);
|
||||
|
||||
bytes[0] = value >>> 24;
|
||||
bytes[1] = value >>> 16;
|
||||
bytes[2] = value >>> 8;
|
||||
bytes[3] = value;
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
export function bytesToBigInt(bytes) {
|
||||
let value = 0n;
|
||||
|
||||
for (const byte of bytes) {
|
||||
value = (value << 8n) + BigInt(byte);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bigint} value
|
||||
* @param {number} length
|
||||
*/
|
||||
export function bigIntToBytes(value, length) {
|
||||
const bytes = createBytes(length);
|
||||
let remaining = value;
|
||||
|
||||
for (let i = length - 1; i >= 0; i -= 1) {
|
||||
bytes[i] = Number(remaining & 0xffn);
|
||||
remaining >>= 8n;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} left
|
||||
* @param {Uint8Array} right
|
||||
*/
|
||||
export function bytesEqual(left, right) {
|
||||
if (left.length !== right.length) return false;
|
||||
|
||||
for (let i = 0; i < left.length; i += 1) {
|
||||
if (left[i] !== right[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import { concatBytes } from "./bytes.js";
|
||||
import { derivePublicKeys, parseXpub } from "./bip32.js";
|
||||
import { encodeP2wshAddressData } from "./address.js";
|
||||
|
||||
const CHECKSUM_SEPARATOR = "#";
|
||||
const WSH_SORTEDMULTI_PREFIX = "wsh(sortedmulti(";
|
||||
const WSH_SORTEDMULTI_SUFFIX = "))";
|
||||
const OP_CHECKMULTISIG = 0xae;
|
||||
const COMPRESSED_PUBLIC_KEY_BYTES = 33;
|
||||
const MAX_WSH_MULTISIG_KEYS = 20;
|
||||
|
||||
/**
|
||||
* @typedef {import("./address.js").BitcoinNetwork} BitcoinNetwork
|
||||
* @typedef {import("./index.js").GeneratedAddress} GeneratedAddress
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DescriptorKey
|
||||
* @property {string} xpub
|
||||
* @property {number[]} path
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SortedMultisigDescriptor
|
||||
* @property {"v0_p2wsh_sortedmulti"} script
|
||||
* @property {number} threshold
|
||||
* @property {DescriptorKey[]} keys
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function compactText(text) {
|
||||
return text.trim().replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function stripDescriptorChecksum(text) {
|
||||
const value = compactText(text);
|
||||
const checksumIndex = value.indexOf(CHECKSUM_SEPARATOR);
|
||||
|
||||
return checksumIndex === -1 ? value : value.slice(0, checksumIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function isSupportedDescriptor(text) {
|
||||
return (
|
||||
text.startsWith(WSH_SORTEDMULTI_PREFIX) &&
|
||||
text.endsWith(WSH_SORTEDMULTI_SUFFIX)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function extractOutputDescriptors(text) {
|
||||
const value = compactText(text);
|
||||
const descriptors = /** @type {string[]} */ ([]);
|
||||
let offset = 0;
|
||||
|
||||
while (offset < value.length) {
|
||||
const start = value.indexOf(WSH_SORTEDMULTI_PREFIX, offset);
|
||||
|
||||
if (start === -1) break;
|
||||
|
||||
let depth = 0;
|
||||
let end = -1;
|
||||
let seenOpen = false;
|
||||
|
||||
for (let index = start; index < value.length; index += 1) {
|
||||
const character = value[index];
|
||||
|
||||
if (character === "(") {
|
||||
depth += 1;
|
||||
seenOpen = true;
|
||||
}
|
||||
|
||||
if (character === ")") {
|
||||
depth -= 1;
|
||||
}
|
||||
|
||||
if (seenOpen && depth === 0) {
|
||||
end = index + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (end === -1) break;
|
||||
|
||||
const descriptor = stripDescriptorChecksum(value.slice(start, end));
|
||||
|
||||
if (isSupportedDescriptor(descriptor)) {
|
||||
descriptors.push(descriptor);
|
||||
}
|
||||
|
||||
offset = end;
|
||||
}
|
||||
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
export function isOutputDescriptor(text) {
|
||||
return extractOutputDescriptors(text).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function readFirstOutputDescriptor(text) {
|
||||
const descriptor = extractOutputDescriptors(text)[0];
|
||||
|
||||
if (!descriptor) {
|
||||
throw new Error("Unsupported output descriptor");
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function splitDescriptorArguments(text) {
|
||||
const values = /** @type {string[]} */ ([]);
|
||||
let bracketDepth = 0;
|
||||
let groupDepth = 0;
|
||||
let start = 0;
|
||||
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const character = text[index];
|
||||
|
||||
if (character === "[") bracketDepth += 1;
|
||||
if (character === "]") bracketDepth -= 1;
|
||||
if (character === "(") groupDepth += 1;
|
||||
if (character === ")") groupDepth -= 1;
|
||||
|
||||
if (character === "," && bracketDepth === 0 && groupDepth === 0) {
|
||||
values.push(text.slice(start, index));
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
values.push(text.slice(start));
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
function readThreshold(value) {
|
||||
const threshold = Number(value);
|
||||
|
||||
if (!Number.isSafeInteger(threshold) || threshold < 1) {
|
||||
throw new Error("Invalid multisig threshold");
|
||||
}
|
||||
|
||||
return threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
function readNonHardenedIndex(value) {
|
||||
if (value.endsWith("'") || value.endsWith("h")) {
|
||||
throw new Error("Descriptor xpub derivation cannot be hardened");
|
||||
}
|
||||
|
||||
const index = Number(value);
|
||||
|
||||
if (!Number.isSafeInteger(index) || index < 0) {
|
||||
throw new Error("Invalid descriptor derivation path");
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
function readDescriptorKeyPath(text) {
|
||||
if (!text.startsWith("/")) {
|
||||
throw new Error("Expected a ranged descriptor key path");
|
||||
}
|
||||
|
||||
const segments = text.slice(1).split("/");
|
||||
|
||||
if (segments[segments.length - 1] !== "*") {
|
||||
throw new Error("Expected a descriptor wildcard path");
|
||||
}
|
||||
|
||||
return segments.slice(0, -1).map(readNonHardenedIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {DescriptorKey}
|
||||
*/
|
||||
function readDescriptorKey(text) {
|
||||
let value = text;
|
||||
|
||||
if (value.startsWith("[")) {
|
||||
const end = value.indexOf("]");
|
||||
|
||||
if (end === -1) {
|
||||
throw new Error("Invalid descriptor key origin");
|
||||
}
|
||||
|
||||
value = value.slice(end + 1);
|
||||
}
|
||||
|
||||
const pathIndex = value.indexOf("/");
|
||||
|
||||
if (pathIndex === -1) {
|
||||
throw new Error("Expected descriptor key derivation");
|
||||
}
|
||||
|
||||
return {
|
||||
xpub: value.slice(0, pathIndex),
|
||||
path: readDescriptorKeyPath(value.slice(pathIndex)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {SortedMultisigDescriptor}
|
||||
*/
|
||||
export function parseOutputDescriptor(text) {
|
||||
const value = readFirstOutputDescriptor(text);
|
||||
|
||||
const body = value.slice(
|
||||
WSH_SORTEDMULTI_PREFIX.length,
|
||||
-WSH_SORTEDMULTI_SUFFIX.length,
|
||||
);
|
||||
const [thresholdText, ...keyTexts] = splitDescriptorArguments(body);
|
||||
const threshold = readThreshold(thresholdText);
|
||||
const keys = keyTexts.map(readDescriptorKey);
|
||||
|
||||
if (
|
||||
threshold > keys.length ||
|
||||
keys.length < 1 ||
|
||||
keys.length > MAX_WSH_MULTISIG_KEYS
|
||||
) {
|
||||
throw new Error("Invalid multisig key count");
|
||||
}
|
||||
|
||||
return {
|
||||
script: "v0_p2wsh_sortedmulti",
|
||||
threshold,
|
||||
keys,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} descriptorText
|
||||
*/
|
||||
function inferDescriptorBranchId(descriptorText) {
|
||||
const descriptor = parseOutputDescriptor(descriptorText);
|
||||
const branchIds = descriptor.keys.map((key) => {
|
||||
return key.path[key.path.length - 1];
|
||||
});
|
||||
const sameBranch = branchIds.every((branchId) => {
|
||||
return branchId === branchIds[0];
|
||||
});
|
||||
|
||||
if (!sameBranch) return undefined;
|
||||
if (branchIds[0] === 0) return "receive";
|
||||
if (branchIds[0] === 1) return "change";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
export function getOutputDescriptorBranchIds(text) {
|
||||
const branchIds = /** @type {string[]} */ ([]);
|
||||
|
||||
for (const descriptor of extractOutputDescriptors(text)) {
|
||||
const branchId = inferDescriptorBranchId(descriptor);
|
||||
|
||||
if (branchId && !branchIds.includes(branchId)) {
|
||||
branchIds.push(branchId);
|
||||
}
|
||||
}
|
||||
|
||||
return branchIds.length ? branchIds : ["receive"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {string} [branchId]
|
||||
*/
|
||||
export function selectOutputDescriptor(source, branchId = "receive") {
|
||||
const descriptors = extractOutputDescriptors(source);
|
||||
|
||||
if (descriptors.length === 0) {
|
||||
throw new Error("Unsupported output descriptor");
|
||||
}
|
||||
|
||||
return descriptors.find((descriptor) => {
|
||||
return inferDescriptorBranchId(descriptor) === branchId;
|
||||
}) ?? descriptors[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} left
|
||||
* @param {Uint8Array} right
|
||||
*/
|
||||
function compareBytes(left, right) {
|
||||
for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
|
||||
if (left[index] !== right[index]) return left[index] - right[index];
|
||||
}
|
||||
|
||||
return left.length - right.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
function encodeScriptNumber(value) {
|
||||
if (value <= 16) return Uint8Array.of(0x50 + value);
|
||||
|
||||
return Uint8Array.of(0x01, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {readonly Uint8Array[]} publicKeys
|
||||
* @param {number} threshold
|
||||
*/
|
||||
function encodeSortedMultisigScript(publicKeys, threshold) {
|
||||
const sortedKeys = [...publicKeys].sort(compareBytes);
|
||||
const pushes = sortedKeys.map((publicKey) => {
|
||||
if (publicKey.length !== COMPRESSED_PUBLIC_KEY_BYTES) {
|
||||
throw new Error("Expected compressed multisig public keys");
|
||||
}
|
||||
|
||||
return concatBytes([Uint8Array.of(COMPRESSED_PUBLIC_KEY_BYTES), publicKey]);
|
||||
});
|
||||
|
||||
return concatBytes([
|
||||
encodeScriptNumber(threshold),
|
||||
...pushes,
|
||||
encodeScriptNumber(sortedKeys.length),
|
||||
Uint8Array.of(OP_CHECKMULTISIG),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} descriptorText
|
||||
* @param {Object} options
|
||||
* @param {number} options.start
|
||||
* @param {number} options.count
|
||||
* @returns {Promise<GeneratedAddress[]>}
|
||||
*/
|
||||
export async function generateAddressesFromDescriptor(descriptorText, options) {
|
||||
const descriptor = parseOutputDescriptor(descriptorText);
|
||||
const parsedKeys = await Promise.all(
|
||||
descriptor.keys.map((key) => parseXpub(key.xpub)),
|
||||
);
|
||||
const network = parsedKeys[0].version.network;
|
||||
const childSets = await Promise.all(
|
||||
parsedKeys.map((key, index) => {
|
||||
if (key.version.network !== network) {
|
||||
throw new Error("Descriptor xpub networks must match");
|
||||
}
|
||||
|
||||
return derivePublicKeys(
|
||||
key,
|
||||
options.start,
|
||||
options.count,
|
||||
descriptor.keys[index].path,
|
||||
);
|
||||
}),
|
||||
);
|
||||
const addresses = /** @type {GeneratedAddress[]} */ ([]);
|
||||
|
||||
for (let offset = 0; offset < options.count; offset += 1) {
|
||||
const publicKeys = childSets.map((children) => children[offset].publicKey);
|
||||
const witnessScript = encodeSortedMultisigScript(
|
||||
publicKeys,
|
||||
descriptor.threshold,
|
||||
);
|
||||
const addressData = await encodeP2wshAddressData(witnessScript, network);
|
||||
|
||||
addresses.push({
|
||||
index: options.start + offset,
|
||||
address: addressData.address,
|
||||
payload: addressData.payload,
|
||||
script: descriptor.script,
|
||||
network,
|
||||
addrType: "v0_p2wsh",
|
||||
});
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import { createBytes } from "./bytes.js";
|
||||
|
||||
const ripemdLeftIndexes = /** @type {const} */ ([
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||
7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
|
||||
3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
|
||||
1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
|
||||
4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13,
|
||||
]);
|
||||
|
||||
const ripemdRightIndexes = /** @type {const} */ ([
|
||||
5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
|
||||
6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
|
||||
15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
|
||||
8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
|
||||
12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11,
|
||||
]);
|
||||
|
||||
const ripemdLeftShifts = /** @type {const} */ ([
|
||||
11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
|
||||
7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
|
||||
11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
|
||||
11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
|
||||
9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6,
|
||||
]);
|
||||
|
||||
const ripemdRightShifts = /** @type {const} */ ([
|
||||
8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
|
||||
9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
|
||||
9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
|
||||
15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
|
||||
8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11,
|
||||
]);
|
||||
|
||||
const ripemdLeftConstants = /** @type {const} */ ([
|
||||
0x00000000,
|
||||
0x5a827999,
|
||||
0x6ed9eba1,
|
||||
0x8f1bbcdc,
|
||||
0xa953fd4e,
|
||||
]);
|
||||
|
||||
const ripemdRightConstants = /** @type {const} */ ([
|
||||
0x50a28be6,
|
||||
0x5c4dd124,
|
||||
0x6d703ef3,
|
||||
0x7a6d76e9,
|
||||
0x00000000,
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
function toArrayBuffer(bytes) {
|
||||
const buffer = new ArrayBuffer(bytes.length);
|
||||
|
||||
new Uint8Array(buffer).set(bytes);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
export async function sha256(bytes) {
|
||||
return new Uint8Array(
|
||||
await crypto.subtle.digest("SHA-256", toArrayBuffer(bytes)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
async function doubleSha256(bytes) {
|
||||
return sha256(await sha256(bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} key
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
export async function hmacSha512(key, bytes) {
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
toArrayBuffer(key),
|
||||
{ name: "HMAC", hash: "SHA-512" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
return new Uint8Array(
|
||||
await crypto.subtle.sign("HMAC", cryptoKey, toArrayBuffer(bytes)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @param {number} bits
|
||||
*/
|
||||
function rotateLeft(value, bits) {
|
||||
return (value << bits) | (value >>> (32 - bits));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} round
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} z
|
||||
*/
|
||||
function ripemdFunction(round, x, y, z) {
|
||||
if (round < 16) return x ^ y ^ z;
|
||||
if (round < 32) return (x & y) | (~x & z);
|
||||
if (round < 48) return (x | ~y) ^ z;
|
||||
if (round < 64) return (x & z) | (y & ~z);
|
||||
|
||||
return x ^ (y | ~z);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
function createRipemdBlocks(bytes) {
|
||||
const bitLength = BigInt(bytes.length) * 8n;
|
||||
const length = bytes.length + 1 + 8;
|
||||
const paddedLength = Math.ceil(length / 64) * 64;
|
||||
const padded = createBytes(paddedLength);
|
||||
|
||||
padded.set(bytes);
|
||||
padded[bytes.length] = 0x80;
|
||||
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
padded[paddedLength - 8 + i] = Number(
|
||||
(bitLength >> (BigInt(i) * 8n)) & 0xffn,
|
||||
);
|
||||
}
|
||||
|
||||
return padded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} block
|
||||
* @param {number} offset
|
||||
*/
|
||||
function readRipemdWords(block, offset) {
|
||||
const words = /** @type {number[]} */ ([]);
|
||||
|
||||
for (let i = 0; i < 16; i += 1) {
|
||||
const start = offset + i * 4;
|
||||
words.push(
|
||||
block[start] |
|
||||
(block[start + 1] << 8) |
|
||||
(block[start + 2] << 16) |
|
||||
(block[start + 3] << 24),
|
||||
);
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} target
|
||||
* @param {number} offset
|
||||
* @param {number} value
|
||||
*/
|
||||
function writeRipemdWord(target, offset, value) {
|
||||
target[offset] = value;
|
||||
target[offset + 1] = value >>> 8;
|
||||
target[offset + 2] = value >>> 16;
|
||||
target[offset + 3] = value >>> 24;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
function ripemd160(bytes) {
|
||||
const blocks = createRipemdBlocks(bytes);
|
||||
const digest = createBytes(20);
|
||||
let h0 = 0x67452301;
|
||||
let h1 = 0xefcdab89;
|
||||
let h2 = 0x98badcfe;
|
||||
let h3 = 0x10325476;
|
||||
let h4 = 0xc3d2e1f0;
|
||||
|
||||
for (let offset = 0; offset < blocks.length; offset += 64) {
|
||||
const words = readRipemdWords(blocks, offset);
|
||||
let al = h0;
|
||||
let bl = h1;
|
||||
let cl = h2;
|
||||
let dl = h3;
|
||||
let el = h4;
|
||||
let ar = h0;
|
||||
let br = h1;
|
||||
let cr = h2;
|
||||
let dr = h3;
|
||||
let er = h4;
|
||||
|
||||
for (let round = 0; round < 80; round += 1) {
|
||||
const leftGroup = Math.floor(round / 16);
|
||||
const rightGroup = Math.floor(round / 16);
|
||||
const nextLeft =
|
||||
(rotateLeft(
|
||||
(al +
|
||||
ripemdFunction(round, bl, cl, dl) +
|
||||
words[ripemdLeftIndexes[round]] +
|
||||
ripemdLeftConstants[leftGroup]) |
|
||||
0,
|
||||
ripemdLeftShifts[round],
|
||||
) +
|
||||
el) |
|
||||
0;
|
||||
const nextRight =
|
||||
(rotateLeft(
|
||||
(ar +
|
||||
ripemdFunction(79 - round, br, cr, dr) +
|
||||
words[ripemdRightIndexes[round]] +
|
||||
ripemdRightConstants[rightGroup]) |
|
||||
0,
|
||||
ripemdRightShifts[round],
|
||||
) +
|
||||
er) |
|
||||
0;
|
||||
|
||||
al = el;
|
||||
el = dl;
|
||||
dl = rotateLeft(cl, 10);
|
||||
cl = bl;
|
||||
bl = nextLeft;
|
||||
|
||||
ar = er;
|
||||
er = dr;
|
||||
dr = rotateLeft(cr, 10);
|
||||
cr = br;
|
||||
br = nextRight;
|
||||
}
|
||||
|
||||
const nextH0 = (h1 + cl + dr) | 0;
|
||||
h1 = (h2 + dl + er) | 0;
|
||||
h2 = (h3 + el + ar) | 0;
|
||||
h3 = (h4 + al + br) | 0;
|
||||
h4 = (h0 + bl + cr) | 0;
|
||||
h0 = nextH0;
|
||||
}
|
||||
|
||||
writeRipemdWord(digest, 0, h0);
|
||||
writeRipemdWord(digest, 4, h1);
|
||||
writeRipemdWord(digest, 8, h2);
|
||||
writeRipemdWord(digest, 12, h3);
|
||||
writeRipemdWord(digest, 16, h4);
|
||||
|
||||
return digest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
export async function hash160(bytes) {
|
||||
return ripemd160(await sha256(bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} bytes
|
||||
*/
|
||||
export async function checksum(bytes) {
|
||||
return (await doubleSha256(bytes)).slice(0, 4);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { encodePublicKeyAddressData } from "./address.js";
|
||||
import { derivePublicKeys, parseXpub } from "./bip32.js";
|
||||
import {
|
||||
generateAddressesFromDescriptor,
|
||||
getOutputDescriptorBranchIds,
|
||||
isOutputDescriptor,
|
||||
selectOutputDescriptor,
|
||||
} from "./descriptor.js";
|
||||
|
||||
const DEFAULT_START_INDEX = 0;
|
||||
const DEFAULT_ADDRESS_COUNT = 20;
|
||||
const MAX_ADDRESS_COUNT = 100;
|
||||
|
||||
const addrTypeByScript = /** @type {const} */ ({
|
||||
p2pkh: "p2pkh",
|
||||
p2sh_p2wpkh: "p2sh",
|
||||
v0_p2wpkh: "v0_p2wpkh",
|
||||
v1_p2tr: "v1_p2tr",
|
||||
v0_p2wsh_sortedmulti: "v0_p2wsh",
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import("./address.js").AddressScript} AddressScript
|
||||
* @typedef {import("./address.js").BitcoinNetwork} BitcoinNetwork
|
||||
* @typedef {(typeof addrTypeByScript)[keyof typeof addrTypeByScript]} AddressType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GeneratedAddress
|
||||
* @property {number} index
|
||||
* @property {string} address
|
||||
* @property {Uint8Array} payload
|
||||
* @property {Uint8Array} [publicKey]
|
||||
* @property {AddressScript} script
|
||||
* @property {BitcoinNetwork} network
|
||||
* @property {AddressType} addrType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} GenerateAddressesOptions
|
||||
* @property {number} [start]
|
||||
* @property {number} [count]
|
||||
* @property {AddressScript} [script]
|
||||
* @property {readonly number[]} [path]
|
||||
* @property {string} [branchId]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number | undefined} value
|
||||
*/
|
||||
function readStart(value) {
|
||||
if (value === undefined) return DEFAULT_START_INDEX;
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new Error("Expected a non-negative start index");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number | undefined} value
|
||||
*/
|
||||
function readCount(value) {
|
||||
const count = value ?? DEFAULT_ADDRESS_COUNT;
|
||||
|
||||
if (!Number.isSafeInteger(count) || count < 1 || count > MAX_ADDRESS_COUNT) {
|
||||
throw new Error(`Expected an address count from 1 to ${MAX_ADDRESS_COUNT}`);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {GenerateAddressesOptions} [options]
|
||||
* @returns {Promise<GeneratedAddress[]>}
|
||||
*/
|
||||
export async function generateAddressesFromKey(source, options = {}) {
|
||||
const key = await parseXpub(source);
|
||||
const start = readStart(options.start);
|
||||
const count = readCount(options.count);
|
||||
const script = options.script ?? key.version.script;
|
||||
const addrType = addrTypeByScript[script];
|
||||
const children = await derivePublicKeys(key, start, count, options.path);
|
||||
const addresses = /** @type {GeneratedAddress[]} */ ([]);
|
||||
|
||||
for (const child of children) {
|
||||
const addressData = await encodePublicKeyAddressData(
|
||||
child.publicKey,
|
||||
script,
|
||||
key.version.network,
|
||||
);
|
||||
|
||||
addresses.push({
|
||||
index: child.index,
|
||||
address: addressData.address,
|
||||
payload: addressData.payload,
|
||||
publicKey: child.publicKey,
|
||||
script,
|
||||
network: key.version.network,
|
||||
addrType,
|
||||
});
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @param {GenerateAddressesOptions} [options]
|
||||
* @returns {Promise<GeneratedAddress[]>}
|
||||
*/
|
||||
export async function generateAddressesFromWalletSource(source, options = {}) {
|
||||
const start = readStart(options.start);
|
||||
const count = readCount(options.count);
|
||||
|
||||
if (isOutputDescriptor(source)) {
|
||||
return generateAddressesFromDescriptor(
|
||||
selectOutputDescriptor(source, options.branchId),
|
||||
{ start, count },
|
||||
);
|
||||
}
|
||||
|
||||
return generateAddressesFromKey(source, {
|
||||
...options,
|
||||
start,
|
||||
count,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
getOutputDescriptorBranchIds,
|
||||
isOutputDescriptor,
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { decodeBase58Check } from "./base58.js";
|
||||
import { readUint32 } from "./bytes.js";
|
||||
|
||||
const EXTENDED_PUBLIC_KEY_LENGTH = 78;
|
||||
const PUBLIC_KEY_LENGTH = 33;
|
||||
const CHAIN_CODE_LENGTH = 32;
|
||||
|
||||
const extendedPublicKeyVersions = /** @type {const} */ ([
|
||||
{
|
||||
version: 0x0488b21e,
|
||||
prefix: "xpub",
|
||||
network: "mainnet",
|
||||
script: "p2pkh",
|
||||
addrType: "p2pkh",
|
||||
},
|
||||
{
|
||||
version: 0x049d7cb2,
|
||||
prefix: "ypub",
|
||||
network: "mainnet",
|
||||
script: "p2sh_p2wpkh",
|
||||
addrType: "p2sh",
|
||||
},
|
||||
{
|
||||
version: 0x04b24746,
|
||||
prefix: "zpub",
|
||||
network: "mainnet",
|
||||
script: "v0_p2wpkh",
|
||||
addrType: "v0_p2wpkh",
|
||||
},
|
||||
{
|
||||
version: 0x043587cf,
|
||||
prefix: "tpub",
|
||||
network: "testnet",
|
||||
script: "p2pkh",
|
||||
addrType: "p2pkh",
|
||||
},
|
||||
{
|
||||
version: 0x044a5262,
|
||||
prefix: "upub",
|
||||
network: "testnet",
|
||||
script: "p2sh_p2wpkh",
|
||||
addrType: "p2sh",
|
||||
},
|
||||
{
|
||||
version: 0x045f1cf6,
|
||||
prefix: "vpub",
|
||||
network: "testnet",
|
||||
script: "v0_p2wpkh",
|
||||
addrType: "v0_p2wpkh",
|
||||
},
|
||||
]);
|
||||
|
||||
/**
|
||||
* @typedef {typeof extendedPublicKeyVersions[number]} ExtendedPublicKeyVersion
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ExtendedPublicKey
|
||||
* @property {string} text
|
||||
* @property {number} depth
|
||||
* @property {number} childNumber
|
||||
* @property {Uint8Array} parentFingerprint
|
||||
* @property {Uint8Array} chainCode
|
||||
* @property {Uint8Array} publicKey
|
||||
* @property {ExtendedPublicKeyVersion} version
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number} version
|
||||
* @returns {ExtendedPublicKeyVersion}
|
||||
*/
|
||||
function findExtendedPublicKeyVersion(version) {
|
||||
const metadata = extendedPublicKeyVersions.find((item) => {
|
||||
return item.version === version;
|
||||
});
|
||||
|
||||
if (!metadata) {
|
||||
throw new Error(`Unsupported extended public key version: ${version}`);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} publicKey
|
||||
*/
|
||||
function validateCompressedPublicKey(publicKey) {
|
||||
if (
|
||||
publicKey.length !== PUBLIC_KEY_LENGTH ||
|
||||
(publicKey[0] !== 0x02 && publicKey[0] !== 0x03)
|
||||
) {
|
||||
throw new Error("Expected a compressed public key");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {Promise<ExtendedPublicKey>}
|
||||
*/
|
||||
export async function parseExtendedPublicKey(text) {
|
||||
const value = text.trim();
|
||||
const bytes = await decodeBase58Check(value);
|
||||
|
||||
if (bytes.length !== EXTENDED_PUBLIC_KEY_LENGTH) {
|
||||
throw new Error("Invalid extended public key length");
|
||||
}
|
||||
|
||||
const version = findExtendedPublicKeyVersion(readUint32(bytes, 0));
|
||||
const parentFingerprint = bytes.slice(5, 9);
|
||||
const chainCode = bytes.slice(13, 13 + CHAIN_CODE_LENGTH);
|
||||
const publicKey = bytes.slice(45);
|
||||
|
||||
validateCompressedPublicKey(publicKey);
|
||||
|
||||
return {
|
||||
text: value,
|
||||
depth: bytes[4],
|
||||
childNumber: readUint32(bytes, 9),
|
||||
parentFingerprint,
|
||||
chainCode,
|
||||
publicKey,
|
||||
version,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const addressScripts = /** @type {const} */ ([
|
||||
{ id: "v0_p2wpkh", label: "P2WPKH" },
|
||||
{ id: "v1_p2tr", label: "P2TR" },
|
||||
{ id: "p2sh_p2wpkh", label: "Nested P2WPKH" },
|
||||
{ id: "p2pkh", label: "P2PKH" },
|
||||
]);
|
||||
@@ -0,0 +1,255 @@
|
||||
import { bigIntToBytes, bytesToBigInt, createBytes } from "./bytes.js";
|
||||
|
||||
const FIELD_PRIME =
|
||||
0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
|
||||
const GROUP_ORDER =
|
||||
0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
||||
const GENERATOR = /** @type {const} */ ({
|
||||
x: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
|
||||
y: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} Secp256k1Point
|
||||
* @property {bigint} x
|
||||
* @property {bigint} y
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {bigint} value
|
||||
* @param {bigint} modulo
|
||||
*/
|
||||
function mod(value, modulo) {
|
||||
const result = value % modulo;
|
||||
|
||||
return result >= 0n ? result : result + modulo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bigint} value
|
||||
* @param {bigint} exponent
|
||||
* @param {bigint} modulo
|
||||
*/
|
||||
function modPow(value, exponent, modulo) {
|
||||
let result = 1n;
|
||||
let base = mod(value, modulo);
|
||||
let power = exponent;
|
||||
|
||||
while (power > 0n) {
|
||||
if (power & 1n) result = mod(result * base, modulo);
|
||||
|
||||
base = mod(base * base, modulo);
|
||||
power >>= 1n;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bigint} value
|
||||
*/
|
||||
function invertField(value) {
|
||||
let low = mod(value, FIELD_PRIME);
|
||||
let high = FIELD_PRIME;
|
||||
let lowCoefficient = 1n;
|
||||
let highCoefficient = 0n;
|
||||
|
||||
while (low > 1n) {
|
||||
const ratio = high / low;
|
||||
|
||||
[low, high] = [high - low * ratio, low];
|
||||
[lowCoefficient, highCoefficient] = [
|
||||
highCoefficient - lowCoefficient * ratio,
|
||||
lowCoefficient,
|
||||
];
|
||||
}
|
||||
|
||||
return mod(lowCoefficient, FIELD_PRIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Secp256k1Point} point
|
||||
*/
|
||||
function isOnCurve(point) {
|
||||
const left = mod(point.y * point.y, FIELD_PRIME);
|
||||
const right = mod(point.x * point.x * point.x + 7n, FIELD_PRIME);
|
||||
|
||||
return left === right;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Secp256k1Point} point
|
||||
*/
|
||||
function doublePoint(point) {
|
||||
if (point.y === 0n) return null;
|
||||
|
||||
const slope = mod(
|
||||
3n * point.x * point.x * invertField(2n * point.y),
|
||||
FIELD_PRIME,
|
||||
);
|
||||
const x = mod(slope * slope - 2n * point.x, FIELD_PRIME);
|
||||
const y = mod(slope * (point.x - x) - point.y, FIELD_PRIME);
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Secp256k1Point} point
|
||||
*/
|
||||
function negatePoint(point) {
|
||||
return { x: point.x, y: mod(-point.y, FIELD_PRIME) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Secp256k1Point} point
|
||||
*/
|
||||
function forceEvenY(point) {
|
||||
return point.y & 1n ? negatePoint(point) : point;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Secp256k1Point | null} left
|
||||
* @param {Secp256k1Point | null} right
|
||||
* @returns {Secp256k1Point | null}
|
||||
*/
|
||||
function addPoints(left, right) {
|
||||
if (!left) return right;
|
||||
if (!right) return left;
|
||||
|
||||
if (left.x === right.x) {
|
||||
if (mod(left.y + right.y, FIELD_PRIME) === 0n) return null;
|
||||
|
||||
return doublePoint(left);
|
||||
}
|
||||
|
||||
const slope = mod(
|
||||
(right.y - left.y) * invertField(right.x - left.x),
|
||||
FIELD_PRIME,
|
||||
);
|
||||
const x = mod(slope * slope - left.x - right.x, FIELD_PRIME);
|
||||
const y = mod(slope * (left.x - x) - left.y, FIELD_PRIME);
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bigint} scalar
|
||||
* @param {Secp256k1Point} point
|
||||
* @returns {Secp256k1Point | null}
|
||||
*/
|
||||
function multiplyPoint(scalar, point) {
|
||||
let result = /** @type {Secp256k1Point | null} */ (null);
|
||||
let addend = /** @type {Secp256k1Point | null} */ (point);
|
||||
let remaining = scalar;
|
||||
|
||||
while (remaining > 0n) {
|
||||
if (remaining & 1n) result = addPoints(result, addend);
|
||||
|
||||
addend = addPoints(addend, addend);
|
||||
remaining >>= 1n;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bigint} x
|
||||
* @param {boolean} odd
|
||||
*/
|
||||
function liftX(x, odd) {
|
||||
if (x >= FIELD_PRIME) {
|
||||
throw new Error("Invalid secp256k1 x coordinate");
|
||||
}
|
||||
|
||||
let y = modPow(x * x * x + 7n, (FIELD_PRIME + 1n) / 4n, FIELD_PRIME);
|
||||
|
||||
if (Boolean(y & 1n) !== odd) {
|
||||
y = FIELD_PRIME - y;
|
||||
}
|
||||
|
||||
const point = { x, y };
|
||||
|
||||
if (!isOnCurve(point)) {
|
||||
throw new Error("Invalid secp256k1 point");
|
||||
}
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} publicKey
|
||||
*/
|
||||
function parseCompressedPublicKey(publicKey) {
|
||||
if (
|
||||
publicKey.length !== 33 ||
|
||||
(publicKey[0] !== 0x02 && publicKey[0] !== 0x03)
|
||||
) {
|
||||
throw new Error("Expected a compressed public key");
|
||||
}
|
||||
|
||||
return liftX(bytesToBigInt(publicKey.slice(1)), publicKey[0] === 0x03);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Secp256k1Point} point
|
||||
*/
|
||||
function compressPublicKey(point) {
|
||||
const publicKey = createBytes(33);
|
||||
|
||||
publicKey[0] = point.y & 1n ? 0x03 : 0x02;
|
||||
publicKey.set(bigIntToBytes(point.x, 32), 1);
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} publicKey
|
||||
*/
|
||||
export function getXOnlyPublicKey(publicKey) {
|
||||
const point = forceEvenY(parseCompressedPublicKey(publicKey));
|
||||
|
||||
return bigIntToBytes(point.x, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} publicKey
|
||||
* @param {Uint8Array} tweak
|
||||
*/
|
||||
export function addPublicKeyTweak(publicKey, tweak) {
|
||||
const scalar = bytesToBigInt(tweak);
|
||||
|
||||
if (scalar === 0n || scalar >= GROUP_ORDER) {
|
||||
throw new Error("Invalid secp256k1 public key tweak");
|
||||
}
|
||||
|
||||
const tweakPoint = multiplyPoint(scalar, GENERATOR);
|
||||
const childPoint = addPoints(parseCompressedPublicKey(publicKey), tweakPoint);
|
||||
|
||||
if (!childPoint) {
|
||||
throw new Error("Invalid secp256k1 child public key");
|
||||
}
|
||||
|
||||
return compressPublicKey(childPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} publicKey
|
||||
* @param {Uint8Array} tweak
|
||||
*/
|
||||
export function addXOnlyPublicKeyTweak(publicKey, tweak) {
|
||||
const scalar = bytesToBigInt(tweak);
|
||||
|
||||
if (scalar >= GROUP_ORDER) {
|
||||
throw new Error("Invalid secp256k1 x-only public key tweak");
|
||||
}
|
||||
|
||||
const internalPoint = forceEvenY(parseCompressedPublicKey(publicKey));
|
||||
const tweakPoint = scalar === 0n ? null : multiplyPoint(scalar, GENERATOR);
|
||||
const outputPoint = addPoints(internalPoint, tweakPoint);
|
||||
|
||||
if (!outputPoint) {
|
||||
throw new Error("Invalid secp256k1 x-only child public key");
|
||||
}
|
||||
|
||||
return bigIntToBytes(outputPoint.x, 32);
|
||||
}
|
||||
Reference in New Issue
Block a user