git / moonbite-network / static/moonbite-address.js · raw

/* MoonBite address derivation — standard native segwit (P2WPKH), matching * MoonBite Core exactly: address = bech32("moon", 0, RIPEMD160(SHA256(pubkey))) * with the COMPRESSED public key. Verified against the node's wpkh descriptor. * * New wallets use BIP39 (12 words) -> BIP84 m/84'/2'/0'/0/0. A phrase that is * not valid BIP39 (an older imported phrase) still yields a real P2WPKH * address via a legacy scalar derivation, so nothing is stranded. */import { getPublicKey } from './vendor/noble-secp256k1.js?v=20260904a';import { mnemonicToSeed, validateMnemonic } from './moonbite-bip39.js?v=20260904a';import { deriveKey, hash160, bytesToHex, hexToBytes } from './moonbite-hd.js?v=20260904a';const SEED_DERIVATION_PREFIX = 'moonbite-seed-v1:';const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';const MOONBITE_HRP = 'moon';const WITVER = 0;const CURVE_ORDER =    BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');export function normalizeSeedPhrase(phrase) {    return (phrase || '').normalize('NFKD').trim().replace(/\s+/g, ' ').toLowerCase();}async function sha256(bytes) { return new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)); }function bech32Polymod(values) {    const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];    let chk = 1;    for (const v of values) {        const b = chk >> 25;        chk = ((chk & 0x1ffffff) << 5) ^ v;        for (let i = 0; i < 5; i++) if ((b >> i) & 1) chk ^= GEN[i];    }    return chk;}function bech32HrpExpand(hrp) {    const out = [];    for (const c of hrp) out.push(c.charCodeAt(0) >> 5);    out.push(0);    for (const c of hrp) out.push(c.charCodeAt(0) & 31);    return out;}function bech32CreateChecksum(hrp, data) {    const values = bech32HrpExpand(hrp).concat(data, [0, 0, 0, 0, 0, 0]);    const polymod = bech32Polymod(values) ^ 1;    const out = [];    for (let i = 0; i < 6; i++) out.push((polymod >> (5 * (5 - i))) & 31);    return out;}function bech32Encode(hrp, data) {    const combined = data.concat(bech32CreateChecksum(hrp, data));    let s = hrp + '1';    for (const d of combined) s += BECH32_CHARSET[d];    return s;}function bech32Decode(addr) {    if (typeof addr !== 'string') throw new Error('address must be a string');    // BIP-173: max 90 chars, no mixed case, every char printable ASCII 33-126.    if (addr.length < 8 || addr.length > 90) throw new Error('bad bech32 length');    for (let i = 0; i < addr.length; i++) {        const c = addr.charCodeAt(i);        if (c < 33 || c > 126) throw new Error('invalid bech32 char');    }    const lower = addr.toLowerCase(), upper = addr.toUpperCase();    if (addr !== lower && addr !== upper) throw new Error('mixed-case bech32');    addr = lower;    const pos = addr.lastIndexOf('1');    if (pos < 1) throw new Error('missing bech32 separator');    if (pos + 7 > addr.length) throw new Error('bech32 data too short');    const hrp = addr.slice(0, pos);    const data = [];    for (const c of addr.slice(pos + 1)) {        const d = BECH32_CHARSET.indexOf(c);        if (d < 0) throw new Error('invalid bech32 char');        data.push(d);    }    if (bech32Polymod(bech32HrpExpand(hrp).concat(data)) !== 1) throw new Error('bad checksum');    return { hrp, data: data.slice(0, -6) };}function convertBits(data, fromBits, toBits, pad) {    let acc = 0, bits = 0;    const out = [];    const maxv = (1 << toBits) - 1;    for (const value of data) {        acc = (acc << fromBits) | value;        bits += fromBits;        while (bits >= toBits) { bits -= toBits; out.push((acc >> bits) & maxv); }    }    if (pad) { if (bits) out.push((acc << (toBits - bits)) & maxv); }    else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) return null;    return out;}/* 20-byte pubkey-hash (hex) -> moon1 P2WPKH address. */export function addressFromPubkeyHash(pkhHex, hrp = MOONBITE_HRP) {    const prog = convertBits(hexToBytes(pkhHex), 8, 5, true);    if (prog === null) throw new Error('cannot encode pubkey hash');    return bech32Encode(hrp, [WITVER].concat(prog));}/* moon1 address -> 20-byte pubkey-hash (hex). */export function pubkeyHashFromAddress(addr) {    const { hrp, data } = bech32Decode(addr);    if (hrp !== MOONBITE_HRP) throw new Error('not a MoonBite address');    if (data[0] !== WITVER) throw new Error('unsupported witness version');    const prog = convertBits(data.slice(1), 5, 8, false);    if (prog === null || prog.length !== 20) throw new Error('bad program length');    return bytesToHex(new Uint8Array(prog));}export function isValidAddress(addr) {    try { pubkeyHashFromAddress(addr); return true; } catch (e) { return false; }}/* Optional extra password (the standard BIP39 passphrase, a.k.a. "25th word"). * * The wallet holds ONE secret string in memory and in its encrypted blobs. * Without a password that string is just the phrase, exactly as before. With * one, it is SECRET_PACK + JSON {m: phrase, p: password}. The marker starts * with NUL, which no typed phrase can contain, so a packed secret can never be * mistaken for a phrase (or vice versa). The password is used verbatim apart * from NFKD (BIP39): case, spaces and symbols all count. An empty password is * byte-for-byte today's wallet. */export const SECRET_PACK = '\u0000mbp1\u0000';export function packSecret(phrase, passphrase = '') {    return passphrase ? SECRET_PACK + JSON.stringify({ m: phrase, p: passphrase }) : phrase;}export function unpackSecret(secret) {    if (typeof secret === 'string' && secret.startsWith(SECRET_PACK)) {        const o = JSON.parse(secret.slice(SECRET_PACK.length));        return { phrase: String(o.m || ''), passphrase: String(o.p || '') };    }    return { phrase: secret, passphrase: '' };}/* Private key (hex) for a phrase (or packed secret): BIP84 for valid BIP39, * else legacy scalar. `passphrase` applies to a plain phrase; a packed secret * carries its own. A password only exists for standard BIP39 phrases. */export async function privkeyFromSeedPhrase(secret, passphrase = '') {    const u = unpackSecret(secret);    const pass = u.passphrase || passphrase || '';    const normalized = normalizeSeedPhrase(u.phrase);    if (!normalized) throw new Error('seed phrase is empty');    if (await validateMnemonic(normalized)) {        const seed = await mnemonicToSeed(normalized, pass);        return (await deriveKey(seed, 0)).privkey;    }    if (pass) throw new Error('An extra password only works with a standard 12-word recovery phrase.');    const material = new TextEncoder().encode(SEED_DERIVATION_PREFIX + normalized);    const digest = await sha256(material);    const scalar = BigInt('0x' + bytesToHex(digest)) % CURVE_ORDER;    if (scalar === 0n) throw new Error('degenerate key for this seed phrase');    return scalar.toString(16).padStart(64, '0');}/* Full derivation -> { private_key, public_key(compressed), pubkey_hash, address }. */export async function deriveFromSeedPhrase(phrase, passphrase = '') {    const privkeyHex = await privkeyFromSeedPhrase(phrase, passphrase);    const compressed = getPublicKey(hexToBytes(privkeyHex), true);    const pubkeyHex = bytesToHex(compressed);    const pkhHex = bytesToHex(await hash160(compressed));    return {        private_key: privkeyHex,        public_key: pubkeyHex,        pubkey_hash: pkhHex,        address: addressFromPubkeyHash(pkhHex),    };}