"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UtxoTransaction = void 0;
const genericMccTypes_1 = require("../../types/genericMccTypes");
const algoUtils_1 = require("../../utils/algoUtils");
const bech32_1 = require("../../utils/bech32");
const constants_1 = require("../../utils/constants");
const errors_1 = require("../../utils/errors");
const utils_1 = require("../../utils/utils");
const utxoUtils_1 = require("../../utils/utxoUtils");
const TransactionBase_1 = require("../TransactionBase");
// Transaction types and their description
// - coinbase        : transaction that mints new coins
// - payment         : what you get from node
// - partial_payment : transaction with some vout of vins added to additional data
// - full_payment    : transaction with vouts for all vins added to additional data
//@Managed()
class UtxoTransaction extends TransactionBase_1.TransactionBase {
    constructor(data, additionalData) {
        super(data, additionalData);
        this.reducerFunctionAdditionalDataVinOuts = (prev, vout) => { var _a; return prev.add(this.toBnValue((_a = vout === null || vout === void 0 ? void 0 : vout.vinvout) === null || _a === void 0 ? void 0 : _a.value)); };
        this.reducerFunctionPrevouts = (prev, vin) => { var _a; return prev.add(this.toBnValue((_a = vin === null || vin === void 0 ? void 0 : vin.prevout) === null || _a === void 0 ? void 0 : _a.value)); };
        this.reducerFunctionVouts = (prev, vout) => prev.add(this.toBnValue(vout.value));
        this.data.vout.forEach((vout) => {
            this.processOutput(vout);
        });
        this.synchronizeAdditionalData();
        this.assertAdditionalData();
    }
    get data() {
        return this.privateData;
    }
    get additionalData() {
        return this.privateAdditionalData;
    }
    set additionalData(data) {
        this.privateAdditionalData = data;
    }
    get txid() {
        return this.data.txid;
    }
    get stdTxid() {
        return (0, utils_1.unPrefix0x)(this.txid);
    }
    get hash() {
        return this.data.hash;
    }
    get reference() {
        const references = [];
        for (const vo of this.data.vout) {
            if (vo.scriptPubKey.hex.substring(0, 2) == (0, utils_1.unPrefix0x)((0, utils_1.toHex)(utxoUtils_1.WordToOpcode.OP_RETURN))) {
                const dataSplit = vo.scriptPubKey.asm.split(" ");
                references.push(dataSplit[1]);
            }
        }
        return references;
    }
    get stdPaymentReference() {
        let paymentReference = this.reference.length === 1 ? (0, utils_1.prefix0x)(this.reference[0]) : "";
        if (!(0, utils_1.isValidBytes32Hex)(paymentReference)) {
            paymentReference = utils_1.ZERO_BYTES_32;
        }
        return paymentReference;
    }
    get unixTimestamp() {
        return this.data.blocktime;
    }
    get sourceAddresses() {
        if (this.type === "coinbase") {
            // Coinbase transactions mint coins
            return [undefined];
        }
        if (!this.additionalData || !this.additionalData.vinouts) {
            return [undefined];
        }
        return this.additionalData.vinouts.map((mapper) => { var _a; return (_a = mapper === null || mapper === void 0 ? void 0 : mapper.vinvout) === null || _a === void 0 ? void 0 : _a.scriptPubKey.address; });
    }
    get receivingAddresses() {
        /* istanbul ignore if */
        // Transaction should always have vout array
        if (!this.data.vout) {
            return [];
        }
        return this.data.vout.map((vout) => vout.scriptPubKey.address);
    }
    toBnValue(value) {
        if (value === undefined) {
            return (0, utils_1.toBN)(0);
        }
        return (0, utils_1.toBN)(Math.round(value * constants_1.BTC_MDU).toFixed(0));
    }
    get fee() {
        if (this.type === "full_payment") {
            if (!this.additionalData || !this.additionalData.vinouts) {
                throw new errors_1.mccError(errors_1.mccErrorCode.InvalidTransaction, Error(`Transaction was not made full`));
            }
            const inSum = this.additionalData.vinouts.reduce(this.reducerFunctionAdditionalDataVinOuts, (0, utils_1.toBN)(0));
            const outSum = this.data.vout.reduce(this.reducerFunctionVouts, (0, utils_1.toBN)(0));
            return inSum.sub(outSum);
        }
        if (this.type === "coinbase") {
            // Coinbase transactions mint coins
            return (0, utils_1.toBN)(0);
        }
        throw new errors_1.mccError(errors_1.mccErrorCode.InvalidResponse, Error("fee can't be calculated for `payment` and `partial_payment` transaction types"));
    }
    get feeSignerTotalAmount() {
        throw new Error("Method not implemented.");
    }
    get spentAmounts() {
        if (this.type === "coinbase") {
            // Coinbase transactions mint coins
            return [
                {
                    amount: (0, utils_1.toBN)(0),
                },
            ];
        }
        if (!this.additionalData || !this.additionalData.vinouts) {
            return [
                {
                    amount: (0, utils_1.toBN)(0),
                },
            ];
        }
        return this.additionalData.vinouts.map((mapper) => {
            var _a, _b, _c, _d;
            let amount;
            if (mapper == undefined) {
                amount = (0, utils_1.toBN)(0);
            }
            else {
                amount = (0, utils_1.toBN)(Math.round((((_a = mapper === null || mapper === void 0 ? void 0 : mapper.vinvout) === null || _a === void 0 ? void 0 : _a.value) || 0) * constants_1.BTC_MDU).toFixed(0));
            }
            return {
                address: (_c = (_b = mapper === null || mapper === void 0 ? void 0 : mapper.vinvout) === null || _b === void 0 ? void 0 : _b.scriptPubKey) === null || _c === void 0 ? void 0 : _c.address,
                amount: amount,
                utxo: (_d = mapper === null || mapper === void 0 ? void 0 : mapper.vinvout) === null || _d === void 0 ? void 0 : _d.n,
            };
        });
    }
    get intendedSpentAmounts() {
        return this.spentAmounts;
    }
    get receivedAmounts() {
        return this.data.vout.map((vout) => {
            return {
                address: vout.scriptPubKey.address,
                amount: this.isValidPkscript(vout.n) ? this.toBnValue(vout.value) : (0, utils_1.toBN)(0),
                utxo: vout.n,
            };
        });
    }
    get intendedReceivedAmounts() {
        return this.receivedAmounts;
    }
    get type() {
        if (this.data.vin.length === 0 || this.data.vin[0].coinbase) {
            return "coinbase";
        }
        let hasUndefined = false;
        let hasDefined = false;
        if (!this.additionalData || !this.additionalData.vinouts) {
            return "payment";
        }
        for (let i = 0; i < this.additionalData.vinouts.length; i++) {
            const vinOut = this.additionalData.vinouts[i];
            if (vinOut === undefined) {
                hasUndefined = true;
            }
            else {
                hasDefined = true;
            }
        }
        if (hasDefined && !hasUndefined) {
            return "full_payment";
        }
        if (hasDefined) {
            return "partial_payment";
        }
        return "payment";
    }
    get isNativePayment() {
        // On these chains there are no other types of transactions
        return true;
    }
    get elementaryUnits() {
        return (0, utils_1.toBN)(constants_1.BTC_MDU);
    }
    get successStatus() {
        return genericMccTypes_1.TransactionSuccessStatus.SUCCESS;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async balanceDecreasingSummary({ sourceAddressIndicator, client }) {
        var _a, _b;
        // We expect sourceAddressIndicator to be utxo vin index (as hex string)
        if (!(0, utils_1.isValidBytes32Hex)(sourceAddressIndicator)) {
            return { status: TransactionBase_1.BalanceDecreasingSummaryStatus.NotValidSourceAddressFormat };
        }
        const vinIndex = parseInt(sourceAddressIndicator, 16);
        if (isNaN(vinIndex)) {
            return { status: TransactionBase_1.BalanceDecreasingSummaryStatus.NotValidSourceAddressFormat };
        }
        try {
            this.assertValidVinIndex(vinIndex);
        }
        catch (e) {
            return { status: TransactionBase_1.BalanceDecreasingSummaryStatus.InvalidInUtxo };
        }
        if (this.isValidAdditionalData()) {
            const spentAmounts = this.spentAmounts;
            const spentAmount = spentAmounts[vinIndex];
            if (spentAmount.address) {
                return {
                    status: TransactionBase_1.BalanceDecreasingSummaryStatus.Success,
                    response: {
                        blockTimestamp: this.unixTimestamp,
                        transactionHash: this.stdTxid,
                        sourceAddressIndicator: sourceAddressIndicator,
                        sourceAddressHash: (0, utils_1.standardAddressHash)(spentAmount.address),
                        sourceAddress: spentAmount.address,
                        spentAmount: spentAmount.amount,
                        transactionStatus: this.successStatus,
                        paymentReference: this.stdPaymentReference,
                        isFull: true,
                    },
                };
            }
            // Else we have to extract vin
        }
        if (!client) {
            throw new Error("Client not provided");
        }
        // TODO how to make sure client you provided is BTC client
        const vinVout = await this.extractVinVoutAt(vinIndex, client);
        if ((_b = (_a = vinVout === null || vinVout === void 0 ? void 0 : vinVout.vinvout) === null || _a === void 0 ? void 0 : _a.scriptPubKey) === null || _b === void 0 ? void 0 : _b.address) {
            return {
                status: TransactionBase_1.BalanceDecreasingSummaryStatus.Success,
                response: {
                    blockTimestamp: this.unixTimestamp,
                    transactionHash: this.stdTxid,
                    sourceAddressIndicator: sourceAddressIndicator,
                    sourceAddressHash: (0, utils_1.standardAddressHash)(vinVout.vinvout.scriptPubKey.address),
                    sourceAddress: vinVout.vinvout.scriptPubKey.address,
                    spentAmount: (0, utils_1.toBN)(Math.round((vinVout.vinvout.value || 0) * constants_1.BTC_MDU).toFixed(0)),
                    transactionStatus: this.successStatus,
                    paymentReference: this.stdPaymentReference,
                    isFull: false,
                },
            };
        }
        // We didn't find the address we are looking for
        return { status: TransactionBase_1.BalanceDecreasingSummaryStatus.NoSourceAddress };
    }
    async makeFull(client) {
        await this.makeFullPayment(client);
    }
    ///////////////////////////////
    //// Utxo specific methods ////
    ///////////////////////////////
    /**
     * Asserts whether the vin index is in valid range. If not, exception is thrown.
     * @param vinIndex vin index
     */
    assertValidVinIndex(vinIndex) {
        if (vinIndex < 0 || vinIndex >= this.sourceAddresses.length) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidParameter, Error("Invalid vin index"));
        }
    }
    /**
     * Asserts whether the vout index is in valid range. If not, exception is thrown.
     * @param voutIndex vout index
     */
    assertValidVoutIndex(voutIndex) {
        if (voutIndex < 0 || voutIndex >= this.receivingAddresses.length) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidParameter, Error("Invalid vout index"));
        }
    }
    /**
     * Asserts whether vinvouts Mapper is full mapper and only has unique and matching indices
     */
    assertAdditionalData() {
        var _a;
        if (this.additionalData) {
            if (((_a = this.additionalData.vinouts) === null || _a === void 0 ? void 0 : _a.length) !== this.data.vin.length) {
                throw new errors_1.mccError(errors_1.mccErrorCode.InvalidParameter, Error("Bad format, Additional data vinvouts and data vin length mismatch"));
            }
            this.additionalData.vinouts.forEach((vinvout, ind) => {
                if (vinvout && vinvout.index !== ind) {
                    throw new errors_1.mccError(errors_1.mccErrorCode.InvalidParameter, Error("Additional data corrupted: indices mismatch"));
                }
            });
        }
    }
    isValidAdditionalData() {
        var _a;
        if (this.additionalData) {
            if (((_a = this.additionalData.vinouts) === null || _a === void 0 ? void 0 : _a.length) !== this.data.vin.length) {
                return false;
            }
            this.additionalData.vinouts.forEach((vinvout, ind) => {
                if (vinvout && vinvout.index !== ind) {
                    return false;
                }
            });
            return true;
        }
        return false;
    }
    synchronizeAdditionalData() {
        const tempAdditionalData = this.additionalData;
        this.additionalData = {
            vinouts: new Array(this.data.vin.length).fill(undefined),
        };
        if (tempAdditionalData === null || tempAdditionalData === void 0 ? void 0 : tempAdditionalData.vinouts) {
            for (const vinvout of tempAdditionalData.vinouts) {
                if (vinvout) {
                    if (vinvout.index < 0 || vinvout.index >= this.data.vin.length || !this.additionalData || !this.additionalData.vinouts) {
                        throw new errors_1.mccError(errors_1.mccErrorCode.InvalidParameter, new Error("vinvout wrong index out of range"));
                    }
                    this.additionalData.vinouts[vinvout.index] = vinvout;
                    this.processOutput(vinvout.vinvout);
                }
            }
        }
    }
    /**
     * Extract vout on vout index. If vout index is not valid or data is corrupted, exception is thrown.
     * @param voutIndex vout index
     * @returns vout on vout index
     */
    extractVoutAt(voutIndex) {
        this.assertValidVoutIndex(voutIndex);
        const toReturn = this.data.vout[voutIndex];
        if (toReturn.n !== voutIndex) {
            throw new errors_1.mccError(errors_1.mccErrorCode.InvalidParameter, new Error(`Vin and vout transaction miss match; requested index ${voutIndex}, found index ${toReturn.n}`));
        }
        return toReturn;
    }
    /**
     *
     * @param vinIndex vin index
     * @returns vin on vin index
     */
    extractVinAt(vinIndex) {
        this.assertValidVinIndex(vinIndex);
        return this.data.vin[vinIndex];
    }
    /**
     * Gets the vout corresponding to vin in the given vin index. If the vout is not fetched, the
     * input transaction is read and the corresponding vout is obtained
     * @param vinIndex vin index
     * @param client mcc client for fetching input the transactions
     * @returns input vout of the relevant utxo of the input transaction on the vin on the index `vinIndex`
     */
    async extractVinVoutAt(vinIndex, client) {
        const vinObject = this.extractVinAt(vinIndex);
        if (!vinObject.txid) {
            return {
                index: vinIndex,
                vinvout: undefined,
            };
        }
        const connectedTrans = await client.getTransaction(vinObject.txid);
        if (connectedTrans === null) {
            return {
                index: vinIndex,
                vinvout: undefined,
            };
        }
        return {
            index: vinIndex,
            vinvout: connectedTrans.extractVoutAt(vinObject.vout || 0),
        };
    }
    /**
     * Gets the vout corresponding to vin in the given vin index. If it is not stored in the additionalData,
     * it's fetched and updated using the client.
     * @param vinIndex vin index
     * @param client mcc client to fetch transaction data
     * @returns input vout of the relevant utxo of the input transaction on the vin on the index `vinIndex`
     */
    async vinVoutAt(vinIndex, client) {
        // note: vinouts list is always initialized
        if (!this.additionalData) {
            this.additionalData = {
                vinouts: new Array(this.data.vin.length).fill(undefined),
            };
        }
        if (!this.additionalData || !this.additionalData.vinouts) {
            throw new errors_1.mccError(errors_1.mccErrorCode.NotImplemented, new Error(`Can not happen`));
        }
        let vinVout = this.additionalData.vinouts[vinIndex];
        if (vinVout) {
            return vinVout;
        }
        if (!client) {
            throw new Error("Client not provided");
        }
        vinVout = await this.extractVinVoutAt(vinIndex, client);
        this.additionalData.vinouts[vinIndex] = vinVout;
        return vinVout;
    }
    /**
     * Checks if the input on index `vinIndex` is fetched.
     * @param vinIndex vin index
     * @returns true if input on the index `vinIndex` is already fetched.
     */
    isSyncedVinIndex(vinIndex) {
        this.assertValidVinIndex(vinIndex);
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return !!this.additionalData.vinouts[vinIndex];
    }
    /**
     * Fetches all the vin data from input transactions and makes the payment full_payment.
     * @param client mcc client to use for fetching input data
     */
    async makeFullPayment(client) {
        const promises = [];
        this.synchronizeAdditionalData();
        for (let i = 0; i < this.data.vin.length; i++) {
            promises.push(this.vinVoutAt(i, client));
        }
        await Promise.all(promises);
    }
    /**
     * Doge has additional brackets around addresses in out tx
     * @param vout
     * @returns
     */
    processOutput(vout) {
        if (!vout) {
            return;
        }
        if (vout.scriptPubKey.address) {
            return;
        }
        if (vout.scriptPubKey.addresses && vout.scriptPubKey.addresses.length === 1) {
            vout.scriptPubKey.address = vout.scriptPubKey.addresses[0];
        }
        // otherwise `address` stays undefined
    }
    /**
     * Weather a output is a valid "nice" payment output currently P2PKH or P2PK
     * @param voutIndex index of the output we are checking
     * @returns weather a vout script is standard P2PKH or P2PK
     */
    isValidPaymentScript(voutIndex) {
        return this.isP2PKH(voutIndex) || this.isP2PK(voutIndex);
    }
    //// P2PKH
    /**
     * Checks if the output on index `voutIndex` is P2PKH. (Pay to public key hash)
     * @param voutIndex index of the output we are checking
     * @returns weather a vout script is standard P2PKH
     */
    isP2PKH(voutIndex) {
        const vout = this.extractVoutAt(voutIndex);
        const script_commands = vout.scriptPubKey.asm.split(" ");
        return (script_commands.length === 5 &&
            script_commands[0] === "OP_DUP" &&
            script_commands[1] === "OP_HASH160" &&
            script_commands[3] === "OP_EQUALVERIFY" &&
            script_commands[4] === "OP_CHECKSIG");
    }
    /**
     * Checks that address specified by `address` is actually the one that can redeem the script for a valid P2PKH output script
     * @param voutIndex index of the output we are checking
     * @returns weather the output script is spendable by address
     */
    isValidP2PKH(voutIndex) {
        if (!this.isP2PKH(voutIndex)) {
            return false;
        }
        const vout = this.extractVoutAt(voutIndex);
        console.dir(vout);
        const address = vout.scriptPubKey.address;
        if (!address) {
            return false;
        }
        const addressHex = (0, utils_1.btcBase58Decode)(address);
        // This is the version check for P2PKH
        if (addressHex[0] !== 0x00) {
            return false;
        }
        const xx = (0, algoUtils_1.bytesToHex)(addressHex.slice(1, 21));
        const script_commands = vout.scriptPubKey.asm.split(" ");
        const hash = script_commands[2];
        return hash === xx;
    }
    //// P2WPKH
    isP2WPKH(voutIndex) {
        const vout = this.extractVoutAt(voutIndex);
        console.dir(vout);
        const script_commands = vout.scriptPubKey.asm.split(" ");
        return script_commands.length === 2 && script_commands[0] === "0";
    }
    //this is to ok
    isValidP2WPKH(voutIndex) {
        if (!this.isP2WPKH(voutIndex)) {
            return false;
        }
        const vout = this.extractVoutAt(voutIndex);
        const address = vout.scriptPubKey.address;
        if (!address) {
            return false;
        }
        // let addressData: string;
        // testnet must start with tb1
        // if (address.startsWith("bc1")) {
        //    addressData = address.slice(3);
        // } else {
        //    return false;
        // }
        const addressHex = (0, bech32_1.bech32Decode)(address);
        console.dir(addressHex);
        // This is the version check for P2PKH
        const script_commands = vout.scriptPubKey.asm.split(" ");
        const hash = script_commands[1];
        return false;
    }
    //// P2PK
    /**
     * Checks if the output on index `voutIndex` is P2PK. (Pay to public key)
     * @param voutIndex index of the output we are checking
     * @returns weather a vout script is standard P2PK
     */
    isP2PK(voutIndex) {
        const vout = this.extractVoutAt(voutIndex);
        const script_commands = vout.scriptPubKey.asm.split(" ");
        return script_commands.length === 2 && script_commands[1] === "OP_CHECKSIG";
    }
    isValidP2PK(voutIndex) {
        if (!this.isP2PK(voutIndex)) {
            return false;
        }
        throw new Error("Not implemented");
    }
}
exports.UtxoTransaction = UtxoTransaction;
//# sourceMappingURL=UtxoTransaction.js.map