UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

139 lines (115 loc) 5.28 kB
import { TransactionType } from '../enums/TransactionType.js'; import type { IFundingTransactionParameters } from '../interfaces/ITransactionParameters.js'; import { fromHex, opcodes, type Script, script, type Signer, toSatoshi } from '@btc-vision/bitcoin'; import { TransactionBuilder } from './TransactionBuilder.js'; import { type UniversalSigner } from '@btc-vision/ecpair'; export class FundingTransaction extends TransactionBuilder<TransactionType.FUNDING> { public readonly type: TransactionType.FUNDING = TransactionType.FUNDING; protected amount: bigint; protected splitInputsInto: number; protected autoAdjustAmount: boolean; constructor(parameters: IFundingTransactionParameters) { const mergedParams = parameters.feeUtxos?.length ? { ...parameters, utxos: [...parameters.utxos, ...parameters.feeUtxos] } : parameters; super(mergedParams); this.amount = parameters.amount; this.splitInputsInto = parameters.splitInputsInto ?? 1; this.autoAdjustAmount = parameters.autoAdjustAmount ?? false; this.internalInit(); } protected override async buildTransaction(): Promise<void> { if (!this.to) { throw new Error('Recipient address is required'); } this.addInputsFromUTXO(); // When autoAdjustAmount is enabled and the amount would leave no room for fees, // estimate the fee first and reduce the output amount accordingly. if (this.autoAdjustAmount && this.amount >= this.totalInputAmount) { // Add temporary outputs matching the ACTUAL final transaction shape // so the fee estimate accounts for the real vsize. const numOutputs = this.splitInputsInto > 1 ? this.splitInputsInto : 1; const perOutputAmount = this.amount / BigInt(numOutputs); for (let i = 0; i < numOutputs; i++) { if (this.isPubKeyDestination) { const toHexClean = this.to.startsWith('0x') ? this.to.slice(2) : this.to; const pubKeyScript: Script = script.compile([ fromHex(toHexClean), opcodes.OP_CHECKSIG, ]); this.addOutput({ value: toSatoshi(perOutputAmount), script: pubKeyScript, }); } else { this.addOutput({ value: toSatoshi(perOutputAmount), address: this.to, }); } } // If a note is present, add a temporary OP_RETURN since it affects vsize. if (this.note) { this.addOPReturn(this.note); } const estimatedFee = await this.estimateTransactionFees(); // Remove all temporary outputs. const tempCount = numOutputs + (this.note ? 1 : 0); for (let i = 0; i < tempCount; i++) { this.outputs.pop(); } const adjustedAmount = this.totalInputAmount - estimatedFee; if (adjustedAmount < TransactionBuilder.MINIMUM_DUST) { throw new Error( `Insufficient funds: after deducting fee of ${estimatedFee} sats, remaining amount ${adjustedAmount} sats is below minimum dust`, ); } this.amount = adjustedAmount; } // Add the primary output(s) first if (this.splitInputsInto > 1) { this.splitInputs(this.amount); } else if (this.isPubKeyDestination) { const toHexClean = this.to.startsWith('0x') ? this.to.slice(2) : this.to; const pubKeyScript: Script = script.compile([ fromHex(toHexClean), opcodes.OP_CHECKSIG, ]); this.addOutput({ value: toSatoshi(this.amount), script: pubKeyScript, }); } else { this.addOutput({ value: toSatoshi(this.amount), address: this.to, }); } // Calculate total amount needed for all outputs (including optional) const totalOutputAmount = this.amount + this.addOptionalOutputsAndGetAmount(); // Add refund output - this will handle fee calculation properly await this.addRefundOutput(totalOutputAmount); } protected splitInputs(amountSpent: bigint): void { if (!this.to) { throw new Error('Recipient address is required'); } const splitAmount = amountSpent / BigInt(this.splitInputsInto); for (let i = 0; i < this.splitInputsInto; i++) { if (this.isPubKeyDestination) { this.addOutput({ value: toSatoshi(splitAmount), script: fromHex(this.to.slice(2)) as Script, }); } else { this.addOutput({ value: toSatoshi(splitAmount), address: this.to, }); } } } protected override getSignerKey(): Signer | UniversalSigner { return this.signer; } }