UNPKG

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

1,057 lines (933 loc) 33.4 kB
/** * @fileoverview Non-Custodial Wallet with Threshold Signature Scheme * @description Implements a non-custodial wallet using the nChain TSS protocol * for distributed key management and threshold signing. * @version 1.0.0 * @author yfbsei * @license ISC */ import { secp256k1 } from '@noble/curves/secp256k1'; import BN from 'bn.js'; import { ThresholdSignatureScheme, createThresholdScheme, Polynomial, CURVE_ORDER } from '../core/crypto/signatures/threshold/index.js'; // HD Derivation imports 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 { encodeP2PKH, encodeP2SH, encodeWIF, hash160 } from '../encoding/address/encode.js'; import { decodeWIFPrivateKey } from '../encoding/address/decode.js'; import { ScriptBuilder } from '../transaction/script-builder.js'; import { TransactionBuilder } from '../transaction/builder.js'; import { BIP322 } from '../transaction/message-signing.js'; import { ECDSA } from '../core/crypto/signatures/ecdsa.js'; import { THRESHOLD_CONSTANTS } from '../core/constants.js'; /** * 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 }; class NonCustodialWalletError extends Error { constructor(message, solution = 'Check threshold configuration') { super(message); this.name = 'NonCustodialWalletError'; this.solution = solution; this.timestamp = new Date().toISOString(); } } /** * Represents a participant's key share for export/import */ class ParticipantShare { constructor(index, keyShare, publicKeyShare = null) { this.index = index; this.keyShare = keyShare instanceof BN ? keyShare : new BN(keyShare, 'hex'); this.publicKeyShare = publicKeyShare; } toJSON() { return { index: this.index, keyShare: this.keyShare.toString('hex'), publicKeyShare: this.publicKeyShare ? this.publicKeyShare.toString('hex') : null }; } static fromJSON(json) { return new ParticipantShare( json.index, json.keyShare, json.publicKeyShare ? Buffer.from(json.publicKeyShare, 'hex') : null ); } } /** * Non-Custodial Wallet using nChain Threshold Signature Scheme * * This wallet distributes key management across multiple participants, * requiring a threshold number to sign transactions. * * Parameters: * - n: Total number of participants * - t: Threshold polynomial degree (t+1 to reconstruct, 2t+1 to sign) * * Common configurations: * - 2-of-3: n=3, t=1 (3 participants, 3 needed to sign) * - 3-of-5: n=5, t=2 (5 participants, 5 needed to sign) * * Note: Due to the INVSS protocol, signing requires 2t+1 participants. */ class NonCustodialWallet { /** * Create a new non-custodial wallet * @param {string} network - 'main' or 'test' * @param {number} n - Total number of participants * @param {number} t - Threshold polynomial degree */ constructor(network, n, t) { // Validate parameters if (n < 2) { throw new NonCustodialWalletError( 'Need at least 2 participants', 'Increase participant count' ); } if (t < 1) { throw new NonCustodialWalletError( 'Threshold degree must be at least 1', 'Increase threshold' ); } // Signing requires 2t+1 participants if (2 * t + 1 > n) { throw new NonCustodialWalletError( `Signing requires ${2 * t + 1} participants (2t+1), but only ${n} available`, 'Increase participants or decrease threshold' ); } if (n > THRESHOLD_CONSTANTS.MAX_PARTICIPANTS) { throw new NonCustodialWalletError( `Participants cannot exceed ${THRESHOLD_CONSTANTS.MAX_PARTICIPANTS}`, 'Reduce participant count' ); } this.network = network === 'main' ? 'main' : 'test'; this.n = n; this.t = t; this.signingThreshold = 2 * t + 1; this.reconstructionThreshold = t + 1; // TSS scheme instance this.scheme = null; // Exported shares for backup/distribution this.exportedShares = []; // Aggregate public key this.aggregatePublicKey = null; // HD Wallet properties /** @type {Object|null} BIP32 master keys (extendedPrivateKey, extendedPublicKey) */ this.masterKeys = null; /** @type {string|null} BIP39 mnemonic phrase */ this.mnemonic = null; /** @type {Map<string, Object>} Cache of derived addresses */ this.derivedAddresses = new Map(); this.version = '2.0.0'; this.created = Date.now(); } /** * Create a new wallet with fresh key generation * @param {string} network - Network type * @param {number} n - Total participants (default 3) * @param {number} t - Threshold degree (default 1, meaning 3-of-3 for signing) * @param {number} ephemeralKeyCount - Pre-generated ephemeral keys * @returns {{wallet: NonCustodialWallet, shares: Array}} Wallet and shares */ static createNew(network = 'main', n = 3, t = 1, ephemeralKeyCount = 10) { const wallet = new NonCustodialWallet(network, n, t); wallet.initialize(ephemeralKeyCount); return { wallet, shares: wallet.getShares(), config: wallet.getThresholdConfig() }; } /** * Create a new HD wallet with fresh mnemonic and TSS key generation * @param {string} network - Network type ('main' or 'test') * @param {number} n - Total participants (default 3) * @param {number} t - Threshold degree (default 1) * @param {number} strength - Mnemonic strength (128, 160, 192, 224, 256) * @param {number} ephemeralKeyCount - Pre-generated ephemeral keys * @returns {{wallet: NonCustodialWallet, mnemonic: string, shares: Array, config: Object}} */ static createNewHD(network = 'main', n = 3, t = 1, strength = 256, ephemeralKeyCount = 10) { try { const mnemonicResult = BIP39.generateMnemonic(strength); const mnemonic = mnemonicResult.mnemonic; const seed = BIP39.deriveSeed(mnemonic); const [masterKeys] = generateMasterKey(seed, network); const wallet = new NonCustodialWallet(network, n, t); wallet.masterKeys = masterKeys; wallet.mnemonic = mnemonic; wallet.initialize(ephemeralKeyCount); return { wallet, mnemonic, shares: wallet.getShares(), config: wallet.getThresholdConfig() }; } catch (error) { throw new NonCustodialWalletError( `Failed to create HD wallet: ${error.message}`, 'Ensure crypto module is available' ); } } /** * Restore an HD wallet from a BIP39 mnemonic phrase * @param {string} network - Network type ('main' or 'test') * @param {string} mnemonic - BIP39 mnemonic phrase (12-24 words) * @param {number} n - Total participants * @param {number} t - Threshold degree * @param {string} passphrase - Optional BIP39 passphrase * @param {number} ephemeralKeyCount - Pre-generated ephemeral keys * @returns {NonCustodialWallet} Restored wallet */ static fromMnemonic(network, mnemonic, n = 3, t = 1, passphrase = '', ephemeralKeyCount = 10) { try { if (!BIP39.validateChecksum(mnemonic)) { throw new Error('Invalid mnemonic checksum'); } const seed = BIP39.deriveSeed(mnemonic, passphrase); const [masterKeys] = generateMasterKey(seed, network); const wallet = new NonCustodialWallet(network, n, t); wallet.masterKeys = masterKeys; wallet.mnemonic = mnemonic; wallet.initialize(ephemeralKeyCount); return wallet; } catch (error) { throw new NonCustodialWalletError( `Failed to restore wallet from mnemonic: ${error.message}`, 'Verify mnemonic phrase is correct' ); } } /** * Create an HD wallet from a raw seed * @param {string} network - Network type ('main' or 'test') * @param {string|Buffer} seed - 64-byte seed as hex string or Buffer * @param {number} n - Total participants * @param {number} t - Threshold degree * @param {number} ephemeralKeyCount - Pre-generated ephemeral keys * @returns {NonCustodialWallet} Wallet instance */ static fromSeed(network, seed, n = 3, t = 1, ephemeralKeyCount = 10) { try { const [masterKeys] = generateMasterKey(seed, network); const wallet = new NonCustodialWallet(network, n, t); wallet.masterKeys = masterKeys; wallet.initialize(ephemeralKeyCount); return wallet; } catch (error) { throw new NonCustodialWalletError( `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) * Note: TSS is still used for signing; HD is for address derivation only * @param {string} network - Network type ('main' or 'test') * @param {string} extendedKey - BIP32 extended key * @param {number} n - Total participants * @param {number} t - Threshold degree * @param {number} ephemeralKeyCount - Pre-generated ephemeral keys * @returns {NonCustodialWallet} Wallet instance */ static fromExtendedKey(network, extendedKey, n = 3, t = 1, ephemeralKeyCount = 10) { 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'); } const wallet = new NonCustodialWallet(network, n, t); wallet.masterKeys = masterKeys; wallet.initialize(ephemeralKeyCount); return wallet; } catch (error) { throw new NonCustodialWalletError( `Failed to create wallet from extended key: ${error.message}`, 'Verify extended key format' ); } } /** * Create a wallet from a WIF private key (single-key mode for HD addresses) * Note: TSS is still used for aggregate key; WIF enables single-key HD derivation * @param {string} wif - WIF-encoded private key * @param {number} n - Total participants for TSS * @param {number} t - Threshold degree * @param {number} ephemeralKeyCount - Pre-generated ephemeral keys * @returns {NonCustodialWallet} Wallet instance */ static fromWIF(wif, n = 3, t = 1, ephemeralKeyCount = 10) { try { const decoded = decodeWIFPrivateKey(wif); const wallet = new NonCustodialWallet(decoded.network, n, t); // Store single key info for single-key mode wallet.masterKeys = { extendedPrivateKey: null, extendedPublicKey: null, singlePrivateKey: decoded.privateKey, compressed: decoded.compressed }; wallet._singleKeyMode = true; wallet.initialize(ephemeralKeyCount); return wallet; } catch (error) { throw new NonCustodialWalletError( `Failed to import WIF: ${error.message}`, 'Verify WIF format is correct' ); } } /** * Initialize the wallet with TSS key generation * @param {number} ephemeralKeyCount - Number of ephemeral keys to pre-generate */ initialize(ephemeralKeyCount = 10) { // Create and initialize the threshold scheme this.scheme = createThresholdScheme(this.n, this.t, ephemeralKeyCount); this.aggregatePublicKey = this.scheme.getPublicKey(); // Export shares for distribution this._exportShares(); } /** * Export shares from the internal scheme for backup */ _exportShares() { if (!this.scheme) return; this.exportedShares = this.scheme.privateKeyShares.map(share => new ParticipantShare( share.index, share.keyShare, share.publicKeyShare ) ); } /** * Restore wallet from exported shares (limited functionality) * Note: Full signing requires the complete JVRSS state * @param {string} network - Network type * @param {Array} shares - Exported shares * @param {number} t - Threshold degree * @returns {NonCustodialWallet} Restored wallet */ static fromShares(network, shares, t) { const n = shares.length; const wallet = new NonCustodialWallet(network, n, t); wallet.exportedShares = shares.map(s => s instanceof ParticipantShare ? s : ParticipantShare.fromJSON(s) ); // Reconstruct public key from shares wallet._reconstructPublicKey(); return wallet; } /** * Reconstruct the aggregate public key from shares */ _reconstructPublicKey() { if (this.exportedShares.length < this.reconstructionThreshold) { throw new NonCustodialWalletError( `Need at least ${this.reconstructionThreshold} shares to reconstruct`, 'Provide more shares' ); } const selectedShares = this.exportedShares.slice(0, this.reconstructionThreshold); const secret = Polynomial.reconstructSecret( selectedShares.map(s => ({ x: new BN(s.index), y: s.keyShare })) ); const secretBuffer = secret.toArrayLike(Buffer, 'be', 32); this.aggregatePublicKey = Buffer.from( secp256k1.getPublicKey(secretBuffer, true) ); } /** * Get wallet address * @param {string} type - 'segwit' or 'taproot' * @returns {string} Bitcoin address */ getAddress(type = 'segwit') { if (!this.aggregatePublicKey) { throw new NonCustodialWalletError('Wallet not initialized'); } const publicKeyHex = this.aggregatePublicKey.toString('hex'); switch (type) { case 'taproot': const xOnlyPubKey = this.aggregatePublicKey.slice(1); return BECH32.to_P2TR(xOnlyPubKey, this.network); case 'segwit': default: return BECH32.to_P2WPKH(publicKeyHex, this.network); } } /** * Derive an HD address at the specified BIP path * Requires HD wallet initialization via createNewHD(), fromMnemonic(), or fromSeed() * @param {number} account - Account index (hardened) * @param {number} change - Change index (0=external/receiving, 1=internal/change) * @param {number} index - Address index * @param {string} type - Address type ('legacy', 'wrapped-segwit', 'segwit', 'taproot') * @returns {Object} Derived address details */ deriveAddress(account = 0, change = 0, index = 0, type = 'segwit') { if (!this.masterKeys) { throw new NonCustodialWalletError( 'HD derivation not available', 'Use createNewHD(), fromMnemonic(), or fromSeed() for HD support' ); } // Handle single-key mode (WIF import) if (this._singleKeyMode) { return this._deriveSingleKeyAddress(type); } const cacheKey = `${type}:${account}:${change}:${index}`; if (this.derivedAddresses.has(cacheKey)) { return this.derivedAddresses.get(cacheKey); } 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 * @param {string} type - Address type * @returns {Object} Address details */ _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 from HD derivation * @param {number} account - Account index * @param {number} index - Address index * @param {string} type - 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 from HD derivation * @param {number} account - Account index * @param {number} index - Address index * @param {string} type - Address type * @returns {Object} Address details */ getChangeAddress(account = 0, index = 0, type = 'segwit') { return this.deriveAddress(account, 1, index, type); } /** * Get multiple addresses of a specific type for an account * @param {number} account - Account index * @param {string} type - Address type * @param {number} count - 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; } /** * Check if HD derivation is available * @returns {boolean} True if HD is available */ hasHD() { return !!this.masterKeys; } /** * Get the wallet's mnemonic phrase (if available) * @returns {string|null} Mnemonic phrase or null */ getMnemonic() { return this.mnemonic; } /** * Get the extended public key (xpub/tpub) * @returns {string|null} Extended public key or null */ getExtendedPublicKey() { return this.masterKeys?.extendedPublicKey || null; } /** * Get the extended private key (xprv/tprv) * @returns {string|null} Extended private key or null */ getExtendedPrivateKey() { return this.masterKeys?.extendedPrivateKey || null; } /** * Get total count of derived addresses * @returns {number} Address count */ getAddressCount() { return this.derivedAddresses.size; } /** * List all derived addresses * @returns {Array<Object>} Array of all derived address objects */ listAddresses() { return Array.from(this.derivedAddresses.values()); } /** * Clear the derived addresses cache */ clearAddressCache() { this.derivedAddresses.clear(); } // ============================================ // Transaction Methods // ============================================ /** * Create a new transaction builder * @returns {TransactionBuilder} Transaction builder instance */ createTransaction() { return new TransactionBuilder(this.network); } /** * Sign a transaction with HD-derived wallet keys * Note: This signs using HD-derived keys, not TSS aggregate key * @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) { if (!this.masterKeys) { throw new NonCustodialWalletError( 'HD wallet not available for transaction signing', 'Use createNewHD() or fromMnemonic() for transaction support' ); } 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 NonCustodialWalletError( `No private key for input ${i}`, 'Wallet may be read-only' ); } await builder.signInput(i, derived.privateKeyBuffer); } return builder; } /** * Sign a message using HD-derived key (legacy Bitcoin message format) * @param {string|Buffer} message - Message to sign * @param {number} account - Account index * @param {number} index - Address index * @param {string} type - Address type for signing * @returns {Object} Signature result */ signMessageHD(message, account = 0, index = 0, type = 'segwit') { if (!this.masterKeys) { throw new NonCustodialWalletError( 'HD wallet required for HD message signing', 'Use createNewHD() or fromMnemonic()' ); } const derived = this.deriveAddress(account, 0, index, type); if (!derived.privateKeyBuffer) { throw new NonCustodialWalletError('No private key available for signing'); } return BIP322.signLegacy(message, derived.privateKeyBuffer); } /** * Sign a message using BIP322 (for SegWit/Taproot HD addresses) * @param {string|Buffer} message - Message to sign * @param {number} account - Account index * @param {number} index - Address index * @param {string} type - Address type ('segwit' or 'taproot') * @returns {Promise<Buffer>} BIP322 signature */ async signMessageBIP322(message, account = 0, index = 0, type = 'segwit') { if (!this.masterKeys) { throw new NonCustodialWalletError( 'HD wallet required for BIP322 signing', 'Use createNewHD() or fromMnemonic()' ); } const derived = this.deriveAddress(account, 0, index, type); if (!derived.privateKeyBuffer) { throw new NonCustodialWalletError('No private key available for signing'); } const addressType = type === 'taproot' ? 'p2tr' : 'p2wpkh'; return BIP322.sign(message, derived.privateKeyBuffer, addressType); } /** * Verify a signed message using HD public key * @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 */ verifyMessageHD(message, signature, publicKey) { return BIP322.verifyLegacy(message, signature, publicKey); } /** * Export HD-derived private key as WIF * @param {number} account - Account index * @param {number} change - Change index * @param {number} index - Address index * @param {string} type - Address type * @returns {string|null} WIF-encoded private key */ exportWIF(account = 0, change = 0, index = 0, type = 'segwit') { if (!this.masterKeys) { return null; } const derived = this.deriveAddress(account, change, index, type); return derived.privateKey; } // ============================================ // Utility Methods // ============================================ /** * Get the wallet's network type * @returns {string} Network type ('main' or 'test') */ getNetwork() { return this.network; } /** * Check if wallet can sign (has private key access) * @returns {boolean} True if wallet can sign */ canSign() { const hasTSS = !!this.scheme; const hasHDPrivate = !!(this.masterKeys?.extendedPrivateKey || this.masterKeys?.singlePrivateKey); return hasTSS || hasHDPrivate; } /** * Check if HD wallet can sign (separate from TSS) * @returns {boolean} True if HD signing is available */ canSignHD() { return !!(this.masterKeys?.extendedPrivateKey || this.masterKeys?.singlePrivateKey); } /** * Get all shares for distribution to participants * @returns {Array} Array of share objects */ getShares() { return this.exportedShares.map(s => s.toJSON()); } /** * Get a specific participant's share * @param {number} index - Participant index (1-indexed) * @returns {Object} Share data */ getShare(index) { const share = this.exportedShares.find(s => s.index === index); if (!share) { throw new NonCustodialWalletError(`Share ${index} not found`); } return share.toJSON(); } /** * Sign a message hash using threshold signature * @param {Buffer|string} messageHash - 32-byte message hash * @param {number[]} participantIndices - Indices of signing participants * @returns {Object} Signature {r, s, signature} */ sign(messageHash, participantIndices = null) { if (!this.scheme) { throw new NonCustodialWalletError( 'Full signing requires initialized scheme', 'Use createNew() to create a signable wallet' ); } // Default to first signingThreshold participants const indices = participantIndices || Array.from({ length: this.signingThreshold }, (_, i) => i + 1); if (indices.length < this.signingThreshold) { throw new NonCustodialWalletError( `Need at least ${this.signingThreshold} participants to sign (2t+1)`, 'Provide more participant indices' ); } return this.scheme.sign(messageHash, indices); } /** * Sign a message (with Bitcoin message prefix) * @param {string|Buffer} message - Message to sign * @param {number[]} participantIndices - Signing participants * @returns {Object} Signature */ signMessage(message, participantIndices = null) { if (!this.scheme) { throw new NonCustodialWalletError( 'Full signing requires initialized scheme', 'Use createNew() to create a signable wallet' ); } return this.scheme.signMessage(message, participantIndices); } /** * Verify a signature * @param {Buffer|string} messageHash - Message hash * @param {Object|Buffer} signature - Signature to verify * @returns {boolean} True if valid */ verify(messageHash, signature) { if (!this.aggregatePublicKey) { throw new NonCustodialWalletError('Wallet not initialized'); } if (this.scheme) { return this.scheme.verify(messageHash, signature); } // Manual verification if scheme not available try { let r, s; if (Buffer.isBuffer(signature) && signature.length === 64) { r = new BN(signature.slice(0, 32)); s = new BN(signature.slice(32, 64)); } else if (signature.r && signature.s) { r = new BN(signature.r, 'hex'); s = new BN(signature.s, 'hex'); } else { return false; } const sig = new secp256k1.Signature( BigInt('0x' + r.toString('hex')), BigInt('0x' + s.toString('hex')) ); let hash; if (typeof messageHash === 'string') { hash = Buffer.from(messageHash, 'hex'); } else { hash = messageHash; } return secp256k1.verify(sig, hash, this.aggregatePublicKey); } catch { return false; } } /** * Get the aggregate public key * @returns {Buffer} Compressed public key */ getPublicKey() { return this.aggregatePublicKey; } /** * Get threshold configuration * @returns {Object} Configuration details */ getThresholdConfig() { return { n: this.n, t: this.t, reconstructionThreshold: this.reconstructionThreshold, signingThreshold: this.signingThreshold, sharesAvailable: this.exportedShares.length, ephemeralKeysAvailable: this.scheme?.getConfig()?.availableEphemeralKeys || 0 }; } /** * Generate more ephemeral keys for signing * @param {number} count - Number of keys to generate */ generateEphemeralKeys(count = 10) { if (!this.scheme) { throw new NonCustodialWalletError('Scheme not initialized'); } this.scheme.generateEphemeralKeys(count); } /** * Export wallet for serialization * @returns {Object} Serialized wallet data */ toJSON() { return { network: this.network, version: this.version, created: this.created, n: this.n, t: this.t, signingThreshold: this.signingThreshold, reconstructionThreshold: this.reconstructionThreshold, aggregatePublicKey: this.aggregatePublicKey?.toString('hex'), sharesCount: this.exportedShares.length, // HD wallet info hasHD: this.hasHD(), extendedPublicKey: this.masterKeys?.extendedPublicKey || null, derivedAddressCount: this.derivedAddresses.size }; } /** * Export shares for backup * @returns {Object} Exportable data */ exportShares() { return { network: this.network, n: this.n, t: this.t, shares: this.getShares() }; } /** * Import shares from backup * @param {Object} exportedData - Previously exported data * @returns {NonCustodialWallet} Restored wallet */ static importShares(exportedData) { return NonCustodialWallet.fromShares( exportedData.network, exportedData.shares, exportedData.t ); } /** * Clear sensitive data */ clear() { if (this.scheme) { this.scheme.clear(); } this.exportedShares = []; this.aggregatePublicKey = null; // Clear HD data this.mnemonic = null; this.masterKeys = null; this.derivedAddresses.clear(); } /** * Securely clear all sensitive data (overwrites buffers) * @returns {void} */ destroy() { // Clear TSS scheme if (this.scheme) { this.scheme.clear(); } this.scheme = null; // Securely overwrite single private key if present if (this.masterKeys?.singlePrivateKey && Buffer.isBuffer(this.masterKeys.singlePrivateKey)) { this.masterKeys.singlePrivateKey.fill(0); } // Clear all sensitive properties this.mnemonic = null; this.masterKeys = null; this.exportedShares = []; this.aggregatePublicKey = null; this.derivedAddresses.clear(); this._singleKeyMode = false; } } export { NonCustodialWallet, NonCustodialWalletError, ParticipantShare }; export default NonCustodialWallet;