import UnlockingScript from '../script/UnlockingScript.js';
import LockingScript from '../script/LockingScript.js';
import { Writer, toHex, toArray, ReaderUint8Array, toUint8Array, WriterUint8Array } from '../primitives/utils.js';
import { hash256 } from '../primitives/Hash.js';
import LivePolicy from './fee-models/LivePolicy.js';
import Spend from '../script/Spend.js';
import { defaultBroadcaster } from './broadcasters/DefaultBroadcaster.js';
import { defaultChainTracker } from './chaintrackers/DefaultChainTracker.js';
import { Beef, BEEF_V1 } from './Beef.js';
import P2PKH from '../script/templates/P2PKH.js';
import TransactionSignature from '../primitives/TransactionSignature.js';
import Random from '../primitives/Random.js';
/**
 * Represents a complete Bitcoin transaction. This class encapsulates all the details
 * required for creating, signing, and processing a Bitcoin transaction, including
 * inputs, outputs, and various transaction-related methods.
 *
 * @class Transaction
 * @property {number} version - The version number of the transaction. Used to specify
 *           which set of rules this transaction follows.
 * @property {TransactionInput[]} inputs - An array of TransactionInput objects, representing
 *           the inputs for the transaction. Each input references a previous transaction's output.
 * @property {TransactionOutput[]} outputs - An array of TransactionOutput objects, representing
 *           the outputs for the transaction. Each output specifies the amount of satoshis to be
 *           transferred and the conditions under which they can be spent.
 * @property {number} lockTime - The lock time of the transaction. If non-zero, it specifies the
 *           earliest time or block height at which the transaction can be added to the block chain.
 * @property {Record<string, any>} metadata - A key-value store for attaching additional data to
 *           the transaction object, not included in the transaction itself. Useful for adding descriptions, internal reference numbers, or other information.
 * @property {MerkleProof} [merklePath] - Optional. A merkle proof demonstrating the transaction's
 *           inclusion in a block. Useful for transaction verification using SPV.
 *
 * @example
 * // Creating a new transaction
 * let tx = new Transaction();
 * tx.addInput(...);
 * tx.addOutput(...);
 * await tx.fee();
 * await tx.sign();
 * await tx.broadcast();
 *
 * @description
 * The Transaction class provides comprehensive
 * functionality to handle various aspects of transaction creation, including
 * adding inputs and outputs, computing fees, signing the transaction, and
 * generating its binary or hexadecimal representation.
 */
