"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tx = void 0;
const bufio_1 = require("@node-dlc/bufio");
const bufio_2 = require("@node-dlc/bufio");
const crypto_1 = require("@node-dlc/crypto");
const HashByteOrder_1 = require("./HashByteOrder");
const HashValue_1 = require("./HashValue");
const LockTime_1 = require("./LockTime");
const OutPoint_1 = require("./OutPoint");
const Script_1 = require("./Script");
const Sequence_1 = require("./Sequence");
const TxIn_1 = require("./TxIn");
const TxOut_1 = require("./TxOut");
const Value_1 = require("./Value");
const Witness_1 = require("./Witness");
/**
 * This class is an immutable Bitcoin transaction. This class is used
 * as a data container from parsed blocks, RPC, or other sources. To use
 * a mutable transaction, you should use `TxBuilder` class.
 */
class Tx {
    /**
     * Decodes a `Tx` stream similar to Bitcoin Core's DecodeTx method
     * in that it will first try to parse with SegWit markers enabled.
     * If there is an error (such as  with a base transaction with no
     * inputs), then it will try parsing using the legacy method.
     * @param reader
     */
    static decode(reader) {
        const data = reader.readBytes();
        try {
            return Tx.parse(bufio_1.StreamReader.fromBuffer(data), true);
        }
        catch (ex) {
            return Tx.parse(bufio_1.StreamReader.fromBuffer(data), false);
        }
    }
    /**
     * Parses a transaction from its byte format in a stream. Capable of
     * parsing both legacy and segwit transactions. This method is
     * similar to Bitcoin Core's `UnserializeTransaction` on the
     * `Transaction` type. This method is expected to throw if witness
     * is enabled and we have an ambiguous base transaction (zero inputs).
     * @param reader
     */
    static parse(reader, allowWitness) {
        // Read the version
        const version = reader.readUInt32LE();
        // Try reading inputs. If this is segwit or a base/dummy, we get
        // an empty array
        let vins = Tx.parseInputs(reader);
        let vouts;
        let flags = 0;
        // If witness is allowed and we had an empty input array we
        // will try parsing a normal witness transaction. This may throw
        // if this is a base transaction.
        if (allowWitness && vins.length === 0) {
            flags = reader.readUInt8();
            if (flags !== 0) {
                vins = Tx.parseInputs(reader);
                vouts = Tx.parseOutputs(reader);
            }
        }
        // Otherwise, we had success reading inputs and can move along
        // and parse the outputs!
        else {
            vouts = Tx.parseOutputs(reader);
        }
        // If we have witness and read a flag, then we we need to
        // process the witness for each input.
        if (allowWitness && flags & 1) {
            for (let i = 0; i < vins.length; i++) {
                const items = Number(reader.readVarInt());
                for (let item = 0; item < items; item++) {
                    vins[i].witness.push(Witness_1.Witness.parse(reader));
                }
            }
        }
        // Finally read the locktime
        const locktime = LockTime_1.LockTime.parse(reader);
        return new Tx(version, vins, vouts, locktime);
    }
    /**
     * Parses the inputs for a transaction
     * @param reader
     * @returns
     */
    static parseInputs(reader) {
        const vinLen = Number(reader.readVarInt());
        const inputs = [];
        for (let idx = 0; idx < vinLen; idx++) {
            inputs.push(new TxIn_1.TxIn(OutPoint_1.OutPoint.parse(reader), Script_1.Script.parse(reader), Sequence_1.Sequence.parse(reader)));
        }
        return inputs;
    }
    /**
     * Parses the outputs for a transaction
     * @param reader
     * @returns
     */
    static parseOutputs(reader) {
        const voutLen = Number(reader.readVarInt());
        const outputs = [];
        for (let idx = 0; idx < voutLen; idx++) {
            outputs.push(new TxOut_1.TxOut(Value_1.Value.fromSats(reader.readBigUInt64LE()), Script_1.Script.parse(reader))); // prettier-ignore
        }
        return outputs;
    }
    /**
     * Parses a transaction from a buffer that contains the fully
     * serialization transaction bytes.
     * @param buf
     */
    static fromBuffer(buf) {
        return Tx.decode(bufio_1.StreamReader.fromBuffer(buf));
    }
    /**
     * Parses a transaction from a hex string containing the fully
     * serialized transaction bytes.
     * @param hex
     */
    static fromHex(hex) {
        return Tx.decode(bufio_1.StreamReader.fromHex(hex));
    }
    /**
     * Get the transaction version. The transaction version corresponds
     * to features that are enabled for the transaction such as time
     * locks.
     */
    get version() {
        return this._version;
    }
    /**
     * Gets the transaction identifier. The `txId` for both legacy and
     * segwit transaction is the hash256 of
     * `hash256(version||inputs||ouputs||locktime)`.
     */
    get txId() {
        if (!this._txId)
            this._lazyCalc();
        return this._txId;
    }
    /**
     * Gets the transaction segwit transaction identifier. For legacy
     * transaction this is the same as the `txId` property. For segwit
     * transaction this is the hash256 of
     * `hash256(version||0x0001||inputs||outputs||witness||locktime)`.
     *
     * This is the same value as the `hash` property in bitcoind RPC
     * results.
     */
    get witnessTxId() {
        if (!this._wtxid)
            this._lazyCalc();
        return this._wtxid;
    }
    /**
     * Gets the transaction inputs.
     */
    get inputs() {
        return this._inputs;
    }
    /**
     * Gets the transaction outputs
     */
    get outputs() {
        return this._outputs;
    }
    /**
     * Gets the transaction `nLocktime` value that is used to control
     * absolute timelocks.
     */
    get locktime() {
        return this._locktime;
    }
    get isSegWit() {
        return this._inputs.some((p) => p.witness.length > 0);
    }
    get size() {
        if (!this._sizes)
            this._lazyCalc();
        return this._sizes.size;
    }
    get vsize() {
        if (!this._sizes)
            this._lazyCalc();
        return this._sizes.vsize;
    }
    get weight() {
        if (!this._sizes)
            this._lazyCalc();
        return this._sizes.weight;
    }
    constructor(version = 2, inputs = [], outputs = [], locktime = new LockTime_1.LockTime(), sizes) {
        this._version = version;
        this._inputs = inputs;
        this._outputs = outputs;
        this._locktime = locktime;
        this._sizes = sizes;
    }
    /**
     * Serializes legacy or segwit transactions into a Buffer
     */
    serialize() {
        if (this.isSegWit)
            return this._serializeSegWit();
        else
            return this._serializeLegacy();
    }
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    toJSON() {
        return {
            version: this.version,
            inputs: this.inputs.map((vin) => vin.toJSON()),
            outputs: this.outputs.map((vout) => vout.toJSON()),
            locktime: this.locktime.toJSON(),
        };
    }
    toHex(pretty = false) {
        if (!pretty)
            return this.serialize().toString('hex');
        else
            return this._prettyHex();
    }
    _prettyHex() {
        const nl = '\n';
        const pad = '    ';
        let s = '';
        s += bufio_2.Hex.uint32LE(this.version) + nl;
        if (this.isSegWit) {
            s += '0001' + nl;
        }
        s += bufio_2.Hex.varint(this.inputs.length) + nl;
        for (const vin of this.inputs) {
            s +=
                pad +
                    vin.outpoint.txid.serialize(HashByteOrder_1.HashByteOrder.Internal).toString('hex') +
                    nl;
            s += pad + bufio_2.Hex.uint32LE(vin.outpoint.outputIndex) + nl;
            s += pad + vin.scriptSig.serialize().toString('hex') + nl;
            s += pad + bufio_2.Hex.uint32LE(vin.sequence.value) + nl;
        }
        s += bufio_2.Hex.varint(this.outputs.length) + nl;
        for (const vout of this.outputs) {
            s += pad + bufio_2.Hex.uint64LE(vout.value.sats) + nl;
            s += pad + vout.scriptPubKey.serialize().toString('hex');
            s += nl;
        }
        if (this.isSegWit) {
            for (const vin of this.inputs) {
                s += bufio_2.Hex.varint(vin.witness.length) + nl;
                for (const w of vin.witness) {
                    s += pad + w.serialize().toString('hex') + nl;
                }
            }
        }
        s += bufio_2.Hex.uint32LE(this.locktime.value);
        return s;
    }
    _serializeLegacy() {
        const writer = new bufio_1.BufferWriter();
        // version
        writer.writeUInt32LE(this.version);
        // inputs
        writer.writeVarInt(this.inputs.length);
        for (const input of this.inputs) {
            writer.writeBytes(input.serialize());
        }
        // outputs
        writer.writeVarInt(this.outputs.length);
        for (const output of this.outputs) {
            writer.writeBytes(output.serialize());
        }
        // locktime
        writer.writeBytes(this.locktime.serialize());
        return writer.toBuffer();
    }
    _serializeSegWit() {
        const writer = new bufio_1.BufferWriter();
        // version
        writer.writeUInt32LE(this.version);
        // write segwit marker and version
        writer.writeBytes(Buffer.from([0x00, 0x01]));
        // inputs
        writer.writeVarInt(this.inputs.length);
        for (const input of this.inputs) {
            writer.writeBytes(input.serialize());
        }
        // outputs
        writer.writeVarInt(this.outputs.length);
        for (const output of this.outputs) {
            writer.writeBytes(output.serialize());
        }
        // witness data
        if (this.isSegWit) {
            for (const input of this.inputs) {
                writer.writeVarInt(input.witness.length);
                for (const witness of input.witness) {
                    writer.writeBytes(witness.serialize());
                }
            }
        }
        // locktime
        writer.writeBytes(this.locktime.serialize());
        return writer.toBuffer();
    }
    /**
     * Decodes the txId and hash from the Buffer.
     *
     * For non-segwit transitions, the hash value is the double-sha256 of
     * version|vins|vouts|locktime. The txid is the reverse of the hash.
     *
     * For segwit transactions, the hash value is returned as the wtxid as
     * calculated by the double-sha256 of
     *  version|0x00|0x01|inputs|outputs|witness|locktime. The txId is
     * calculate the same as legacy transactions by performing a double
     * sha256 hash of the data minus segwit data and markers.
     */
    _calcTxId() {
        const txId = (0, crypto_1.hash256)(this._serializeLegacy());
        const hash = this.isSegWit
            ? (0, crypto_1.hash256)(this._serializeSegWit())
            : Buffer.from(txId); // prettier-ignore
        return {
            txId: new HashValue_1.HashValue(txId),
            hash: new HashValue_1.HashValue(hash),
        };
    }
    /**
     * Calculates the size, virtual size, and weight properties from the
     * based on the current inputs and outputs.
     *
     * `size` is the number of raw bytes.
     * `weight` is the number of witness bytes + the number of non-witness
     *   bytes multiplied by four.
     * `vsize` is the weight divided by four.
     */
    _calcSize() {
        const hasWitness = this.isSegWit;
        let standardBytes = 0;
        let witnessBytes = 0;
        // version is 4-bytes
        standardBytes += 4;
        // witness flags are 2 bytes
        if (hasWitness) {
            witnessBytes += 2;
        }
        // number of inputs
        standardBytes += (0, bufio_1.varIntBytes)(this.inputs.length);
        // add each input
        for (const input of this.inputs) {
            // prev out hash
            standardBytes += 32;
            // prev out index
            standardBytes += 4;
            // scriptSig length
            const scriptSig = input.scriptSig.serializeCmds();
            standardBytes += (0, bufio_1.varIntBytes)(scriptSig.length);
            standardBytes += scriptSig.length;
            // sequence, 4-bytes
            standardBytes += 4;
            // input witness
            if (hasWitness) {
                // number of witness
                witnessBytes += (0, bufio_1.varIntBytes)(input.witness.length);
                // for each witness
                for (const witness of input.witness) {
                    witnessBytes += (0, bufio_1.varIntBytes)(witness.data.length);
                    witnessBytes += witness.data.length;
                }
            }
        }
        // number of outputs
        standardBytes += (0, bufio_1.varIntBytes)(this.outputs.length);
        // add each output
        for (const output of this.outputs) {
            // value
            standardBytes += 8;
            // scriptPubKey length
            const scriptPubKey = output.scriptPubKey.serializeCmds();
            standardBytes += (0, bufio_1.varIntBytes)(scriptPubKey.length);
            standardBytes += scriptPubKey.length;
        }
        // locktime
        standardBytes += 4;
        // size will be the raw length of bytes
        const size = standardBytes + witnessBytes;
        // weight is non-witness bytes * 4 + witness bytes
        const weight = standardBytes * 4 + witnessBytes;
        // virtual size is weight / 4
        // this is equivalent for non-segwit transactions
        const vsize = Math.ceil(weight / 4);
        return {
            size,
            vsize,
            weight,
        };
    }
    _lazyCalc() {
        this._sizes = this._calcSize();
        const ids = this._calcTxId();
        this._txId = ids.txId;
        this._wtxid = ids.hash;
    }
}
exports.Tx = Tx;
//# sourceMappingURL=Tx.js.map