@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
977 lines • 37.2 kB
JavaScript
import bitcoin, { equals, fromHex, getFinalScripts, opcodes, Psbt, script, toSatoshi, toXOnly, Transaction, } from '@btc-vision/bitcoin';
import { witnessStackToScriptWitness } from '../utils/WitnessUtils.js';
import { TransactionType } from '../enums/TransactionType.js';
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
import {} from '@btc-vision/ecpair';
import { AddressVerificator } from '../../keypair/AddressVerificator.js';
import { TweakedTransaction } from '../shared/TweakedTransaction.js';
import { UnisatSigner } from '../browser/extensions/UnisatSigner.js';
import { P2WDADetector } from '../../p2wda/P2WDADetector.js';
import { FeaturePriority, Features, } from '../../generators/Features.js';
import { BITCOIN_PROTOCOL_ID, getChainId } from '../../chain/ChainData.js';
import { BinaryWriter } from '../../buffer/BinaryWriter.js';
import { MLDSASecurityLevel } from '@btc-vision/bip32';
import { MessageSigner } from '../../keypair/MessageSigner.js';
import { getLevelFromPublicKeyLength } from '../../generators/MLDSAData.js';
export const MINIMUM_AMOUNT_REWARD = 330n; //540n;
export const MINIMUM_AMOUNT_CA = 297n;
export const ANCHOR_SCRIPT = fromHex('51024e73');
/**
* Allows to build a transaction like you would on Ethereum.
* @description The transaction builder class
* @abstract
* @class TransactionBuilder
*/
export class TransactionBuilder extends TweakedTransaction {
static MINIMUM_DUST = 330n;
logColor = '#785def';
debugFees = false;
// Cancel script
LOCK_LEAF_SCRIPT;
/**
* @description The overflow fees of the transaction
* @public
*/
overflowFees = 0n;
/**
* @description Cost in satoshis of the transaction fee
*/
transactionFee = 0n;
/**
* @description The estimated fees of the transaction
*/
estimatedFees = 0n;
/**
* @param {ITransactionParameters} parameters - The transaction parameters
*/
optionalOutputs;
/**
* @description The transaction itself.
*/
transaction;
/**
* @description Inputs to update later on.
*/
updateInputs = [];
/**
* @description The outputs of the transaction
*/
outputs = [];
/**
* @description Output that will be used to pay the fees
*/
feeOutput = null;
/**
* @description The total amount of satoshis in the inputs
*/
totalInputAmount;
/**
* @description The signer of the transaction
*/
signer;
/**
* @description The network where the transaction will be broadcasted
*/
network;
/**
* @description The fee rate of the transaction
*/
feeRate;
/**
* @description The opnet priority fee of the transaction
*/
priorityFee;
gasSatFee;
/**
* @description The utxos used in the transaction
*/
utxos;
/**
* @description The inputs of the transaction
* @protected
*/
optionalInputs;
/**
* @description The address where the transaction is sent to
* @protected
*/
to;
/**
* @description The address where the transaction is sent from
* @protected
*/
from;
/**
* @description The maximum fee rate of the transaction
*/
_maximumFeeRate = 100000000;
/**
* @description Is the destionation P2PK
* @protected
*/
isPubKeyDestination;
/**
* @description If the transaction need an anchor output
* @protected
*/
anchor;
note;
optionalOutputsAdded = false;
constructor(parameters) {
super(parameters);
if (parameters.estimatedFees) {
this.estimatedFees = parameters.estimatedFees;
}
this.signer = parameters.signer;
this.network = parameters.network;
this.feeRate = parameters.feeRate;
this.priorityFee = parameters.priorityFee ?? 0n;
this.gasSatFee = parameters.gasSatFee ?? 0n;
this.utxos = parameters.utxos;
this.optionalInputs = parameters.optionalInputs || [];
this.to = parameters.to || undefined;
this.debugFees = parameters.debugFees || false;
this.LOCK_LEAF_SCRIPT = this.defineLockScript();
if (parameters.note) {
if (typeof parameters.note === 'string') {
this.note = new TextEncoder().encode(parameters.note);
}
else {
this.note = parameters.note;
}
}
this.anchor = parameters.anchor ?? false;
this.isPubKeyDestination = this.to
? AddressVerificator.isValidPublicKey(this.to, this.network)
: false;
this.optionalOutputs = parameters.optionalOutputs;
this.from = TransactionBuilder.getFrom(parameters.from, this.signer, this.network);
this.totalInputAmount = this.calculateTotalUTXOAmount();
const totalVOut = this.calculateTotalVOutAmount();
if (totalVOut < this.totalInputAmount) {
throw new Error(`Vout value is less than the value to send`);
}
this.transaction = new Psbt({
network: this.network,
version: this.txVersion,
});
}
static getFrom(from, keypair, network) {
return from || EcKeyPair.getTaprootAddress(keypair, network);
}
/**
* @description Converts the witness stack to a script witness
* @param {Uint8Array[]} witness - The witness stack
* @protected
* @returns {Uint8Array}
*/
static witnessStackToScriptWitness(witness) {
return witnessStackToScriptWitness(witness);
}
[Symbol.dispose]() {
super[Symbol.dispose]();
this.updateInputs.length = 0;
this.outputs.length = 0;
this.feeOutput = null;
this.optionalOutputs = undefined;
this.utxos = [];
this.optionalInputs = [];
}
addOPReturn(buffer) {
const compileScript = script.compile([opcodes.OP_RETURN, buffer]);
this.addOutput({
value: toSatoshi(0n),
script: compileScript,
});
}
addAnchor() {
this.addOutput({
value: toSatoshi(0n),
script: ANCHOR_SCRIPT,
});
}
async getFundingTransactionParameters() {
if (!this.estimatedFees) {
this.estimatedFees = await this.estimateTransactionFees();
}
return {
utxos: this.utxos,
to: this.getScriptAddress(),
signer: this.signer,
network: this.network,
feeRate: this.feeRate,
priorityFee: this.priorityFee ?? 0n,
gasSatFee: this.gasSatFee ?? 0n,
from: this.from,
amount: this.estimatedFees,
optionalInputs: this.optionalInputs,
mldsaSigner: null,
...(this.optionalOutputs !== undefined
? { optionalOutputs: this.optionalOutputs }
: {}),
};
}
/**
* Set the destination address of the transaction
* @param {string} address - The address to set
*/
setDestinationAddress(address) {
this.to = address; // this.getScriptAddress()
}
/**
* Set the maximum fee rate of the transaction in satoshis per byte
* @param {number} feeRate - The fee rate to set
* @public
*/
setMaximumFeeRate(feeRate) {
this._maximumFeeRate = feeRate;
}
/**
* @description Signs the transaction
* @public
* @returns {Promise<Transaction>} - The signed transaction in hex format
* @throws {Error} - If something went wrong
*/
async signTransaction() {
if (!this.utxos.length) {
throw new Error('No UTXOs specified');
}
if (this.to &&
!this.isPubKeyDestination &&
!EcKeyPair.verifyContractAddress(this.to, this.network)) {
throw new Error('Invalid contract address. The contract address must be a taproot address.');
}
if (this.signed)
throw new Error('Transaction is already signed');
this.signed = true;
await this.buildTransaction();
const builtTx = await this.internalBuildTransaction(this.transaction);
if (builtTx) {
if (this.regenerated) {
throw new Error('Transaction was regenerated');
}
return this.transaction.extractTransaction(true, true);
}
throw new Error('Could not sign transaction');
}
/**
* @description Generates the transaction minimal signatures
* @public
*/
async generateTransactionMinimalSignatures(checkPartialSigs = false) {
if (this.to &&
!this.isPubKeyDestination &&
!EcKeyPair.verifyContractAddress(this.to, this.network)) {
throw new Error('Invalid contract address. The contract address must be a taproot address.');
}
await this.buildTransaction();
if (this.transaction.data.inputs.length === 0) {
const inputs = this.getInputs();
const outputs = this.getOutputs();
this.transaction.setMaximumFeeRate(this._maximumFeeRate);
this.transaction.addInputs(inputs, checkPartialSigs);
for (let i = 0; i < this.updateInputs.length; i++) {
this.transaction.updateInput(i, this.updateInputs[i]);
}
this.transaction.addOutputs(outputs);
}
}
/**
* @description Signs the transaction
* @public
* @returns {Promise<Psbt>} - The signed transaction in hex format
* @throws {Error} - If something went wrong
*/
async signPSBT() {
if (await this.signTransaction()) {
return this.transaction;
}
throw new Error('Could not sign transaction');
}
/**
* Add an input to the transaction.
* @param {PsbtInputExtended} input - The input to add
* @public
* @returns {void}
*/
addInput(input) {
this.inputs.push(input);
}
/**
* Add an output to the transaction.
* @param {PsbtOutputExtended} output - The output to add
* @param bypassMinCheck
* @public
* @returns {void}
*/
addOutput(output, bypassMinCheck = false) {
if (output.value === toSatoshi(0n)) {
const scriptOutput = output;
if (!scriptOutput.script || scriptOutput.script.length === 0) {
throw new Error('Output value is 0 and no script provided');
}
if (scriptOutput.script.length < 2) {
throw new Error('Output script is too short');
}
if (scriptOutput.script[0] !== opcodes.OP_RETURN &&
!equals(scriptOutput.script, ANCHOR_SCRIPT)) {
throw new Error('Output script must start with OP_RETURN or be an ANCHOR when value is 0');
}
}
else if (!bypassMinCheck && BigInt(output.value) < TransactionBuilder.MINIMUM_DUST) {
throw new Error(`Output value is less than the minimum dust ${output.value} < ${TransactionBuilder.MINIMUM_DUST}`);
}
this.outputs.push(output);
}
/**
* Returns the total value of all outputs added so far (excluding the fee/change output).
* @public
* @returns {bigint}
*/
getTotalOutputValue() {
return this.outputs.reduce((total, output) => total + BigInt(output.value), 0n);
}
/**
* Receiver address.
* @public
* @returns {string} - The receiver address
*/
toAddress() {
return this.to;
}
/**
* @description Returns the script address
* @returns {string} - The script address
*/
address() {
return this.tapData?.address;
}
/**
* Estimates the transaction fees with accurate size calculation.
*
* @note The P2TR estimation is made for a 2-leaf tree with both a tapScriptSig and a tapInternalKey input, which is a common case for many transactions.
* This provides a more accurate fee estimation for typical P2TR transactions, but may not be perfectly accurate for all possible script configurations.
* Adjustments may be needed for more complex scripts or different leaf structures.
*
* @public
* @returns {Promise<bigint>}
*/
async estimateTransactionFees() {
await Promise.resolve();
const fakeTx = new Psbt({ network: this.network });
const inputs = this.getInputs();
const outputs = this.getOutputs();
fakeTx.addInputs(inputs);
fakeTx.addOutputs(outputs);
const dummySchnorrSig = new Uint8Array(64);
const dummyEcdsaSig = new Uint8Array(72);
const dummyCompressedPubkey = new Uint8Array(33).fill(2);
const finalizer = (inputIndex, input) => {
if (input.isPayToAnchor || this.anchorInputIndices.has(inputIndex)) {
return {
finalScriptSig: undefined,
finalScriptWitness: Uint8Array.from([0]),
};
}
if (input.witnessScript && P2WDADetector.isP2WDAWitnessScript(input.witnessScript)) {
// Create dummy witness stack for P2WDA
const dummyDataSlots = [];
for (let i = 0; i < 10; i++) {
dummyDataSlots.push(new Uint8Array(0));
}
const dummyEcdsaSig = new Uint8Array(72);
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
...dummyDataSlots,
dummyEcdsaSig,
input.witnessScript,
]),
};
}
if (inputIndex === 0 && this.tapLeafScript) {
const dummySecret = new Uint8Array(32);
const dummyScript = this.tapLeafScript.script;
// A control block for a 2-leaf tree contains one 32-byte hash.
// P2TR: 33 (version + internal pubkey) + 32 (merkle path) = 65 bytes
// P2MR: 1 (version) + 32 (merkle path) = 33 bytes (no internal pubkey)
const controlBlockSize = this.useP2MR ? 1 + 32 : 1 + 32 + 32;
const dummyControlBlock = new Uint8Array(controlBlockSize);
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
dummySecret,
dummySchnorrSig, // It's a tapScriptSig, which is Schnorr
dummySchnorrSig, // Second Schnorr signature
dummyScript,
dummyControlBlock,
]),
};
}
if (input.witnessUtxo) {
const script = input.witnessUtxo.script;
const decompiled = bitcoin.script.decompile(script);
if (decompiled &&
decompiled.length === 5 &&
decompiled[0] === opcodes.OP_DUP &&
decompiled[1] === opcodes.OP_HASH160 &&
decompiled[3] === opcodes.OP_EQUALVERIFY &&
decompiled[4] === opcodes.OP_CHECKSIG) {
return {
finalScriptSig: bitcoin.script.compile([
dummyEcdsaSig,
dummyCompressedPubkey,
]),
finalScriptWitness: undefined,
};
}
}
if (input.witnessScript) {
if (this.csvInputIndices.has(inputIndex)) {
// CSV P2WSH needs: [signature, witnessScript]
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
dummyEcdsaSig,
input.witnessScript,
]),
};
}
if (input.redeemScript) {
// P2SH-P2WSH needs redeemScript in scriptSig and witness data
const dummyWitness = [dummyEcdsaSig, input.witnessScript];
return {
finalScriptSig: input.redeemScript,
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(dummyWitness),
};
}
const decompiled = bitcoin.script.decompile(input.witnessScript);
if (decompiled && decompiled.length >= 4) {
const firstOp = decompiled[0];
const lastOp = decompiled[decompiled.length - 1];
// Check if it's M-of-N multisig
if (typeof firstOp === 'number' &&
firstOp >= opcodes.OP_1 &&
lastOp === opcodes.OP_CHECKMULTISIG) {
const m = firstOp - opcodes.OP_1 + 1;
const signatures = [];
for (let i = 0; i < m; i++) {
signatures.push(dummyEcdsaSig);
}
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
new Uint8Array(0), // OP_0 due to multisig bug
...signatures,
input.witnessScript,
]),
};
}
}
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
dummyEcdsaSig,
input.witnessScript,
]),
};
}
else if (input.redeemScript) {
const decompiled = bitcoin.script.decompile(input.redeemScript);
if (decompiled &&
decompiled.length === 2 &&
decompiled[0] === opcodes.OP_0 &&
decompiled[1] instanceof Uint8Array &&
decompiled[1].length === 20) {
// P2SH-P2WPKH
return {
finalScriptSig: input.redeemScript,
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
dummyEcdsaSig,
dummyCompressedPubkey,
]),
};
}
}
if (input.redeemScript && !input.witnessScript && !input.witnessUtxo) {
// Pure P2SH needs signatures + redeemScript in scriptSig
return {
finalScriptSig: bitcoin.script.compile([dummyEcdsaSig, input.redeemScript]),
finalScriptWitness: undefined,
};
}
const inputScript = input.witnessUtxo?.script;
if (!inputScript)
return { finalScriptSig: undefined, finalScriptWitness: undefined };
if (input.tapInternalKey) {
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
dummySchnorrSig,
]),
};
}
if (inputScript.length === 22 && inputScript[0] === opcodes.OP_0) {
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
dummyEcdsaSig,
dummyCompressedPubkey,
]),
};
}
if (input.redeemScript?.length === 22 && input.redeemScript[0] === opcodes.OP_0) {
return {
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
dummyEcdsaSig,
dummyCompressedPubkey,
]),
};
}
return getFinalScripts(inputIndex, input, inputScript, true, !!input.redeemScript, !!input.witnessScript);
};
try {
for (let i = 0; i < fakeTx.data.inputs.length; i++) {
const fullInput = inputs[i];
if (fullInput) {
fakeTx.finalizeInput(i, (idx) => finalizer(idx, fullInput));
}
}
}
catch (e) {
this.warn(`Could not finalize dummy tx: ${e.message}`);
}
const tx = fakeTx.extractTransaction(true, true);
const size = tx.virtualSize();
const fee = this.feeRate * size;
const finalFee = BigInt(Math.ceil(fee));
if (this.debugFees) {
this.log(`Estimating fees: feeRate=${this.feeRate}, accurate_vSize=${size}, fee=${finalFee}n`);
}
return finalFee;
}
async rebuildFromBase64(base64) {
this.transaction = Psbt.fromBase64(base64, {
network: this.network,
version: this.txVersion,
});
this.signed = false;
this.sighashTypes = [Transaction.SIGHASH_ANYONECANPAY, Transaction.SIGHASH_ALL];
return await this.signPSBT();
}
setPSBT(psbt) {
this.transaction = psbt;
}
/**
* Returns the inputs of the transaction.
* @protected
* @returns {PsbtInputExtended[]}
*/
getInputs() {
return this.inputs;
}
/**
* Returns the outputs of the transaction.
* @protected
* @returns {PsbtOutputExtended[]}
*/
getOutputs() {
const outputs = [...this.outputs];
if (this.feeOutput)
outputs.push(this.feeOutput);
return outputs;
}
getOptionalOutputValue() {
if (!this.optionalOutputs)
return 0n;
let total = 0n;
for (let i = 0; i < this.optionalOutputs.length; i++) {
total += BigInt(this.optionalOutputs[i].value);
}
return total;
}
async addRefundOutput(amountSpent, expectRefund = false) {
if (this.note) {
this.addOPReturn(this.note);
}
if (this.anchor) {
this.addAnchor();
}
// Add a dummy change output to estimate fee with the change-output shape
this.feeOutput = this.createChangeOutput(TransactionBuilder.MINIMUM_DUST);
const feeWithChange = await this.estimateTransactionFees();
const sendBackAmount = this.totalInputAmount - amountSpent - feeWithChange;
if (this.debugFees) {
this.log(`Fee with change: ${feeWithChange} sats, inputAmount=${this.totalInputAmount}, amountSpent=${amountSpent}, sendBackAmount=${sendBackAmount}`);
}
if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
// Change output is viable, set it to the real value
this.feeOutput = this.createChangeOutput(sendBackAmount);
this.overflowFees = sendBackAmount;
this.transactionFee = feeWithChange;
}
else {
// Change output not viable, remove it and re-estimate without it
this.feeOutput = null;
this.overflowFees = 0n;
const feeWithoutChange = await this.estimateTransactionFees();
this.transactionFee = feeWithoutChange;
if (this.debugFees) {
this.warn(`Amount to send back (${sendBackAmount} sat) is less than minimum dust. Fee without change: ${feeWithoutChange} sats`);
}
if (this.totalInputAmount <= amountSpent) {
throw new Error(`Insufficient funds: need ${amountSpent + feeWithoutChange} sats but only have ${this.totalInputAmount} sats`);
}
if (expectRefund && sendBackAmount < 0n) {
throw new Error(`Insufficient funds: need at least ${-sendBackAmount} more sats to cover fees.`);
}
}
if (this.debugFees) {
this.log(`Final fee: ${this.transactionFee} sats, Change output: ${this.feeOutput ? `${this.feeOutput.value} sats` : 'none'}`);
}
}
defineLockScript() {
return script.compile([toXOnly(this.signer.publicKey), opcodes.OP_CHECKSIG]);
}
/**
* @description Adds the value to the output
* @param {number | bigint} value - The value to add
* @protected
* @returns {void}
*/
addValueToToOutput(value) {
if (BigInt(value) < TransactionBuilder.MINIMUM_DUST) {
throw new Error(`Value to send is less than the minimum dust ${value} < ${TransactionBuilder.MINIMUM_DUST}`);
}
for (let i = 0; i < this.outputs.length; i++) {
const output = this.outputs[i];
if ('address' in output && output.address === this.to) {
this.outputs[i] = {
...output,
value: toSatoshi(BigInt(output.value) + BigInt(value)),
};
return;
}
}
throw new Error('Output not found');
}
generateLegacySignature() {
this.tweakSigner();
if (!this.tweakedSigner) {
throw new Error('Tweaked signer is not defined');
}
const tweakedKey = toXOnly(this.tweakedSigner.publicKey);
const originalKey = this.signer.publicKey;
if (originalKey.length !== 33) {
throw new Error('Original public key must be compressed (33 bytes)');
}
const chainId = getChainId(this.network);
const writer = new BinaryWriter();
// ONLY SUPPORT MLDSA-44 FOR NOW.
writer.writeU8(MLDSASecurityLevel.LEVEL2);
writer.writeBytes(this.hashedPublicKey);
writer.writeBytes(tweakedKey);
writer.writeBytes(originalKey);
writer.writeBytes(BITCOIN_PROTOCOL_ID);
writer.writeBytes(chainId);
const message = writer.getBuffer();
const signature = MessageSigner.signMessage(this.tweakedSigner, message);
const isValid = MessageSigner.verifySignature(tweakedKey, message, signature.signature);
if (!isValid) {
throw new Error('Could not verify generated legacy signature for MLDSA link request');
}
return new Uint8Array(signature.signature);
}
generateMLDSASignature() {
if (!this.mldsaSigner) {
throw new Error('MLDSA signer is not defined');
}
this.tweakSigner();
if (!this.tweakedSigner) {
throw new Error('Tweaked signer is not defined');
}
const tweakedKey = toXOnly(this.tweakedSigner.publicKey);
const originalKey = this.signer.publicKey;
if (originalKey.length !== 33) {
throw new Error('Original public key must be compressed (33 bytes)');
}
const chainId = getChainId(this.network);
const level = getLevelFromPublicKeyLength(this.mldsaSigner.publicKey.length);
if (level !== MLDSASecurityLevel.LEVEL2) {
throw new Error('Only MLDSA level 2 is supported for link requests');
}
const writer = new BinaryWriter();
writer.writeU8(level);
writer.writeBytes(this.hashedPublicKey);
writer.writeBytes(this.mldsaSigner.publicKey);
writer.writeBytes(tweakedKey);
writer.writeBytes(originalKey);
writer.writeBytes(BITCOIN_PROTOCOL_ID);
writer.writeBytes(chainId);
const message = writer.getBuffer();
const signature = MessageSigner.signMLDSAMessage(this.mldsaSigner, message);
const isValid = MessageSigner.verifyMLDSASignature(this.mldsaSigner, message, signature.signature);
if (!isValid) {
throw new Error('Could not verify generated MLDSA signature for link request');
}
return new Uint8Array(signature.signature);
}
generateMLDSALinkRequest(parameters, features) {
const mldsaSigner = this.mldsaSigner;
const legacySignature = this.generateLegacySignature();
let mldsaSignature = null;
if (parameters.revealMLDSAPublicKey) {
mldsaSignature = this.generateMLDSASignature();
}
const mldsaRequest = {
priority: FeaturePriority.MLDSA_LINK_PUBKEY,
opcode: Features.MLDSA_LINK_PUBKEY,
data: {
verifyRequest: !!parameters.revealMLDSAPublicKey,
publicKey: mldsaSigner.publicKey,
hashedPublicKey: this.hashedPublicKey,
level: getLevelFromPublicKeyLength(mldsaSigner.publicKey.length),
legacySignature: legacySignature,
mldsaSignature: mldsaSignature,
},
};
features.push(mldsaRequest);
}
/**
* @description Returns the transaction opnet fee
* @protected
* @returns {bigint}
*/
getTransactionOPNetFee() {
const totalFee = this.priorityFee + this.gasSatFee;
if (totalFee > TransactionBuilder.MINIMUM_DUST) {
return totalFee;
}
return TransactionBuilder.MINIMUM_DUST;
}
/**
* @description Returns the total amount of satoshis in the inputs
* @protected
* @returns {bigint}
*/
calculateTotalUTXOAmount() {
let total = 0n;
for (const utxo of this.utxos) {
total += utxo.value;
}
for (const utxo of this.optionalInputs) {
total += utxo.value;
}
return total;
}
/**
* @description Returns the total amount of satoshis in the outputs
* @protected
* @returns {bigint}
*/
calculateTotalVOutAmount() {
let total = 0n;
for (const utxo of this.utxos) {
total += utxo.value;
}
for (const utxo of this.optionalInputs) {
total += utxo.value;
}
return total;
}
/**
* @description Adds optional outputs to transaction and returns their total value in satoshi to calculate refund transaction
* @protected
* @returns {bigint}
*/
addOptionalOutputsAndGetAmount() {
if (!this.optionalOutputs || this.optionalOutputsAdded)
return 0n;
let refundedFromOptionalOutputs = 0n;
for (let i = 0; i < this.optionalOutputs.length; i++) {
this.addOutput(this.optionalOutputs[i]);
refundedFromOptionalOutputs += BigInt(this.optionalOutputs[i].value);
}
this.optionalOutputsAdded = true;
return refundedFromOptionalOutputs;
}
/**
* @description Adds the inputs from the utxos
* @protected
* @returns {void}
*/
addInputsFromUTXO() {
if (this.utxos.length) {
//throw new Error('No UTXOs specified');
if (this.totalInputAmount < TransactionBuilder.MINIMUM_DUST) {
throw new Error(`Total input amount is ${this.totalInputAmount} sat which is less than the minimum dust ${TransactionBuilder.MINIMUM_DUST} sat.`);
}
for (let i = 0; i < this.utxos.length; i++) {
const utxo = this.utxos[i];
// Register signer BEFORE generating input (needed for tapInternalKey)
this.registerInputSigner(i, utxo);
const input = this.generatePsbtInputExtended(utxo, i);
this.addInput(input);
}
}
if (this.optionalInputs) {
for (let i = this.utxos.length; i < this.optionalInputs.length + this.utxos.length; i++) {
const utxo = this.optionalInputs[i - this.utxos.length];
// Register signer BEFORE generating input (needed for tapInternalKey)
this.registerInputSigner(i, utxo);
const input = this.generatePsbtInputExtended(utxo, i, true);
this.addInput(input);
}
}
}
/**
* Internal init.
* @protected
*/
internalInit() {
this.verifyUTXOValidity();
super.internalInit();
}
/**
* Add an input update
* @param {UpdateInput} input - The input to update
* @protected
* @returns {void}
*/
updateInput(input) {
this.updateInputs.push(input);
}
/**
* Adds the fee to the output.
* @param amountSpent
* @param contractAddress
* @param epochChallenge
* @param addContractOutput
* @protected
*/
addFeeToOutput(amountSpent, contractAddress, epochChallenge, addContractOutput) {
if (addContractOutput) {
let amountToCA;
if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
amountToCA = MINIMUM_AMOUNT_CA;
}
else {
amountToCA = amountSpent;
}
// ALWAYS THE FIRST INPUT.
this.addOutput({
value: toSatoshi(amountToCA),
address: contractAddress,
}, true);
// ALWAYS SECOND.
if (amountToCA === MINIMUM_AMOUNT_CA &&
amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD) {
this.addOutput({
value: toSatoshi(amountSpent - amountToCA),
address: epochChallenge.address,
}, true);
}
}
else {
// When SEND_AMOUNT_TO_CA is false, always send to epochChallenge
// Use the maximum of amountSpent or MINIMUM_AMOUNT_REWARD
const amountToEpoch = amountSpent < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : amountSpent;
this.addOutput({
value: toSatoshi(amountToEpoch),
address: epochChallenge.address,
}, true);
}
}
/**
* Returns the witness of the tap transaction.
* @protected
* @returns {Uint8Array}
*/
getWitness() {
if (!this.tapData || !this.tapData.witness) {
throw new Error('Witness is required');
}
if (this.tapData.witness.length === 0) {
throw new Error('Witness is empty');
}
return this.tapData.witness[this.tapData.witness.length - 1];
}
/**
* Returns the tap output.
* @protected
* @returns {Uint8Array}
*/
getTapOutput() {
if (!this.tapData || !this.tapData.output) {
throw new Error('Tap data is required');
}
return this.tapData.output;
}
/**
* Verifies that the utxos are valid.
* @protected
*/
verifyUTXOValidity() {
for (const utxo of this.utxos) {
if (!utxo.scriptPubKey) {
throw new Error('Address is required');
}
}
for (const utxo of this.optionalInputs) {
if (!utxo.scriptPubKey) {
throw new Error('Address is required');
}
}
}
/**
* Builds the transaction.
* @param {Psbt} transaction - The transaction to build
* @param checkPartialSigs
* @protected
* @returns {Promise<boolean>}
* @throws {Error} - If something went wrong while building the transaction
*/
async internalBuildTransaction(transaction, checkPartialSigs = false) {
if (transaction.data.inputs.length === 0) {
const inputs = this.getInputs();
const outputs = this.getOutputs();
transaction.setMaximumFeeRate(this._maximumFeeRate);
transaction.addInputs(inputs, checkPartialSigs);
for (let i = 0; i < this.updateInputs.length; i++) {
transaction.updateInput(i, this.updateInputs[i]);
}
transaction.addOutputs(outputs);
}
try {
await this.signInputs(transaction);
if (this.finalized) {
this.transactionFee = BigInt(transaction.getFee());
}
return true;
}
catch (e) {
const err = e;
this.error(`[internalBuildTransaction] Something went wrong while getting building the transaction: ${err.stack}`);
}
return false;
}
createChangeOutput(amount) {
if (AddressVerificator.isValidP2TRAddress(this.from, this.network)) {
return {
value: toSatoshi(amount),
address: this.from,
tapInternalKey: this.internalPubKeyToXOnly(),
};
}
else if (AddressVerificator.isValidPublicKey(this.from, this.network)) {
const pubKeyScript = script.compile([
fromHex(this.from.startsWith('0x') ? this.from.slice(2) : this.from),
opcodes.OP_CHECKSIG,
]);
return {
value: toSatoshi(amount),
script: pubKeyScript,
};
}
else {
return {
value: toSatoshi(amount),
address: this.from,
};
}
}
}
//# sourceMappingURL=TransactionBuilder.js.map