export default class Transaction {
    version;
    inputs;
    outputs;
    lockTime;
    metadata;
    merklePath;
    cachedHash;
    rawBytesCache;
    hexCache;
    // Recursive function for adding merkle proofs or input transactions
    static addPathOrInputs(obj, transactions, BUMPs) {
        if (typeof obj.pathIndex === 'number') {
            const path = BUMPs[obj.pathIndex];
            if (typeof path !== 'object') {
                throw new TypeError('Invalid merkle path index found in BEEF!');
            }
            obj.tx.merklePath = path;
        }
        else {
            for (const input of obj.tx.inputs) {
                if (input.sourceTXID === undefined) {
                    throw new Error('Input sourceTXID is undefined');
                }
                const sourceObj = transactions[input.sourceTXID];
                if (typeof sourceObj !== 'object') {
                    throw new TypeError(`Reference to unknown TXID in BEEF: ${input.sourceTXID ?? 'undefined'}`);
                }
                input.sourceTransaction = sourceObj.tx;
                this.addPathOrInputs(sourceObj, transactions, BUMPs);
            }
        }
    }
    /**
     * Creates a new transaction, linked to its inputs and their associated merkle paths, from a BEEF V1, V2 or Atomic.
     * Optionally, you can provide a specific TXID to retrieve a particular transaction from the BEEF data.
     * If the TXID is provided but not found in the BEEF data, an error will be thrown.
     * If no TXID is provided, the last transaction in the BEEF data is returned, or the atomic txid.
     * @param beef A binary representation of transactions in BEEF format.
     * @param txid Optional TXID of the transaction to retrieve from the BEEF data.
     * @returns An anchored transaction, linked to its associated inputs populated with merkle paths.
     */
    static fromBEEF(beef, txid) {
        const { tx } = Transaction.fromAnyBeef(beef, txid);
        return tx;
    }
    /**
     * Creates a new transaction from an Atomic BEEF (BRC-95) structure.
     * Extracts the subject transaction and supporting merkle path and source transactions contained in the BEEF data
     *
     * @param beef A binary representation of an Atomic BEEF structure.
     * @returns The subject transaction, linked to its associated inputs populated with merkle paths.
     */
    static fromAtomicBEEF(beef) {
        const { tx, txid, beef: b } = Transaction.fromAnyBeef(beef);
        if (txid !== b.atomicTxid) {
            if (b.atomicTxid == null) {
                throw new Error('beef must conform to BRC-95 and must contain the subject txid.');
            }
            else {
                throw new Error(`Transaction with TXID ${b.atomicTxid} not found in BEEF data.`);
            }
        }
        return tx;
    }
    static fromAnyBeef(beef, txid) {
        const b = Beef.fromBinary(beef);
        if (b.txs.length < 1) {
            throw new Error('beef must include at least one transaction.');
        }
        const lastTx = b.txs.at(-1);
        if (lastTx == null) {
            throw new Error('beef must include at least one transaction.');
        }
        const target = txid ?? b.atomicTxid ?? lastTx.txid;
        const tx = b.findAtomicTransaction(target);
        if (tx == null) {
            if (txid == null) {
                throw new Error('beef does not contain transaction for atomic txid.');
            }
            else {
                throw new Error(`Transaction with TXID ${String(target)} not found in BEEF data.`);
            }
        }
        return { tx, beef: b, txid: target };
    }
    /**
     * Creates a new transaction, linked to its inputs and their associated merkle paths, from a EF (BRC-30) structure.
     * @param ef A binary representation of a transaction in EF format.
     * @returns An extended transaction, linked to its associated inputs by locking script and satoshis amounts only.
     */
    static fromEF(ef) {
        const br = ReaderUint8Array.makeReader(ef);
        const version = br.readUInt32LE();
        if (toHex(br.read(6)) !== '0000000000ef') {
            throw new Error('Invalid EF marker');
        }
        const inputsLength = br.readVarIntNum();
        const inputs = [];
        for (let i = 0; i < inputsLength; i++) {
            const sourceTXID = toHex(br.readReverse(32));
            const sourceOutputIndex = br.readUInt32LE();
            const scriptLength = br.readVarIntNum();
            const scriptBin = br.read(scriptLength);
            const unlockingScript = UnlockingScript.fromBinary(scriptBin);
            const sequence = br.readUInt32LE();
            const satoshis = br.readUInt64LEBn().toNumber();
            const lockingScriptLength = br.readVarIntNum();
            const lockingScriptBin = br.read(lockingScriptLength);
            const lockingScript = LockingScript.fromBinary(lockingScriptBin);
            const sourceTransaction = new Transaction(undefined, [], [], undefined);
            sourceTransaction.outputs = new Array(sourceOutputIndex + 1).fill(null);
            sourceTransaction.outputs[sourceOutputIndex] = {
                satoshis,
                lockingScript
            };
            inputs.push({
                sourceTransaction,
                sourceTXID,
                sourceOutputIndex,
                unlockingScript,
                sequence
            });
        }
        const outputsLength = br.readVarIntNum();
        const outputs = [];
        for (let i = 0; i < outputsLength; i++) {
            const satoshis = br.readUInt64LEBn().toNumber();
            const scriptLength = br.readVarIntNum();
            const scriptBin = br.read(scriptLength);
            const lockingScript = LockingScript.fromBinary(scriptBin);
            outputs.push({
                satoshis,
                lockingScript
            });
        }
        const lockTime = br.readUInt32LE();
        return new Transaction(version, inputs, outputs, lockTime);
    }
    /**
     * Since the validation of blockchain data is atomically transaction data validation,
     * any application seeking to validate data in output scripts must store the entire transaction as well.
     * Since the transaction data includes the output script data, saving a second copy of potentially
     * large scripts can bloat application storage requirements.
     *
     * This function efficiently parses binary transaction data to determine the offsets and lengths of each script.
     * This supports the efficient retreival of script data from transaction data.
     *
     * @param bin binary transaction data
     * @returns {
     *   inputs: { vin: number, offset: number, length: number }[]
     *   outputs: { vout: number, offset: number, length: number }[]
     * }
     */
    static parseScriptOffsets(bin) {
        const br = ReaderUint8Array.makeReader(bin);
        const inputs = [];
        const outputs = [];
        br.pos += 4; // version
        const inputsLength = br.readVarIntNum();
        for (let i = 0; i < inputsLength; i++) {
            br.pos += 36; // txid and vout
            const scriptLength = br.readVarIntNum();
            inputs.push({ vin: i, offset: br.pos, length: scriptLength });
            br.pos += scriptLength + 4; // script and sequence
        }
        const outputsLength = br.readVarIntNum();
        for (let i = 0; i < outputsLength; i++) {
            br.pos += 8; // satoshis
            const scriptLength = br.readVarIntNum();
            outputs.push({ vout: i, offset: br.pos, length: scriptLength });
            br.pos += scriptLength;
        }
        return { inputs, outputs };
    }
    static fromReader(br) {
        const version = br.readUInt32LE();
        const inputsLength = br.readVarIntNum();
        const inputs = [];
        for (let i = 0; i < inputsLength; i++) {
            const sourceTXID = toHex(br.readReverse(32));
            const sourceOutputIndex = br.readUInt32LE();
            const scriptLength = br.readVarIntNum();
            const scriptBin = br.read(scriptLength);
            const unlockingScript = UnlockingScript.fromBinary(scriptBin);
            const sequence = br.readUInt32LE();
            inputs.push({
                sourceTXID,
                sourceOutputIndex,
                unlockingScript,
                sequence
            });
        }
        const outputsLength = br.readVarIntNum();
        const outputs = [];
        for (let i = 0; i < outputsLength; i++) {
            const satoshis = br.readUInt64LEBn().toNumber();
            const scriptLength = br.readVarIntNum();
            const scriptBin = br.read(scriptLength);
            const lockingScript = LockingScript.fromBinary(scriptBin);
            outputs.push({
                satoshis,
                lockingScript
            });
        }
        const lockTime = br.readUInt32LE();
        return new Transaction(version, inputs, outputs, lockTime);
    }
    /**
     * Creates a Transaction instance from a binary array.
     *
     * @static
     * @param {number[]} bin - The binary array representation of the transaction.
     * @returns {Transaction} - A new Transaction instance.
     */
    static fromBinary(bin) {
        const copy = bin.slice();
        const rawBytes = Uint8Array.from(copy);
        const br = new ReaderUint8Array(rawBytes);
        const tx = Transaction.fromReader(br);
        tx.rawBytesCache = rawBytes;
        return tx;
    }
    /**
     * Creates a Transaction instance from a hexadecimal string.
     *
     * @static
     * @param {string} hex - The hexadecimal string representation of the transaction.
     * @returns {Transaction} - A new Transaction instance.
     */
    static fromHex(hex) {
        const rawBytes = toUint8Array(hex, 'hex');
        const br = new ReaderUint8Array(rawBytes);
        const tx = Transaction.fromReader(br);
        tx.rawBytesCache = rawBytes;
        tx.hexCache = toHex(rawBytes);
        return tx;
    }
    /**
     * Creates a Transaction instance from a hexadecimal string encoded EF.
     *
     * @static
     * @param {string} hex - The hexadecimal string representation of the transaction EF.
     * @returns {Transaction} - A new Transaction instance.
     */
    static fromHexEF(hex) {
        return Transaction.fromEF(toUint8Array(hex, 'hex'));
    }
    /**
     * Creates a Transaction instance from a hexadecimal string encoded BEEF.
     * Optionally, you can provide a specific TXID to retrieve a particular transaction from the BEEF data.
     * If the TXID is provided but not found in the BEEF data, an error will be thrown.
     * If no TXID is provided, the last transaction in the BEEF data is returned.
     *
     * @static
     * @param {string} hex - The hexadecimal string representation of the transaction BEEF.
     * @param {string} [txid] - Optional TXID of the transaction to retrieve from the BEEF data.
     * @returns {Transaction} - A new Transaction instance.
     */
    static fromHexBEEF(hex, txid) {
        return Transaction.fromBEEF(toArray(hex, 'hex'), txid);
    }
    constructor(version = 1, inputs = [], outputs = [], lockTime = 0, metadata = new Map(), merklePath) {
        this.version = version;
        this.inputs = inputs;
        this.outputs = outputs;
        this.lockTime = lockTime;
        this.metadata = metadata;
        this.merklePath = merklePath;
    }
    invalidateSerializationCaches() {
        this.cachedHash = undefined;
        this.rawBytesCache = undefined;
        this.hexCache = undefined;
    }
    /**
     * Adds a new input to the transaction.
     *
     * @param {TransactionInput} input - The TransactionInput object to add to the transaction.
     * @throws {Error} - If the input does not have a sourceTXID or sourceTransaction defined.
     */
    addInput(input) {
        if (input.sourceTXID === undefined &&
            input.sourceTransaction === undefined) {
            throw new TypeError('A reference to an an input transaction is required. If the input transaction itself cannot be referenced, its TXID must still be provided.');
        }
        // If the input sequence number hasn't been set, the expectation is that it is final.
        input.sequence ??= 0xffffffff;
        this.invalidateSerializationCaches();
        this.inputs.push(input);
    }
    /**
     * Adds a new output to the transaction.
     *
     * @param {TransactionOutput} output - The TransactionOutput object to add to the transaction.
     */
    addOutput(output) {
        this.cachedHash = undefined;
        if (output.change !== true) {
            if (output.satoshis === undefined) {
                throw new TypeError('either satoshis must be defined or change must be set to true');
            }
            if (output.satoshis < 0) {
                throw new Error('satoshis must be a positive integer or zero');
            }
        }
        if (output.lockingScript == null)
            throw new Error('lockingScript must be defined');
        this.outputs.push(output);
    }
    /**
     * Adds a new P2PKH output to the transaction.
     *
     * @param {number[] | string} address - The P2PKH address of the output.
     * @param {number} [satoshis] - The number of satoshis to send to the address - if not provided, the output is considered a change output.
     *
     */
    addP2PKHOutput(address, satoshis) {
        const lockingScript = new P2PKH().lock(address);
        if (satoshis === undefined) {
            return this.addOutput({ lockingScript, change: true });
        }
        this.addOutput({
            lockingScript,
            satoshis
        });
    }
    /**
     * Updates the transaction's metadata.
     *
     * @param {Record<string, any>} metadata - The metadata object to merge into the existing metadata.
     */
    updateMetadata(metadata) {
        this.metadata = {
            ...this.metadata,
            ...metadata
        };
    }
    /**
     * Computes fees prior to signing.
     * If no fee model is provided, uses a LivePolicy fee model that fetches current rates from ARC.
     * If fee is a number, the transaction uses that value as fee.
     *
     * @param modelOrFee - The initialized fee model to use or fixed fee for the transaction
     * @param changeDistribution - Specifies how the change should be distributed
     * amongst the change outputs
     *
     */
    async fee(modelOrFee = LivePolicy.getInstance(), changeDistribution = 'equal') {
        this.invalidateSerializationCaches();
        if (typeof modelOrFee === 'number') {
            const sats = modelOrFee;
            modelOrFee = {
                computeFee: async () => sats
            };
        }
        const fee = await modelOrFee.computeFee(this);
        const change = this.calculateChange(fee);
        if (change <= 0) {
            this.outputs = this.outputs.filter((output) => output.change !== true);
            return;
        }
        this.distributeChange(change, changeDistribution);
    }
    calculateChange(fee) {
        let change = 0;
        for (const input of this.inputs) {
            if (typeof input.sourceTransaction !== 'object') {
                throw new TypeError('Source transactions are required for all inputs during fee computation');
            }
            change +=
                input.sourceTransaction.outputs[input.sourceOutputIndex].satoshis ?? 0;
        }
        change -= fee;
        for (const out of this.outputs) {
            if (out.change !== true) {
                if (out.satoshis !== undefined) {
                    change -= out.satoshis;
                }
            }
        }
        return change;
    }
    distributeChange(change, changeDistribution) {
        let distributedChange = 0;
        const changeOutputs = this.outputs.filter((out) => out.change);
        if (changeDistribution === 'random') {
            distributedChange = this.distributeRandomChange(change, changeOutputs);
        }
        else if (changeDistribution === 'equal') {
            distributedChange = this.distributeEqualChange(change, changeOutputs);
        }
        if (distributedChange < change) {
            const lastOutput = this.outputs.at(-1);
            if (lastOutput.satoshis === undefined) {
                lastOutput.satoshis = change - distributedChange;
            }
            else {
                lastOutput.satoshis += change - distributedChange;
            }
        }
    }
    distributeRandomChange(change, changeOutputs) {
        let distributedChange = 0;
        let changeToUse = change;
        const benfordNumbers = new Array(changeOutputs.length).fill(1);
        changeToUse -= changeOutputs.length;
        distributedChange += changeOutputs.length;
        for (let i = 0; i < changeOutputs.length - 1; i++) {
            const portion = this.benfordNumber(0, changeToUse);
            benfordNumbers[i] = benfordNumbers[i] + portion;
            distributedChange += portion;
            changeToUse -= portion;
        }
        for (const output of this.outputs) {
            if (output.change === true)
                output.satoshis = benfordNumbers.shift();
        }
        return distributedChange;
    }
    distributeEqualChange(change, changeOutputs) {
        let distributedChange = 0;
        const perOutput = Math.floor(change / changeOutputs.length);
        for (const out of changeOutputs) {
            distributedChange += perOutput;
            out.satoshis = perOutput;
        }
        return distributedChange;
    }
    benfordNumber(min, max) {
        const d = Random(1)[0] % 9 + 1;
        return Math.floor(min + ((max - min) * Math.log10(1 + 1 / d)) / Math.log10(10));
    }
    /**
     * Utility method that returns the current fee based on inputs and outputs
     *
     * @returns The current transaction fee
     */
    getFee() {
        let totalIn = 0;
        for (const input of this.inputs) {
            if (typeof input.sourceTransaction !== 'object') {
                throw new TypeError('Source transactions or sourceSatoshis are required for all inputs to calculate fee');
            }
            totalIn +=
                input.sourceTransaction.outputs[input.sourceOutputIndex].satoshis ?? 0;
        }
        let totalOut = 0;
        for (const output of this.outputs) {
            totalOut += output.satoshis ?? 0;
        }
        return totalIn - totalOut;
    }
    /**
     * Signs a transaction, hydrating all its unlocking scripts based on the provided script templates where they are available.
     */
    async sign() {
        this.invalidateSerializationCaches();
        for (const out of this.outputs) {
            if (out.satoshis === undefined) {
                if (out.change === true) {
                    throw new Error('There are still change outputs with uncomputed amounts. Use the fee() method to compute the change amounts and transaction fees prior to signing.');
                }
                else {
                    throw new Error('One or more transaction outputs is missing an amount. Ensure all output amounts are provided before signing.');
                }
            }
        }
        const unlockingScripts = await Promise.all(this.inputs.map(async (x, i) => {
            if (typeof this.inputs[i].unlockingScriptTemplate === 'object') {
                return await this.inputs[i]?.unlockingScriptTemplate?.sign(this, i);
            }
            else {
                return await Promise.resolve(undefined);
            }
        }));
        for (let i = 0, l = this.inputs.length; i < l; i++) {
            if (typeof this.inputs[i].unlockingScriptTemplate === 'object') {
                this.inputs[i].unlockingScript = unlockingScripts[i];
            }
        }
    }
    /**
     * Broadcasts a transaction.
     *
     * @param broadcaster The Broadcaster instance wwhere the transaction will be sent
     * @returns A BroadcastResponse or BroadcastFailure from the Broadcaster
     */
    async broadcast(broadcaster = defaultBroadcaster()) {
        return await broadcaster.broadcast(this);
    }
    writeTransactionBody(writer) {
        writer.writeUInt32LE(this.version);
        writer.writeVarIntNum(this.inputs.length);
        for (const i of this.inputs) {
            if (i.sourceTXID === undefined) {
                if (i.sourceTransaction == null) {
                    throw new Error('sourceTransaction is undefined');
                }
                else {
                    writer.write(i.sourceTransaction.hash());
                }
            }
            else {
                writer.writeReverse(toArray(i.sourceTXID, 'hex'));
            }
            writer.writeUInt32LE(i.sourceOutputIndex);
            if (i.unlockingScript == null) {
                throw new Error('unlockingScript is undefined');
            }
            const scriptBin = i.unlockingScript.toUint8Array();
            writer.writeVarIntNum(scriptBin.length);
            writer.write(scriptBin);
            writer.writeUInt32LE(i.sequence ?? 0xffffffff);
        }
        writer.writeVarIntNum(this.outputs.length);
        for (const o of this.outputs) {
            writer.writeUInt64LE(o.satoshis ?? 0);
            const scriptBin = o.lockingScript.toUint8Array();
            writer.writeVarIntNum(scriptBin.length);
            writer.write(scriptBin);
        }
        writer.writeUInt32LE(this.lockTime);
    }
    buildSerializedBytes() {
        const writer = new WriterUint8Array();
        this.writeTransactionBody(writer);
        return writer.toUint8Array();
    }
    getSerializedBytes() {
        this.rawBytesCache ??= this.buildSerializedBytes();
        return this.rawBytesCache;
    }
    /**
     * Converts the transaction to a binary array format.
     *
     * @returns {number[]} - The binary array representation of the transaction.
     */
    toBinary() {
        return Array.from(this.getSerializedBytes());
    }
    toUint8Array() {
        return this.getSerializedBytes();
    }
    writeEF(writer) {
        writer.writeUInt32LE(this.version);
        writer.write([0, 0, 0, 0, 0, 0xef]);
        writer.writeVarIntNum(this.inputs.length);
        for (const i of this.inputs) {
            if (i.sourceTransaction === undefined) {
                throw new TypeError('All inputs must have source transactions when serializing to EF format');
            }
            if (i.sourceTXID === undefined) {
                writer.write(i.sourceTransaction.hash());
            }
            else {
                writer.write(toArray(i.sourceTXID, 'hex').reverse());
            }
            writer.writeUInt32LE(i.sourceOutputIndex);
            if (i.unlockingScript == null) {
                throw new Error('unlockingScript is undefined');
            }
            const scriptBin = i.unlockingScript.toBinary();
            writer.writeVarIntNum(scriptBin.length);
            writer.write(scriptBin);
            writer.writeUInt32LE(i.sequence ?? 0xffffffff); // default to max sequence
            writer.writeUInt64LE(i.sourceTransaction.outputs[i.sourceOutputIndex].satoshis ?? 0);
            const lockingScriptBin = i.sourceTransaction.outputs[i.sourceOutputIndex].lockingScript.toBinary();
            writer.writeVarIntNum(lockingScriptBin.length);
            writer.write(lockingScriptBin);
        }
        writer.writeVarIntNum(this.outputs.length);
        for (const o of this.outputs) {
            writer.writeUInt64LE(o.satoshis ?? 0);
            const scriptBin = o.lockingScript.toBinary();
            writer.writeVarIntNum(scriptBin.length);
            writer.write(scriptBin);
        }
        writer.writeUInt32LE(this.lockTime);
    }
    /**
     * Converts the transaction to a BRC-30 EF format.
     *
     * @returns {number[]} - The BRC-30 EF representation of the transaction.
     */
    toEF() {
        const writer = new Writer();
        this.writeEF(writer);
        return writer.toArray();
    }
    /**
     * Converts the transaction to a BRC-30 EF format.
     *
     * @returns {Uint8Array} - The BRC-30 EF representation of the transaction.
     */
    toEFUint8Array() {
        const writer = new WriterUint8Array();
        this.writeEF(writer);
        return writer.toUint8Array();
    }
    /**
     * Converts the transaction to a hexadecimal string EF.
     *
     * @returns {string} - The hexadecimal string representation of the transaction EF.
     */
    toHexEF() {
        return toHex(this.toEFUint8Array());
    }
    /**
     * Converts the transaction to a hexadecimal string format.
     *
     * @returns {string} - The hexadecimal string representation of the transaction.
     */
    toHex() {
        if (this.hexCache != null) {
            return this.hexCache;
        }
        const bytes = this.getSerializedBytes();
        const hex = toHex(bytes);
        this.hexCache = hex;
        return hex;
    }
    /**
     * Converts the transaction to a hexadecimal string BEEF.
     *
     * @returns {string} - The hexadecimal string representation of the transaction BEEF.
     */
    toHexBEEF() {
        return toHex(this.toBEEF());
    }
    /**
     * Converts the transaction to a hexadecimal string Atomic BEEF.
     *
     * @returns {string} - The hexadecimal string representation of the transaction Atomic BEEF.
     */
    toHexAtomicBEEF() {
        return toHex(this.toAtomicBEEF());
    }
    /**
     * Calculates the transaction's hash.
     *
     * @param {'hex' | undefined} enc - The encoding to use for the hash. If 'hex', returns a hexadecimal string; otherwise returns a binary array.
     * @returns {string | number[]} - The hash of the transaction in the specified format.
     */
    hash(enc) {
        this.cachedHash ??= hash256(this.getSerializedBytes());
        if (enc === 'hex') {
            return toHex(this.cachedHash);
        }
        return this.cachedHash;
    }
    /**
     * Calculates the transaction's ID.
     *
     * @param {'hex' | undefined} enc - The encoding to use for the ID. If 'hex', returns a hexadecimal string; otherwise returns a binary array.
     * @returns {string | number[]} - The ID of the transaction in the specified format.
     */
    id(enc) {
        const id = [...this.hash()];
        id.reverse();
        if (enc === 'hex') {
            return toHex(id);
        }
        return id;
    }
    /**
     * Verifies the legitimacy of the Bitcoin transaction according to the rules of SPV by ensuring all the input transactions link back to valid block headers, the chain of spends for all inputs are valid, and the sum of inputs is not less than the sum of outputs.
     *
     * @param chainTracker - An instance of ChainTracker, a Bitcoin block header tracker. If the value is set to 'scripts only', headers will not be verified. If not provided then the default chain tracker will be used.
     * @param feeModel - An instance of FeeModel, a fee model to use for fee calculation. If not provided then the default fee model will be used.
     * @param memoryLimit - The maximum memory in bytes usage allowed for script evaluation. If not provided then the default memory limit will be used.
     *
     * @returns Whether the transaction is valid according to the rules of SPV.
     *
     * @example tx.verify(new WhatsOnChain(), LivePolicy.getInstance())
     */
    async verify(chainTracker = defaultChainTracker(), feeModel, memoryLimit) {
        const verifiedTxids = new Set();
        const txQueue = [this];
        while (txQueue.length > 0) {
            const tx = txQueue.shift();
            const txid = tx?.id('hex') ?? '';
            if (txid != null && txid !== '' && verifiedTxids.has(txid)) {
                continue;
            }
            // If the transaction has a valid merkle path, verification is complete.
            if (typeof tx?.merklePath === 'object') {
                if (chainTracker === 'scripts only') {
                    if (txid != null) {
                        verifiedTxids.add(txid);
                    }
                    continue;
                }
                else {
                    const proofValid = await tx.merklePath.verify(txid, chainTracker);
                    // If the proof is valid, no need to verify inputs.
                    if (proofValid) {
                        verifiedTxids.add(txid);
                        continue;
                    }
                    else {
                        throw new Error(`Invalid merkle path for transaction ${txid}`);
                    }
                }
            }
            // Verify fee if feeModel is provided
            if (feeModel !== undefined) {
                if (tx === undefined) {
                    throw new Error('Transaction is undefined');
                }
                const cpTx = Transaction.fromEF(tx.toEF());
                delete cpTx.outputs[0].satoshis;
                cpTx.outputs[0].change = true;
                await cpTx.fee(feeModel);
                if (tx.getFee() < cpTx.getFee()) {
                    throw new Error(`Verification failed because the transaction ${txid} has an insufficient fee and has not been mined.`);
                }
            }
            // Verify each input transaction and evaluate the spend events.
            // Also, keep a total of the input amounts for later.
            let inputTotal = 0;
            if (tx === undefined) {
                throw new Error('Transaction is undefined');
            }
            for (let i = 0; i < tx.inputs.length; i++) {
                const input = tx.inputs[i];
                if (typeof input.sourceTransaction !== 'object') {
                    throw new TypeError(`Verification failed because the input at index ${i} of transaction ${txid} is missing an associated source transaction. This source transaction is required for transaction verification because there is no merkle proof for the transaction spending a UTXO it contains.`);
                }
                if (typeof input.unlockingScript !== 'object') {
                    throw new TypeError(`Verification failed because the input at index ${i} of transaction ${txid} is missing an associated unlocking script. This script is required for transaction verification because there is no merkle proof for the transaction spending the UTXO.`);
                }
                const sourceOutput = input.sourceTransaction.outputs[input.sourceOutputIndex];
                inputTotal += sourceOutput.satoshis ?? 0;
                const sourceTxid = input.sourceTransaction.id('hex');
                if (!verifiedTxids.has(sourceTxid)) {
                    txQueue.push(input.sourceTransaction);
                }
                const otherInputs = tx.inputs.filter((_, idx) => idx !== i);
                input.sourceTXID ??= sourceTxid;
                const spend = new Spend({
                    sourceTXID: input.sourceTXID,
                    sourceOutputIndex: input.sourceOutputIndex,
                    lockingScript: sourceOutput.lockingScript,
                    sourceSatoshis: sourceOutput.satoshis ?? 0,
                    transactionVersion: tx.version,
                    otherInputs,
                    unlockingScript: input.unlockingScript,
                    inputSequence: input.sequence ?? 0xffffffff, // default to max sequence
                    inputIndex: i,
                    outputs: tx.outputs,
                    lockTime: tx.lockTime,
                    memoryLimit
                });
                const spendValid = spend.validate();
                if (!spendValid) {
                    return false;
                }
            }
            // Total the outputs to ensure they don't amount to more than the inputs
            let outputTotal = 0;
            for (const out of tx.outputs) {
                if (typeof out.satoshis !== 'number') {
                    throw new TypeError('Every output must have a defined amount during transaction verification.');
                }
                outputTotal += out.satoshis;
            }
            if (outputTotal > inputTotal) {
                return false;
            }
            verifiedTxids.add(txid);
        }
        return true;
    }
    /**
     * Serializes this transaction, together with its inputs and the respective merkle proofs, into the BEEF (BRC-62) format. This enables efficient verification of its compliance with the rules of SPV.
     *
     * @param writer The writer to serialize to
     * @param allowPartial If true, error will not be thrown if there are any missing sourceTransactions.
     *
     * @returns The serialized BEEF structure
     * @throws Error if there are any missing sourceTransactions unless `allowPartial` is true.
     */
    writeSerializedBEEF(writer, allowPartial) {
        writer.writeUInt32LE(BEEF_V1);
        const BUMPs = [];
        const bumpIndexByInstance = new Map();
        const bumpIndexByRoot = new Map();
        const txs = [];
        const seenTxids = new Set();
        const getBumpIndex = (merklePath) => {
            const existingByInstance = bumpIndexByInstance.get(merklePath);
            if (existingByInstance !== undefined) {
                return existingByInstance;
            }
            const key = `${merklePath.blockHeight}:${merklePath.computeRoot()}`;
            const existingByRoot = bumpIndexByRoot.get(key);
            if (existingByRoot !== undefined) {
                BUMPs[existingByRoot].combine(merklePath);
                bumpIndexByInstance.set(merklePath, existingByRoot);
                return existingByRoot;
            }
            const newIndex = BUMPs.length;
            BUMPs.push(merklePath);
            bumpIndexByInstance.set(merklePath, newIndex);
            bumpIndexByRoot.set(key, newIndex);
            return newIndex;
        };
        // Recursive function to add paths and input transactions for a TX
        const addPathsAndInputs = (tx) => {
            const txid = tx.id('hex');
            if (seenTxids.has(txid)) {
                return;
            }
            const obj = { tx };
            const merklePath = tx.merklePath;
            const hasProof = typeof merklePath === 'object';
            if (hasProof && merklePath != null) {
                obj.pathIndex = getBumpIndex(merklePath);
            }
            if (!hasProof) {
                for (let i = tx.inputs.length - 1; i >= 0; i--) {
                    const input = tx.inputs[i];
                    if (typeof input.sourceTransaction === 'object') {
                        addPathsAndInputs(input.sourceTransaction);
                    }
                    else if (allowPartial === false) {
                        throw new Error('A required source transaction is missing!');
                    }
                }
            }
            seenTxids.add(txid);
            txs.push(obj);
        };
        addPathsAndInputs(this);
        writer.writeVarIntNum(BUMPs.length);
        for (const b of BUMPs) {
            writer.write(b.toBinary());
        }
        writer.writeVarIntNum(txs.length);
        for (const t of txs) {
            writer.write(t.tx.toBinary());
            if (typeof t.pathIndex === 'number') {
                writer.writeUInt8(1);
                writer.writeVarIntNum(t.pathIndex);
            }
            else {
                writer.writeUInt8(0);
            }
        }
        return writer.toArray();
    }
    /**
     * Serializes this transaction, together with its inputs and the respective merkle proofs, into the BEEF (BRC-62) format. This enables efficient verification of its compliance with the rules of SPV.
     *
     * @param allowPartial If true, error will not be thrown if there are any missing sourceTransactions.
     *
     * @returns {number[]} The serialized BEEF structure
     * @throws Error if there are any missing sourceTransactions unless `allowPartial` is true.
     */
    toBEEF(allowPartial) {
        const writer = new Writer();
        this.writeSerializedBEEF(writer, allowPartial);
        return writer.toArray();
    }
    /**
     * Serializes this transaction, together with its inputs and the respective merkle proofs, into the BEEF (BRC-62) format. This enables efficient verification of its compliance with the rules of SPV.
     *
     * @param allowPartial If true, error will not be thrown if there are any missing sourceTransactions.
     *
     * @returns {number[]} The serialized BEEF structure
     * @throws Error if there are any missing sourceTransactions unless `allowPartial` is true.
     */
    toBEEFUint8Array(allowPartial) {
        const writer = new WriterUint8Array();
        this.writeSerializedBEEF(writer, allowPartial);
        return writer.toArray();
    }
    /**
     * Serializes this transaction and its inputs into the Atomic BEEF (BRC-95) format.
     * The Atomic BEEF format starts with a 4-byte prefix `0x01010101`, followed by the TXID of the subject transaction,
     * and then the BEEF data containing only the subject transaction and its dependencies.
     * This format ensures that the BEEF structure is atomic and contains no unrelated transactions.
     *
     * @param allowPartial If true, error will not be thrown if there are any missing sourceTransactions.
     *
     * @returns {number[]} - The serialized Atomic BEEF structure.
     * @throws Error if there are any missing sourceTransactions unless `allowPartial` is true.
     */
    toAtomicBEEF(allowPartial) {
        const prefix = [1, 1, 1, 1];
        const txHash = this.hash();
        const beefData = this.toBEEF(allowPartial);
        return prefix.concat(txHash, beefData);
    }
    /**
     * Serializes this transaction and its inputs into the Atomic BEEF (BRC-95) format.
     * The Atomic BEEF format starts with a 4-byte prefix `0x01010101`, followed by the TXID of the subject transaction,
     * and then the BEEF data containing only the subject transaction and its dependencies.
     * This format ensures that the BEEF structure is atomic and contains no unrelated transactions.
     *
     * @param allowPartial If true, error will not be thrown if there are any missing sourceTransactions.
     *
     * @returns {number[]} - The serialized Atomic BEEF structure.
     * @throws Error if there are any missing sourceTransactions unless `allowPartial` is true.
     */
    toAtomicBEEFUint8Array(allowPartial) {
        const writer = new WriterUint8Array();
        const prefix = [1, 1, 1, 1];
        writer.write(prefix);
        const txHash = this.hash();
        writer.write(txHash);
        this.writeSerializedBEEF(writer, allowPartial);
        return writer.toUint8Array();
    }
    /**
     * Completes the transaction using a wallet interface, which will handle
     * signing and transaction finalization. This method converts the current
     * transaction into a format that can be processed by the wallet, and then
     * updates this transaction object with the result from the wallet.
     *
     * @param {WalletInterface} wallet - The BRC-100 compliant wallet to use for completing the transaction
     * @param {string} [actionDescription] - Optional description for the action
     * @param {string} [originator] - Optional originator domain name
     * @param {CreateActionOptions} [options] - Optional settings for transaction creation (e.g., acceptDelayedBroadcast, trustSelf, noSend, etc.)
     * @returns {Promise<void>}
     */
    async completeWithWallet(wallet, actionDescription, originator, options) {
        const inputCount = this.inputs.length;
        const outputCount = this.outputs.length;
        const description = actionDescription ?? `Transaction with ${inputCount} input(s) and ${outputCount} output(s)`;
        const actionArgs = {
            description,
            inputs: [],
            outputs: [],
            lockTime: this.lockTime,
            version: this.version
        };
        // Check if any input has an unlocking script template
        const hasTemplates = this.inputs.some(input => input.unlockingScriptTemplate != null);
        // Process inputs - collect all source transactions and convert them to BEEF
        const beefData = new Beef();
        for (let i = 0; i < this.inputs.length; i++) {
            const input = this.inputs[i];
            if (input.sourceTransaction == null) {
                throw new Error('All inputs must have a sourceTransaction when using completeWithWallet');
            }
            // Merge source transaction into BEEF
            const sourceBEEF = input.sourceTransaction.toBEEF();
            beefData.mergeBeef(sourceBEEF);
            const sourceTXID = input.sourceTransaction.id('hex');
            const inputArg = {
                outpoint: `${sourceTXID}.${input.sourceOutputIndex}`,
                inputDescription: 'Input from source transaction',
                sequenceNumber: input.sequence
            };
            // Handle inputs with templates vs scripts
            if (hasTemplates) {
                // When using signAction flow, need to provide length for templates
                if (input.unlockingScriptTemplate != null) {
                    const estimatedLength = await input.unlockingScriptTemplate.estimateLength(this, i);
                    inputArg.unlockingScriptLength = estimatedLength;
                }
                else if (input.unlockingScript != null) {
                    // Still provide the script if it exists
                    inputArg.unlockingScript = input.unlockingScript.toHex();
                }
                else {
                    throw new Error(`Input ${i} must have either an unlockingScript or unlockingScriptTemplate`);
                }
            }
            else {
                // Original flow: all inputs must have unlocking scripts
                if (input.unlockingScript == null) {
                    throw new Error('All inputs must have an unlockingScript when using completeWithWallet');
                }
                inputArg.unlockingScript = input.unlockingScript.toHex();
            }
            actionArgs.inputs.push(inputArg);
        }
        // Add inputBEEF if there are inputs
        if (this.inputs.length > 0) {
            actionArgs.inputBEEF = beefData.toBinary();
        }
        // Process outputs
        for (const output of this.outputs) {
            actionArgs.outputs.push({
                satoshis: output.satoshis,
                lockingScript: output.lockingScript.toHex(),
                outputDescription: 'Output from source transaction'
            });
        }
        // Add any labels from metadata if they exist
        if (this.metadata?.labels != null && Array.isArray(this.metadata.labels)) {
            actionArgs.labels = this.metadata.labels;
        }
        let atomicBEEF;
        // Use signAction flow for templates
        if (hasTemplates) {
            // Merge user options with required signAndProcess: false for template flow
            actionArgs.options = {
                ...options,
                signAndProcess: false
            };
            const { signableTransaction } = await wallet.createAction(actionArgs, originator);
            if (signableTransaction == null) {
                throw new Error('Wallet createAction did not return signableTransaction');
            }
            // Parse the signable transaction BEEF to get the unsigned transaction
            const partialTx = Transaction.fromBEEF(signableTransaction.tx);
            // Sign inputs with templates and collect all unlocking scripts
            const spends = {};
            for (let i = 0; i < this.inputs.length; i++) {
                const input = this.inputs[i];
                if (input.unlockingScriptTemplate != null) {
                    // Use the template to sign this input
                    const unlockingScript = await input.unlockingScriptTemplate.sign(partialTx, i);
                    spends[i] = {
                        unlockingScript: unlockingScript.toHex()
                    };
                }
                else if (input.unlockingScript != null) {
                    // Include pre-existing unlocking scripts
                    spends[i] = {
                        unlockingScript: input.unlockingScript.toHex()
                    };
                }
            }
            // Extract options that apply to signAction (subset of CreateActionOptions)
            const signActionOptions = options == null
                ? undefined
                : {
                    acceptDelayedBroadcast: options.acceptDelayedBroadcast,
                    returnTXIDOnly: options.returnTXIDOnly,
                    noSend: options.noSend,
                    sendWith: options.sendWith
                };
            // Call signAction with the generated unlocking scripts
            const signResult = await wallet.signAction({
                reference: signableTransaction.reference,
                spends,
                options: signActionOptions
            }, originator);
            if (signResult.tx == null) {
                throw new Error('Wallet signAction did not return transaction data');
            }
            atomicBEEF = signResult.tx;
        }
        else {
            // Pass through user options for standard flow
            if (options != null) {
                actionArgs.options = options;
            }
            const { tx } = await wallet.createAction(actionArgs, originator);
            if (tx == null) {
                throw new Error('Wallet createAction did not return transaction data');
            }
            atomicBEEF = tx;
        }
        // Create a new transaction from the atomic BEEF
        const newTransaction = Transaction.fromAtomicBEEF(atomicBEEF);
        // Update this transaction's properties with the new transaction's properties
        this.version = newTransaction.version;
        this.inputs = newTransaction.inputs;
        this.outputs = newTransaction.outputs;
        this.lockTime = newTransaction.lockTime;
        this.merklePath = newTransaction.merklePath;
        this.cachedHash = newTransaction.cachedHash;
        // Preserve metadata from the original transaction but update with any new metadata
        this.metadata = {
            ...this.metadata,
            ...newTransaction.metadata
        };
    }
    /**
     * Returns the formatted preimage of a transaction for the requested input index, signature scope (default SIGHASH_FORKID | SIGHASH_ALL), and optional subscript.
     * @param inputIndex - The index of the input to generate the preimage for
     * @param signatureScope - The signature scope to use for the preimage
     * @param subscript - The subscript to use for the preimage (optional)
     * @returns The formatted preimage
     */
    preimage(inputIndex, signatureScope, subscript) {
        inputIndex ??= 0;
        signatureScope ??= TransactionSignature.SIGHASH_FORKID | TransactionSignature.SIGHASH_ALL;
        if (inputIndex < 0 || inputIndex >= this.inputs.length) {
            throw new Error('Invalid input index');
        }
        const flags = signatureScope & 0xf0;
        if (flags !== 224 && flags !== 192 && flags !== 64) {
            throw new Error('FORKID must be set');
        }
        const coverage = signatureScope & 0x0f;
        if (coverage < 1 || coverage > 3) {
            throw new Error('Invalid signature coverage, must be all, none or single');
        }
        const input = this.inputs[inputIndex];
        if (input.sourceTransaction == null) {
            throw new Error('Source transaction is required');
        }
        const output = input.sourceTransaction.outputs[input.sourceOutputIndex];
        if (output == null) {
            throw new Error(`Source transaction's output at index ${input.sourceOutputIndex} is required`);
        }
        const otherInputs = this.inputs.filter((_, index) => index !== inputIndex);
        return TransactionSignature.format({
            sourceTXID: input.sourceTXID ?? input.sourceTransaction.id('hex'),
            sourceOutputIndex: input.sourceOutputIndex,
            sourceSatoshis: output.satoshis,
            transactionVersion: this.version,
            otherInputs,
            inputIndex,
            outputs: this.outputs,
            inputSequence: input.sequence ?? 0xffffffff,
            subscript: subscript ?? output.lockingScript,
            lockTime: this.lockTime,
            scope: signatureScope
        });
    }
}
//# sourceMappingURL=Transaction.js.map