UNPKG

@btc-vision/transaction

Version:

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

977 lines 37.2 kB
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