src/bip/bip49.js

/**
 * @fileoverview BIP49 - Wrapped SegWit Address Generation
 * @description P2SH-P2WPKH (3... addresses) derivation and address generation
 * @version 1.0.0
 * @author yfbsei
 * @license ISC
 */

import { createHash } from 'node:crypto';
import rmd160 from '../core/crypto/hash/ripemd160.js';
import { encodeP2SH } from '../encoding/address/encode.js';
import { ScriptBuilder } from '../transaction/script-builder.js';

/**
 * Hash160 (SHA256 + RIPEMD160)
 * @param {Buffer} data - Data to hash
 * @returns {Buffer} 20-byte hash
 */
function hash160(data) {
    const sha = createHash('sha256').update(data).digest();
    return rmd160(sha);
}

/**
 * BIP49 Derivation Path Constants
 * @constant {Object}
 */
const BIP49_CONSTANTS = {
    PURPOSE: 49,
    MAINNET_PATH: "m/49'/0'/0'",
    TESTNET_PATH: "m/49'/1'/0'"
};

/**
 * BIP49 utilities for wrapped SegWit addresses
 * @class BIP49
 */
class BIP49 {
    /**
     * Create P2SH-P2WPKH address from public key
     * @param {Buffer|string} publicKey - Compressed public key (33 bytes)
     * @param {string} [network='main'] - Network type
     * @returns {string} P2SH address (3... or 2...)
     */
    static toAddress(publicKey, network = 'main') {
        const pubKeyBuffer = Buffer.isBuffer(publicKey)
            ? publicKey
            : Buffer.from(publicKey, 'hex');

        if (pubKeyBuffer.length !== 33) {
            throw new Error('BIP49 requires 33-byte compressed public key');
        }

        // Create P2WPKH script: OP_0 <20-byte pubkey hash>
        const pubkeyHash = hash160(pubKeyBuffer);
        const p2wpkhScript = ScriptBuilder.createP2WPKH(pubkeyHash);

        // Hash the P2WPKH script to create P2SH script hash
        const scriptHash = hash160(p2wpkhScript);

        // Encode as P2SH address
        return encodeP2SH(scriptHash, network);
    }

    /**
     * Create redeem script for P2SH-P2WPKH
     * @param {Buffer|string} publicKey - Compressed public key
     * @returns {Buffer} Redeem script (P2WPKH script)
     */
    static createRedeemScript(publicKey) {
        const pubKeyBuffer = Buffer.isBuffer(publicKey)
            ? publicKey
            : Buffer.from(publicKey, 'hex');

        const pubkeyHash = hash160(pubKeyBuffer);
        return ScriptBuilder.createP2WPKH(pubkeyHash);
    }

    /**
     * Create scriptPubKey for P2SH-P2WPKH address
     * @param {Buffer|string} publicKey - Compressed public key
     * @returns {Buffer} P2SH scriptPubKey
     */
    static createScriptPubKey(publicKey) {
        const redeemScript = this.createRedeemScript(publicKey);
        const scriptHash = hash160(redeemScript);
        return ScriptBuilder.createP2SH(scriptHash);
    }

    /**
     * Get derivation path for BIP49
     * @param {string} [network='main'] - Network type
     * @param {number} [account=0] - Account index
     * @param {number} [change=0] - Change (0=external, 1=internal)
     * @param {number} [index=0] - Address index
     * @returns {string} Derivation path
     */
    static getDerivationPath(network = 'main', account = 0, change = 0, index = 0) {
        const coinType = network === 'main' ? 0 : 1;
        return `m/49'/${coinType}'/${account}'/${change}/${index}`;
    }

    /**
     * Get account-level derivation path
     * @param {string} [network='main'] - Network type
     * @param {number} [account=0] - Account index
     * @returns {string} Account path
     */
    static getAccountPath(network = 'main', account = 0) {
        const coinType = network === 'main' ? 0 : 1;
        return `m/49'/${coinType}'/${account}'`;
    }
}

export {
    BIP49,
    BIP49_CONSTANTS,
    hash160
};

export default BIP49;