j-bitcoin
Version:
Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features
588 lines (528 loc) • 20 kB
JavaScript
/**
* @fileoverview Custodial Bitcoin Wallet implementation
* @description Full-featured HD wallet with all address types and transaction signing
* @version 2.0.0
* @author yfbsei
* @license ISC
*/
import { BIP39 } from '../bip/bip39/mnemonic.js';
import { generateMasterKey } from '../bip/bip32/master-key.js';
import { derive } from '../bip/bip32/derive.js';
import { BECH32 } from '../bip/BIP173-BIP350.js';
import { ECDSA } from '../core/crypto/signatures/ecdsa.js';
import { encodeP2PKH, encodeP2SH, encodeWIF, hash160 } from '../encoding/address/encode.js';
import { decodeWIFPrivateKey } from '../encoding/address/decode.js';
import { NETWORK_VERSIONS, BIP_PURPOSES } from '../core/constants.js';
import { TransactionBuilder } from '../transaction/builder.js';
import { ScriptBuilder } from '../transaction/script-builder.js';
import { BIP322 } from '../transaction/message-signing.js';
/**
* Custom error class for custodial wallet operations
* @class CustodialWalletError
* @extends Error
*/
class CustodialWalletError extends Error {
/**
* Create a custodial wallet error
* @param {string} message - Error message
* @param {string} [solution=''] - Suggested solution for the error
*/
constructor(message, solution = '') {
super(message);
/** @type {string} */
this.name = 'CustodialWalletError';
/** @type {string} */
this.solution = solution;
/** @type {string} */
this.timestamp = new Date().toISOString();
}
}
/**
* Address type mapping to BIP purpose
* @constant {Object}
*/
const ADDRESS_TYPE_PURPOSE = {
legacy: 44, // BIP44 - P2PKH
'wrapped-segwit': 49, // BIP49 - P2SH-P2WPKH
segwit: 84, // BIP84 - P2WPKH
taproot: 86 // BIP86 - P2TR
};
/**
* Custodial Bitcoin wallet with full key management
* @class CustodialWallet
* @description Implements a fully-featured custodial HD wallet supporting
* BIP32/39/44/49/84/86 standards with legacy, wrapped SegWit, native SegWit,
* and Taproot address support.
*
* @example
* // Create a new wallet
* const { wallet, mnemonic } = CustodialWallet.createNew('main');
* console.log('Backup phrase:', mnemonic);
*
* @example
* // Get different address types
* const legacy = wallet.getReceivingAddress(0, 0, 'legacy'); // 1...
* const wrapped = wallet.getReceivingAddress(0, 0, 'wrapped-segwit'); // 3...
* const native = wallet.getReceivingAddress(0, 0, 'segwit'); // bc1q...
* const taproot = wallet.getReceivingAddress(0, 0, 'taproot'); // bc1p...
*/
class CustodialWallet {
/**
* Create a custodial wallet instance
* @param {string} network - Network type ('main' or 'test')
* @param {Object} masterKeys - Master key pair
* @param {string} masterKeys.extendedPrivateKey - BIP32 extended private key (xprv/tprv)
* @param {string} masterKeys.extendedPublicKey - BIP32 extended public key (xpub/tpub)
* @param {string|null} [mnemonic=null] - BIP39 mnemonic phrase
*/
constructor(network, masterKeys, mnemonic = null) {
/** @type {string} */
this.network = network === 'main' ? 'main' : 'test';
/** @type {Object} */
this.masterKeys = masterKeys;
/** @type {string|null} */
this.mnemonic = mnemonic;
/** @type {Map<string, Object>} */
this.derivedAddresses = new Map();
/** @type {string} */
this.version = '2.0.0';
/** @type {number} */
this.created = Date.now();
}
/**
* Create a new wallet with a fresh mnemonic
* @static
* @param {string} [network='main'] - Network type ('main' or 'test')
* @param {number} [strength=256] - Mnemonic strength (128, 160, 192, 224, 256)
* @returns {{wallet: CustodialWallet, mnemonic: string}} New wallet and backup mnemonic
* @throws {CustodialWalletError} If wallet creation fails
* @example
* const { wallet, mnemonic } = CustodialWallet.createNew('main');
*/
static createNew(network = 'main', strength = 256) {
try {
const mnemonicResult = BIP39.generateMnemonic(strength);
const mnemonic = mnemonicResult.mnemonic;
const seed = BIP39.deriveSeed(mnemonic);
const [masterKeys] = generateMasterKey(seed, network);
const wallet = new CustodialWallet(network, masterKeys, mnemonic);
return { wallet, mnemonic };
} catch (error) {
throw new CustodialWalletError(
`Failed to create wallet: ${error.message}`,
'Ensure crypto module is available'
);
}
}
/**
* Restore a wallet from a BIP39 mnemonic phrase
* @static
* @param {string} network - Network type ('main' or 'test')
* @param {string} mnemonic - BIP39 mnemonic phrase (12-24 words)
* @param {string} [passphrase=''] - Optional BIP39 passphrase
* @returns {CustodialWallet} Restored wallet instance
* @throws {CustodialWalletError} If mnemonic is invalid
* @example
* const wallet = CustodialWallet.fromMnemonic('main', 'abandon abandon abandon...');
*/
static fromMnemonic(network, mnemonic, passphrase = '') {
try {
if (!BIP39.validateChecksum(mnemonic)) {
throw new Error('Invalid mnemonic checksum');
}
const seed = BIP39.deriveSeed(mnemonic, passphrase);
const [masterKeys] = generateMasterKey(seed, network);
return new CustodialWallet(network, masterKeys, mnemonic);
} catch (error) {
throw new CustodialWalletError(
`Failed to restore wallet: ${error.message}`,
'Verify mnemonic phrase is correct'
);
}
}
/**
* Create a wallet from a raw seed
* @static
* @param {string} network - Network type ('main' or 'test')
* @param {string|Buffer} seed - 64-byte seed as hex string or Buffer
* @returns {CustodialWallet} Wallet instance
* @throws {CustodialWalletError} If seed is invalid
* @example
* const wallet = CustodialWallet.fromSeed('main', seedHex);
*/
static fromSeed(network, seed) {
try {
const [masterKeys] = generateMasterKey(seed, network);
return new CustodialWallet(network, masterKeys, null);
} catch (error) {
throw new CustodialWalletError(
`Failed to create wallet from seed: ${error.message}`,
'Verify seed is valid hex string'
);
}
}
/**
* Create a wallet from an extended key (xprv/xpub/tprv/tpub)
* @static
* @param {string} network - Network type ('main' or 'test')
* @param {string} extendedKey - BIP32 extended key
* @returns {CustodialWallet} Wallet instance
* @throws {CustodialWalletError} If extended key format is invalid
* @example
* const wallet = CustodialWallet.fromExtendedKey('main', 'xprv...');
*/
static fromExtendedKey(network, extendedKey) {
try {
const masterKeys = {
extendedPrivateKey: extendedKey.startsWith('xprv') || extendedKey.startsWith('tprv')
? extendedKey
: null,
extendedPublicKey: extendedKey.startsWith('xpub') || extendedKey.startsWith('tpub')
? extendedKey
: null
};
if (!masterKeys.extendedPrivateKey && !masterKeys.extendedPublicKey) {
throw new Error('Invalid extended key format');
}
return new CustodialWallet(network, masterKeys, null);
} catch (error) {
throw new CustodialWalletError(
`Failed to create wallet from extended key: ${error.message}`,
'Verify extended key format'
);
}
}
/**
* Create a wallet from a WIF private key
* @static
* @param {string} wif - WIF-encoded private key
* @returns {CustodialWallet} Wallet instance (single-key, no derivation)
* @throws {CustodialWalletError} If WIF is invalid
*/
static fromWIF(wif) {
try {
const decoded = decodeWIFPrivateKey(wif);
// For WIF import, create a minimal wallet structure
const wallet = new CustodialWallet(decoded.network, {
extendedPrivateKey: null,
extendedPublicKey: null,
singlePrivateKey: decoded.privateKey,
compressed: decoded.compressed
}, null);
wallet._singleKeyMode = true;
return wallet;
} catch (error) {
throw new CustodialWalletError(
`Failed to import WIF: ${error.message}`,
'Verify WIF format is correct'
);
}
}
/**
* Derive a Bitcoin address at the specified path
* @param {number} [account=0] - Account index (hardened)
* @param {number} [change=0] - Change index (0=external, 1=internal)
* @param {number} [index=0] - Address index
* @param {string} [type='segwit'] - Address type ('legacy', 'wrapped-segwit', 'segwit', 'taproot')
* @returns {Object} Derived address details
* @returns {string} returns.address - Bitcoin address
* @returns {string} returns.publicKey - Compressed public key hex
* @returns {string|null} returns.privateKey - WIF-encoded private key
* @returns {Buffer|null} returns.privateKeyBuffer - Raw private key buffer
* @returns {string} returns.path - Full derivation path
* @returns {string} returns.type - Address type
* @returns {string} returns.network - Network type
* @returns {Buffer} returns.scriptPubKey - Output script
* @returns {Buffer|null} returns.redeemScript - Redeem script (for P2SH types)
*/
deriveAddress(account = 0, change = 0, index = 0, type = 'segwit') {
const cacheKey = `${type}:${account}:${change}:${index}`;
if (this.derivedAddresses.has(cacheKey)) {
return this.derivedAddresses.get(cacheKey);
}
// Handle single-key mode (WIF import)
if (this._singleKeyMode) {
return this._deriveSingleKeyAddress(type);
}
const purpose = ADDRESS_TYPE_PURPOSE[type] || ADDRESS_TYPE_PURPOSE.segwit;
const coinType = this.network === 'main' ? 0 : 1;
const path = `m/${purpose}'/${coinType}'/${account}'/${change}/${index}`;
const derived = derive(path, this.masterKeys.extendedPrivateKey);
const publicKeyHex = derived.publicKey.toString('hex');
const pubkeyHash = hash160(derived.publicKey);
let address, scriptPubKey, redeemScript = null;
switch (type) {
case 'legacy':
address = encodeP2PKH(derived.publicKey, this.network);
scriptPubKey = ScriptBuilder.createP2PKH(pubkeyHash);
break;
case 'wrapped-segwit':
// BIP49: P2SH-P2WPKH
const p2wpkhScript = ScriptBuilder.createP2WPKH(pubkeyHash);
const scriptHash = hash160(p2wpkhScript);
address = encodeP2SH(scriptHash, this.network);
scriptPubKey = ScriptBuilder.createP2SH(scriptHash);
redeemScript = p2wpkhScript;
break;
case 'taproot':
const xOnlyPubKey = derived.publicKey.slice(1);
address = BECH32.to_P2TR(xOnlyPubKey, this.network);
scriptPubKey = ScriptBuilder.createP2TR(xOnlyPubKey);
break;
case 'segwit':
default:
address = BECH32.to_P2WPKH(publicKeyHex, this.network);
scriptPubKey = ScriptBuilder.createP2WPKH(pubkeyHash);
}
const result = {
address,
publicKey: publicKeyHex,
publicKeyBuffer: derived.publicKey,
privateKey: derived.privateKey ? encodeWIF(derived.privateKey, this.network) : null,
privateKeyBuffer: derived.privateKey || null,
path,
type,
network: this.network,
scriptPubKey,
redeemScript
};
this.derivedAddresses.set(cacheKey, result);
return result;
}
/**
* Derive address for single-key mode (WIF import)
* @private
*/
_deriveSingleKeyAddress(type) {
const privateKey = this.masterKeys.singlePrivateKey;
const publicKey = ECDSA.getPublicKey(privateKey, this.masterKeys.compressed);
const publicKeyHex = publicKey.toString('hex');
const pubkeyHash = hash160(publicKey);
let address, scriptPubKey, redeemScript = null;
switch (type) {
case 'legacy':
address = encodeP2PKH(publicKey, this.network);
scriptPubKey = ScriptBuilder.createP2PKH(pubkeyHash);
break;
case 'wrapped-segwit':
const p2wpkhScript = ScriptBuilder.createP2WPKH(pubkeyHash);
const scriptHash = hash160(p2wpkhScript);
address = encodeP2SH(scriptHash, this.network);
scriptPubKey = ScriptBuilder.createP2SH(scriptHash);
redeemScript = p2wpkhScript;
break;
case 'taproot':
const xOnlyPubKey = publicKey.slice(1);
address = BECH32.to_P2TR(xOnlyPubKey, this.network);
scriptPubKey = ScriptBuilder.createP2TR(xOnlyPubKey);
break;
case 'segwit':
default:
address = BECH32.to_P2WPKH(publicKeyHex, this.network);
scriptPubKey = ScriptBuilder.createP2WPKH(pubkeyHash);
}
return {
address,
publicKey: publicKeyHex,
publicKeyBuffer: publicKey,
privateKey: encodeWIF(privateKey, this.network, this.masterKeys.compressed),
privateKeyBuffer: privateKey,
path: 'single-key',
type,
network: this.network,
scriptPubKey,
redeemScript
};
}
/**
* Get a receiving (external) address
* @param {number} [account=0] - Account index
* @param {number} [index=0] - Address index
* @param {string} [type='segwit'] - Address type
* @returns {Object} Address details
*/
getReceivingAddress(account = 0, index = 0, type = 'segwit') {
return this.deriveAddress(account, 0, index, type);
}
/**
* Get a change (internal) address
* @param {number} [account=0] - Account index
* @param {number} [index=0] - Address index
* @param {string} [type='segwit'] - Address type
* @returns {Object} Address details
*/
getChangeAddress(account = 0, index = 0, type = 'segwit') {
return this.deriveAddress(account, 1, index, type);
}
/**
* Create a new transaction builder
* @returns {TransactionBuilder} Transaction builder instance
*/
createTransaction() {
return new TransactionBuilder(this.network);
}
/**
* Sign a transaction with wallet keys
* @param {TransactionBuilder} builder - Transaction builder with inputs added
* @param {Array<Object>} inputInfo - Array of {account, change, index, type} for each input
* @returns {Promise<TransactionBuilder>} Signed transaction builder
*/
async signTransaction(builder, inputInfo) {
for (let i = 0; i < inputInfo.length; i++) {
const info = inputInfo[i];
const derived = this.deriveAddress(
info.account ?? 0,
info.change ?? 0,
info.index ?? 0,
info.type ?? 'segwit'
);
if (!derived.privateKeyBuffer) {
throw new CustodialWalletError(
`No private key for input ${i}`,
'Wallet may be read-only'
);
}
await builder.signInput(i, derived.privateKeyBuffer);
}
return builder;
}
/**
* Sign a message using Bitcoin message signing
* @param {string|Buffer} message - Message to sign
* @param {number} [account=0] - Account index
* @param {number} [index=0] - Address index
* @param {string} [type='segwit'] - Address type for signing
* @returns {Object} Signature result
* @throws {CustodialWalletError} If no private key available
*/
signMessage(message, account = 0, index = 0, type = 'segwit') {
const derived = this.deriveAddress(account, 0, index, type);
if (!derived.privateKeyBuffer) {
throw new CustodialWalletError('No private key available for signing');
}
// Use legacy Bitcoin message format
return BIP322.signLegacy(message, derived.privateKeyBuffer);
}
/**
* Sign a message using BIP322 (for SegWit/Taproot addresses)
* @param {string|Buffer} message - Message to sign
* @param {number} [account=0] - Account index
* @param {number} [index=0] - Address index
* @param {string} [type='segwit'] - Address type
* @returns {Promise<Buffer>} BIP322 signature
*/
async signMessageBIP322(message, account = 0, index = 0, type = 'segwit') {
const derived = this.deriveAddress(account, 0, index, type);
if (!derived.privateKeyBuffer) {
throw new CustodialWalletError('No private key available for signing');
}
const addressType = type === 'taproot' ? 'p2tr' : 'p2wpkh';
return BIP322.sign(message, derived.privateKeyBuffer, addressType);
}
/**
* Verify a signed message
* @param {string|Buffer} message - Original message
* @param {Object} signature - Signature to verify
* @param {string|Buffer} publicKey - Public key to verify against
* @returns {boolean} True if signature is valid
*/
verifyMessage(message, signature, publicKey) {
return BIP322.verifyLegacy(message, signature, publicKey);
}
/**
* Export private key as WIF
* @param {number} [account=0] - Account index
* @param {number} [change=0] - Change index
* @param {number} [index=0] - Address index
* @param {string} [type='segwit'] - Address type
* @returns {string|null} WIF-encoded private key
*/
exportWIF(account = 0, change = 0, index = 0, type = 'segwit') {
const derived = this.deriveAddress(account, change, index, type);
return derived.privateKey;
}
/**
* Get all addresses of a specific type for an account
* @param {number} [account=0] - Account index
* @param {string} [type='segwit'] - Address type
* @param {number} [count=20] - Number of addresses to generate
* @returns {Array<Object>} Array of address details
*/
getAddresses(account = 0, type = 'segwit', count = 20) {
const addresses = [];
for (let i = 0; i < count; i++) {
addresses.push(this.getReceivingAddress(account, i, type));
}
return addresses;
}
/**
* Get the master extended public key (xpub/tpub)
* @returns {string} Extended public key
*/
getExtendedPublicKey() {
return this.masterKeys.extendedPublicKey;
}
/**
* Get the master extended private key (xprv/tprv)
* @returns {string} Extended private key
*/
getExtendedPrivateKey() {
return this.masterKeys.extendedPrivateKey;
}
/**
* Get the wallet's mnemonic phrase
* @returns {string|null} Mnemonic phrase or null if not available
*/
getMnemonic() {
return this.mnemonic;
}
/**
* Get the wallet's network type
* @returns {string} Network type ('main' or 'test')
*/
getNetwork() {
return this.network;
}
/**
* Check if wallet has private key access
* @returns {boolean} True if wallet can sign
*/
canSign() {
return !!(this.masterKeys.extendedPrivateKey || this.masterKeys.singlePrivateKey);
}
/**
* Serialize wallet to JSON (excludes sensitive data)
* @returns {Object} JSON-serializable wallet data
*/
toJSON() {
return {
network: this.network,
version: this.version,
created: this.created,
extendedPublicKey: this.masterKeys.extendedPublicKey,
addressCount: this.derivedAddresses.size,
canSign: this.canSign()
};
}
/**
* Clear the derived addresses cache
* @returns {void}
*/
clearCache() {
this.derivedAddresses.clear();
}
/**
* Securely clear all sensitive data
* @returns {void}
*/
destroy() {
if (this.masterKeys.singlePrivateKey) {
this.masterKeys.singlePrivateKey.fill(0);
}
this.mnemonic = null;
this.masterKeys = { extendedPrivateKey: null, extendedPublicKey: null };
this.derivedAddresses.clear();
}
}
export { CustodialWallet, CustodialWalletError };
export default CustodialWallet;