UNPKG

@btc-stamps/tx-builder

Version:

Transaction builder for Bitcoin Stamps and SRC-20 tokens with advanced UTXO selection

278 lines (245 loc) 6.61 kB
/** * PSBT (Partially Signed Bitcoin Transaction) Builder * Advanced PSBT operations and utilities */ import { Buffer } from 'node:buffer'; import * as bitcoin from 'bitcoinjs-lib'; import type { Network } from 'bitcoinjs-lib'; export interface PSBTOptions { network?: Network; maximumFeeRate?: number; version?: number; locktime?: number; } /** * Builder for creating and manipulating Partially Signed Bitcoin Transactions (PSBTs) * * @remarks * PSBTBuilder provides a fluent interface for constructing PSBTs with support for: * - Multiple input types (P2PKH, P2WPKH, P2SH, P2WSH) * - Witness and non-witness UTXOs * - Custom scripts and redeem scripts * - Fee calculation and validation * - Transaction finalization * * @example * ```typescript * const builder = new PSBTBuilder(); * const psbt = builder * .addInput(utxo) * .addOutput({ address: 'bc1q...', value: 100000 }) * .build(); * ``` */ export class PSBTBuilder { protected psbt: bitcoin.Psbt; private network: Network; constructor(options: PSBTOptions = {}) { this.network = options.network ?? bitcoin.networks.bitcoin; // @ts-ignore - maximumFeeRate can be undefined for bitcoinjs-lib this.psbt = new bitcoin.Psbt({ network: this.network, maximumFeeRate: options.maximumFeeRate, }); if (options.version !== undefined) { this.psbt.setVersion(options.version); } if (options.locktime !== undefined) { this.psbt.setLocktime(options.locktime); } } /** * Create PSBT from base64 string */ static fromBase64(base64: string, network?: Network): PSBTBuilder { // @ts-ignore - network can be undefined for bitcoinjs-lib const psbt = bitcoin.Psbt.fromBase64(base64, { network }); // @ts-ignore - network can be undefined in constructor const builder = new PSBTBuilder({ network }); builder.psbt = psbt; return builder; } /** * Create PSBT from hex string */ static fromHex(hex: string, network?: Network): PSBTBuilder { // @ts-ignore - network can be undefined for bitcoinjs-lib const psbt = bitcoin.Psbt.fromHex(hex, { network }); // @ts-ignore - network can be undefined in constructor const builder = new PSBTBuilder({ network }); builder.psbt = psbt; return builder; } /** * Create PSBT from buffer */ static fromBuffer(buffer: Buffer, network?: Network): PSBTBuilder { // @ts-ignore - network can be undefined for bitcoinjs-lib const psbt = bitcoin.Psbt.fromBuffer(buffer, { network }); // @ts-ignore - network can be undefined in constructor const builder = new PSBTBuilder({ network }); builder.psbt = psbt; return builder; } /** * Add standard input */ addInput( txid: string, vout: number, options: { sequence?: number; witnessUtxo?: { script: Buffer; value: number }; nonWitnessUtxo?: Buffer; redeemScript?: Buffer; witnessScript?: Buffer; } = {}, ): this { // Convert txid to string if it's a Buffer const txidString = Buffer.isBuffer(txid) ? txid.toString('hex') : txid; // Ensure txid is a proper 64-character hex string const normalizedTxid = txidString.length === 64 && /^[0-9a-fA-F]+$/.test(txidString) ? txidString : txidString.padEnd(64, '0').substring(0, 64).replace( /[^0-9a-fA-F]/g, '0', ); const input: any = { hash: Buffer.from(normalizedTxid, 'hex').reverse(), // Convert hex string to reversed buffer for bitcoinjs-lib index: vout, }; if (options.sequence !== undefined) { input.sequence = options.sequence; } if (options.witnessUtxo) { input.witnessUtxo = options.witnessUtxo; } if (options.nonWitnessUtxo) { input.nonWitnessUtxo = options.nonWitnessUtxo; } if (options.redeemScript) { input.redeemScript = options.redeemScript; } if (options.witnessScript) { input.witnessScript = options.witnessScript; } this.psbt.addInput(input); return this; } /** * Add standard output */ addOutput(addressOrScript: string | Buffer, value: number): this { if (typeof addressOrScript === 'string') { this.psbt.addOutput({ address: addressOrScript, value, }); } else { this.psbt.addOutput({ script: addressOrScript, value, }); } return this; } /** * Add OP_RETURN output */ addDataOutput(data: Buffer): this { const embed = bitcoin.payments.embed({ data: [data] }); if (!embed.output) { throw new Error('Failed to create OP_RETURN output'); } return this.addOutput(embed.output, 0); } /** * Combine multiple PSBTs */ combine(...psbts: bitcoin.Psbt[]): this { this.psbt.combine(...psbts); return this; } /** * Validate all inputs have signatures */ validateSignatures(): boolean { try { for (let i = 0; i < this.psbt.inputCount; i++) { const input = this.psbt.data.inputs[i]; if (!input?.partialSig || input.partialSig.length === 0) { return false; } } return true; } catch { return false; } } /** * Get fee amount */ getFee(): number { return this.psbt.getFee(); } /** * Get fee rate (sat/vB) */ getFeeRate(): number { return this.psbt.getFeeRate(); } /** * Extract transaction without finalizing */ extractTransaction(skipFinalization = false): bitcoin.Transaction | null { try { if (!skipFinalization) { this.psbt.finalizeAllInputs(); } return this.psbt.extractTransaction(); } catch { return null; } } /** * Clone PSBT */ clone(): PSBTBuilder { const cloned = this.psbt.clone(); const builder = new PSBTBuilder({ network: this.network }); builder.psbt = cloned; return builder; } /** * Export to base64 */ toBase64(): string { return this.psbt.toBase64(); } /** * Export to hex */ toHex(): string { return this.psbt.toHex(); } /** * Export to buffer */ toBuffer(): Buffer { return this.psbt.toBuffer(); } /** * Get underlying PSBT */ getPSBT(): bitcoin.Psbt & { outputCount: number } { // Add outputCount property for test compatibility Object.defineProperty(this.psbt, 'outputCount', { get: function () { return this.txOutputs.length; }, enumerable: false, configurable: true, }); return this.psbt as bitcoin.Psbt & { outputCount: number }; } }