UNPKG

@nawab_kibria/bitcoin-lib

Version:

A comprehensive Bitcoin HD wallet library with BIP84, BIP44, BIP49 support, mnemonic generation and restoration

382 lines (381 loc) 15.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TransactionBuilder = void 0; const bitcoin = __importStar(require("bitcoinjs-lib")); const ecpair_1 = require("ecpair"); const ecc = __importStar(require("tiny-secp256k1")); const ECPair = (0, ecpair_1.ECPairFactory)(ecc); // Initialize bitcoinjs-lib with ECC for Taproot support bitcoin.initEccLib(ecc); class TransactionBuilder { constructor(network = bitcoin.networks.bitcoin) { this.network = network; } /** * Create a Bitcoin transaction from inputs and outputs */ createTransaction(inputs_1, outputs_1) { return __awaiter(this, arguments, void 0, function* (inputs, outputs, options = {}) { const { feeRate = 1, changeAddress, subtractFeeFromOutputs = [], rbf = true, minConfirmations = 0 } = options; // Validate inputs if (inputs.length === 0) { throw new Error('At least one input is required'); } if (outputs.length === 0) { throw new Error('At least one output is required'); } // Filter inputs by confirmation requirements const confirmedInputs = inputs.filter(input => { // If height is 0, it's unconfirmed return minConfirmations === 0 || input.value > 0; // Basic validation }); if (confirmedInputs.length === 0) { throw new Error('No confirmed inputs available'); } // Calculate total input value const totalInputValue = confirmedInputs.reduce((sum, input) => sum + input.value, 0); // Calculate total output value (before fee) let totalOutputValue = outputs.reduce((sum, output) => sum + output.value, 0); // Estimate transaction size and calculate fee const estimatedSize = this.estimateTransactionSize(confirmedInputs, outputs, !!changeAddress); let fee = Math.ceil(estimatedSize * feeRate); // Handle fee subtraction from outputs if (subtractFeeFromOutputs.length > 0) { const feePerOutput = Math.ceil(fee / subtractFeeFromOutputs.length); subtractFeeFromOutputs.forEach(index => { if (index < outputs.length) { outputs[index].value -= feePerOutput; if (outputs[index].value < 546) { // dust limit throw new Error(`Output ${index} would be dust after fee subtraction`); } } }); totalOutputValue = outputs.reduce((sum, output) => sum + output.value, 0); } // Calculate change const changeValue = totalInputValue - totalOutputValue - fee; let changeOutput; if (changeValue > 546 && changeAddress) { // Only create change if above dust limit changeOutput = { address: changeAddress, value: changeValue }; } else if (changeValue < 0) { throw new Error(`Insufficient funds. Need ${totalOutputValue + fee}, have ${totalInputValue}`); } else if (changeValue > 0 && changeValue <= 546) { // Add dust to fee fee += changeValue; } // Create the transaction const psbt = new bitcoin.Psbt({ network: this.network }); // Add inputs for (const input of confirmedInputs) { yield this.addInputToPsbt(psbt, input); } // Add outputs outputs.forEach(output => { psbt.addOutput({ address: output.address, value: output.value }); }); // Add change output if needed if (changeOutput) { psbt.addOutput({ address: changeOutput.address, value: changeOutput.value }); } // Enable RBF if requested if (rbf) { confirmedInputs.forEach((_, index) => { psbt.setInputSequence(index, 0xfffffffd); }); } // Sign inputs for (let i = 0; i < confirmedInputs.length; i++) { const input = confirmedInputs[i]; const keyPair = ECPair.fromPrivateKey(Buffer.from(input.privateKey, 'hex')); // Convert ECPair to proper signer interface const signer = { publicKey: Buffer.from(keyPair.publicKey), sign: (hash) => Buffer.from(keyPair.sign(hash)) }; psbt.signInput(i, signer); } // Finalize all inputs psbt.finalizeAllInputs(); // Extract transaction const tx = psbt.extractTransaction(); const txHex = tx.toHex(); const txid = tx.getId(); return { hex: txHex, txid: txid, fee: fee, size: tx.byteLength(), vsize: tx.virtualSize(), inputs: confirmedInputs, outputs: outputs, changeOutput: changeOutput }; }); } /** * Add input to PSBT based on address type */ addInputToPsbt(psbt, input) { return __awaiter(this, void 0, void 0, function* () { const inputData = { hash: input.txHash, index: input.outputIndex }; switch (input.addressType) { case 'legacy': // For legacy addresses, we need the full previous transaction // In practice, you'd fetch this from the blockchain inputData.nonWitnessUtxo = Buffer.from('placeholder', 'hex'); // This should be the full previous tx break; case 'native-segwit': case 'segwit': // For segwit addresses, we need the witness UTXO const script = this.getScriptForAddress(input.address, input.addressType); inputData.witnessUtxo = { script: script, value: input.value }; break; } psbt.addInput(inputData); }); } /** * Get script for address based on type */ getScriptForAddress(address, addressType) { try { switch (addressType) { case 'legacy': const legacyDecoded = bitcoin.address.fromBase58Check(address); return bitcoin.script.compile([ bitcoin.opcodes.OP_DUP, bitcoin.opcodes.OP_HASH160, legacyDecoded.hash, bitcoin.opcodes.OP_EQUALVERIFY, bitcoin.opcodes.OP_CHECKSIG ]); case 'native-segwit': case 'segwit': const segwitDecoded = bitcoin.address.fromBech32(address); return bitcoin.script.compile([ bitcoin.opcodes.OP_0, segwitDecoded.data ]); default: throw new Error(`Unsupported address type: ${addressType}`); } } catch (error) { throw new Error(`Failed to create script for address ${address}: ${error}`); } } /** * Estimate transaction size in virtual bytes */ estimateTransactionSize(inputs, outputs, hasChange = false) { // Base transaction size let size = 10; // version (4) + input count (1) + output count (1) + locktime (4) // Input sizes inputs.forEach(input => { switch (input.addressType) { case 'legacy': size += 148; // P2PKH input break; case 'native-segwit': size += 68; // P2WPKH input (base) + 27 (witness) break; case 'segwit': size += 91; // P2SH-P2WPKH input break; } }); // Output sizes const outputCount = outputs.length + (hasChange ? 1 : 0); outputs.forEach(output => { if (output.address.startsWith('bc1') || output.address.startsWith('tb1') || output.address.startsWith('bcrt1')) { size += 31; // P2WPKH output } else { size += 34; // P2PKH/P2SH output } }); // Change output size (if applicable) if (hasChange) { size += 31; // Assume P2WPKH change } return Math.ceil(size); } /** * Convert UTXOs to transaction inputs */ convertUTXOsToInputs(utxos, addressDetails) { return utxos.map(utxo => { const addressInfo = addressDetails.get(utxos[0].tx_hash); // This should map properly if (!addressInfo) { throw new Error(`No address info found for UTXO ${utxo.tx_hash}:${utxo.tx_pos}`); } return { txHash: utxo.tx_hash, outputIndex: utxo.tx_pos, value: utxo.value, address: addressInfo.address, addressType: addressInfo.addressType, privateKey: addressInfo.privateKey, publicKey: addressInfo.publicKey }; }); } /** * Select UTXOs for a transaction using a simple selection algorithm */ selectUTXOs(utxos, targetAmount, feeRate = 1) { // Sort UTXOs by value (largest first for efficiency) const sortedUTXOs = [...utxos].sort((a, b) => b.value - a.value); const selectedUTXOs = []; let totalValue = 0; for (const utxo of sortedUTXOs) { selectedUTXOs.push(utxo); totalValue += utxo.value; // Estimate fee with current selection const estimatedFee = this.estimateTransactionSize(selectedUTXOs.map(u => ({ addressType: 'native-segwit' })), [{ address: '', value: targetAmount }], true) * feeRate; // Check if we have enough if (totalValue >= targetAmount + estimatedFee) { return { selectedUTXOs, totalValue, estimatedFee }; } } throw new Error(`Insufficient funds. Need ${targetAmount}, have ${totalValue}`); } /** * Create a PSBT (Partially Signed Bitcoin Transaction) */ createPSBT(options) { const psbt = new bitcoin.Psbt({ network: options.network }); // Add inputs options.inputs.forEach(input => { const inputData = { hash: input.hash, index: input.index }; if (input.witnessUtxo) { inputData.witnessUtxo = input.witnessUtxo; } if (input.nonWitnessUtxo) { inputData.nonWitnessUtxo = input.nonWitnessUtxo; } if (input.bip32Derivation) { inputData.bip32Derivation = input.bip32Derivation; } psbt.addInput(inputData); }); // Add outputs options.outputs.forEach(output => { psbt.addOutput({ address: output.address, value: output.value }); }); return psbt; } /** * Validate transaction before broadcasting */ validateTransaction(tx) { // Basic validation if (!tx.hex || !tx.txid) { throw new Error('Invalid transaction: missing hex or txid'); } if (tx.fee <= 0) { throw new Error('Invalid transaction: fee must be positive'); } if (tx.inputs.length === 0) { throw new Error('Invalid transaction: no inputs'); } if (tx.outputs.length === 0) { throw new Error('Invalid transaction: no outputs'); } // Check dust limit const allOutputs = [...tx.outputs]; if (tx.changeOutput) { allOutputs.push(tx.changeOutput); } allOutputs.forEach((output, index) => { if (output.value < 546) { throw new Error(`Output ${index} is dust (${output.value} < 546 sats)`); } }); return true; } /** * Get network-specific dust limit */ getDustLimit() { return 546; // Standard dust limit in satoshis } /** * Calculate transaction priority score */ calculatePriority(inputs, feeRate) { const totalValue = inputs.reduce((sum, input) => sum + input.value, 0); const age = 1; // Simplified - would calculate actual age from block height return (totalValue * age) / (this.estimateTransactionSize(inputs, [], false) * feeRate); } } exports.TransactionBuilder = TransactionBuilder;