@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
text/typescript
/**
* 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 };
}
}