@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
JavaScript
;
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;