"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionQueue = void 0;
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const bluebird_1 = __importDefault(require("bluebird"));
const events_1 = require("events");
const lodash_1 = require("lodash");
const internal_1 = require("../internal");
const queries_1 = require("../middleware/queries");
const types_1 = require("../types");
const internal_2 = require("../utils/internal");
/**
 * @hidden
 */
var Event;
(function (Event) {
    Event["StatusChange"] = "StatusChange";
    Event["TransactionStatusChange"] = "TransactionStatusChange";
    Event["ProcessedByMiddleware"] = "ProcessedByMiddleware";
})(Event || (Event = {}));
/**
 * Class to manage procedural transaction queues
 */
class TransactionQueue {
    /**
     * Create a transaction queue
     *
     * @param args.transactions - list of transactions to be run in this queue
     * @param args.procedureResult - value that will be returned by the queue after it is run. It can be a {@link PostTransactionValue}
     * @param args.transformer - function that transforms the procedure's return value before returning it after the queue is run
     */
    constructor(args, context) {
        /**
         * status of the queue
         */
        this.status = types_1.TransactionQueueStatus.Idle;
        /**
         * @hidden
         * internal queue of transactions to be run
         */
        this.queue = [];
        const { transactions, procedureResult, transformer = (val) => __awaiter(this, void 0, void 0, function* () { return val; }), } = args;
        this.emitter = new events_1.EventEmitter();
        this.procedureResult = procedureResult;
        this.hasRun = false;
        this.context = context;
        this.transactions = [];
        this.transformer = transformer;
        transactions.forEach(transaction => {
            transaction.onStatusChange(updatedTransaction => {
                this.emitter.emit(Event.TransactionStatusChange, updatedTransaction, this);
            });
            this.transactions.push(transaction);
        });
    }
    /**
     * Run the transactions in the queue in sequential order. If a transaction fails or the user refuses to sign it, one of two things can happen:
     *
     * 1) If `transaction.isCritical === true`, the entire queue fails and the corresponding error is stored in `this.error` as well as thrown
     * 2) Otherwise, the queue continues executing and the error is stored in `transaction.error`
     */
    run() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.hasRun) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.General,
                    message: 'Cannot re-run a Transaction Queue',
                });
            }
            yield this.assertFeesCovered();
            this.queue = [...this.transactions];
            this.updateStatus(types_1.TransactionQueueStatus.Running);
            let procRes;
            let res;
            try {
                yield this.executeTransactionQueue();
                this.updateStatus(types_1.TransactionQueueStatus.Succeeded);
                const { procedureResult } = this;
                if (procedureResult instanceof internal_1.PostTransactionValue) {
                    procRes = procedureResult.value;
                }
                else {
                    procRes = procedureResult;
                }
                res = yield this.transformer(procRes);
            }
            catch (err) {
                this.error = err;
                this.updateStatus(types_1.TransactionQueueStatus.Failed);
                throw err;
            }
            finally {
                this.hasRun = true;
                this.emitWhenMiddlewareIsSynced();
            }
            return res;
        });
    }
    /**
     * Retrieve a lower bound of the fees required to execute this transaction queue.
     *   Transaction fees can be higher at execution time for three reasons:
     *
     * - One or more transaction arguments depend on the result of another transaction in the queue.
     *   This means fees can't be calculated for said transaction until previous transactions in the queue have run
     * - Protocol or gas fees may vary between when this value is fetched and when the transaction is actually executed because of a
     *   governance vote
     *
     * Transaction fees are broken down between those that have to be paid by the signing Account and
     *   those that will be paid by third parties. In most cases, the entirety of the fees will be paid by
     *   either the signing Account or a **single** third party Account
     */
    getMinFees() {
        return __awaiter(this, void 0, void 0, function* () {
            const _a = yield this._getMinFees(), { 
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            subsidizedTransactions } = _a, rest = __rest(_a, ["subsidizedTransactions"]);
            return rest;
        });
    }
    /**
     * @hidden
     *
     * Return a breakdown of the queue's fees and which transactions are being subsidized
     */
    _getMinFees() {
        return __awaiter(this, void 0, void 0, function* () {
            const { context, transactions } = this;
            // get the fees and paying Account for each transaction in the queue
            const allFees = yield bluebird_1.default.map(transactions, (transaction) => __awaiter(this, void 0, void 0, function* () {
                const [payingAccount, fees] = yield Promise.all([
                    transaction.getPayingAccount(),
                    transaction.getFees(),
                ]);
                if (!fees) {
                    return null;
                }
                if (payingAccount) {
                    return {
                        fees,
                        payingAccount,
                    };
                }
                return {
                    fees,
                };
            }));
            const addFees = (a, b) => {
                return {
                    protocol: a.protocol.plus(b.protocol),
                    gas: a.gas.plus(b.gas),
                };
            };
            // Account address -> fee data (for efficiency)
            const breakdownByAccount = {};
            // fees for the signing Account
            let accountFees = {
                protocol: new bignumber_js_1.default(0),
                gas: new bignumber_js_1.default(0),
            };
            const subsidizedTransactions = [];
            // compile the fees and other data for each paying Account (and the signing Account as well)
            const eachPromise = bluebird_1.default.each(allFees, (transactionFees, index) => __awaiter(this, void 0, void 0, function* () {
                if (!transactionFees) {
                    return;
                }
                const { fees, payingAccount } = transactionFees;
                if (!payingAccount) {
                    // payingAccount === null means the signing Account has to pay
                    accountFees = addFees(accountFees, fees);
                }
                else {
                    const { account, type } = payingAccount;
                    const { address } = account;
                    const thirdPartyData = breakdownByAccount[address];
                    if (type === types_1.PayingAccountType.Subsidy) {
                        subsidizedTransactions.push(transactions[index]);
                    }
                    if (!thirdPartyData) {
                        // first time encountering this third party Account, we must populate the initial values
                        const { free: balance } = yield account.getBalance();
                        breakdownByAccount[address] = Object.assign(Object.assign({}, payingAccount), { balance,
                            fees });
                    }
                    else {
                        // already encountered the Account before, just add the fees
                        thirdPartyData.fees = addFees(thirdPartyData.fees, fees);
                    }
                }
            }));
            const { free: accountBalance } = yield context.accountBalance();
            yield eachPromise;
            const thirdPartyFees = (0, lodash_1.values)(breakdownByAccount);
            return {
                thirdPartyFees,
                accountBalance,
                accountFees,
                subsidizedTransactions,
            };
        });
    }
    /**
     * Subscribe to status changes on the Transaction Queue
     *
     * @param listener - callback function that will be called whenever the Transaction Queue's status changes
     *
     * @returns unsubscribe function
     */
    onStatusChange(listener) {
        this.emitter.on(Event.StatusChange, listener);
        return () => {
            this.emitter.removeListener(Event.StatusChange, listener);
        };
    }
    /**
     * Subscribe to status changes on individual transactions
     *
     * @param listener - callback function that will be called whenever the individual transaction's status changes
     *
     * @returns unsubscribe function
     */
    onTransactionStatusChange(listener) {
        this.emitter.on(Event.TransactionStatusChange, listener);
        return () => {
            this.emitter.removeListener(Event.TransactionStatusChange, listener);
        };
    }
    /**
     * Subscribe to the results of this queue being processed by the harvester (and as such, available to the middleware)
     *
     * @param listener - callback function that will be called whenever the middleware is updated with the latest data.
     *   If there is an error (timeout or middleware offline) it will be passed to this callback
     *
     * @note this event will be fired even if the queue fails
     * @returns unsubscribe function
     * @throws if the middleware wasn't enabled when instantiating the SDK client
     */
    onProcessedByMiddleware(listener) {
        if (!this.context.isMiddlewareEnabled()) {
            throw new internal_1.PolymeshError({
                code: types_1.ErrorCode.General,
                message: 'Cannot subscribe without an enabled middleware connection',
            });
        }
        this.emitter.on(Event.ProcessedByMiddleware, listener);
        return () => {
            this.emitter.removeListener(Event.ProcessedByMiddleware, listener);
        };
    }
    /**
     * @hidden
     */
    updateStatus(status) {
        this.status = status;
        switch (status) {
            case types_1.TransactionQueueStatus.Running:
            case types_1.TransactionQueueStatus.Succeeded: {
                this.emitter.emit(Event.StatusChange, this);
                return;
            }
            case types_1.TransactionQueueStatus.Failed: {
                this.emitter.emit(Event.StatusChange, this, this.error);
            }
        }
    }
    /**
     * @hidden
     */
    executeTransactionQueue() {
        return __awaiter(this, void 0, void 0, function* () {
            const nextTransaction = this.queue.shift();
            if (!nextTransaction) {
                return;
            }
            try {
                yield nextTransaction.run();
            }
            catch (err) {
                if (nextTransaction.isCritical) {
                    throw err;
                }
            }
            yield this.executeTransactionQueue();
        });
    }
    /**
     * Poll the middleware every 2 seconds to see if it has already processed the
     *   block that reflects the changes brought on by this queue being run. If so,
     *   emit the corresponding event. After 5 retries (or if the middleware can't be reached),
     *   the event is emitted with an error
     *
     * @note uses the middleware
     */
    emitWhenMiddlewareIsSynced() {
        return __awaiter(this, void 0, void 0, function* () {
            const { context } = this;
            if (!context.isMiddlewareEnabled()) {
                return;
            }
            const blockNumber = yield context.getLatestBlock();
            let done = false;
            bluebird_1.default.each((0, lodash_1.range)(6), (i) => __awaiter(this, void 0, void 0, function* () {
                if (done) {
                    return;
                }
                try {
                    const { data: { latestBlock: { id: processedBlock }, }, } = yield context.queryMiddleware((0, queries_1.latestProcessedBlock)());
                    if (blockNumber.lte(processedBlock)) {
                        done = true;
                        this.emitter.emit(Event.ProcessedByMiddleware);
                        return;
                    }
                }
                catch (err) { }
                if (i === 5) {
                    this.emitter.emit(Event.ProcessedByMiddleware, new internal_1.PolymeshError({ code: types_1.ErrorCode.MiddlewareError, message: 'Timed out' }));
                }
                return (0, internal_2.delay)(2000);
            }));
        });
    }
    /**
     * Check if balances and allowances (both third party and signing Account)
     *   are sufficient to cover this queue's fees
     */
    assertFeesCovered() {
        return __awaiter(this, void 0, void 0, function* () {
            const breakdown = yield this._getMinFees();
            const { accountBalance, accountFees, thirdPartyFees, subsidizedTransactions } = breakdown;
            const unsupportedTransactions = subsidizedTransactions.filter(transaction => !transaction.supportsSubsidy());
            if (unsupportedTransactions.length) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.UnmetPrerequisite,
                    message: 'Some of the transactions in the queue cannot be run with a subsidized Account',
                    data: {
                        unsupportedTransactions,
                    },
                });
            }
            const calculateTotal = ({ protocol, gas }) => protocol.plus(gas);
            const noThirdPartyAllowance = [];
            const noThirdPartyBalance = [];
            thirdPartyFees.forEach(data => {
                const { allowance, balance, fees } = data;
                const total = calculateTotal(fees);
                if (balance.lt(total)) {
                    noThirdPartyBalance.push(data);
                }
                if (allowance === null || allowance === void 0 ? void 0 : allowance.lt(total)) {
                    noThirdPartyAllowance.push(data);
                }
            });
            if (noThirdPartyAllowance.length) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.InsufficientBalance,
                    message: "Not enough POLYX third party allowance to pay for this procedure's fees",
                    data: {
                        thirdPartyFees: noThirdPartyAllowance,
                    },
                });
            }
            if (noThirdPartyBalance.length) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.InsufficientBalance,
                    message: "Not enough POLYX third party balance to pay for this procedure's fees",
                    data: {
                        thirdPartyFees: noThirdPartyBalance,
                    },
                });
            }
            const totalFees = calculateTotal(accountFees);
            if (accountBalance.lt(totalFees)) {
                throw new internal_1.PolymeshError({
                    code: types_1.ErrorCode.InsufficientBalance,
                    message: "Not enough POLYX balance to pay for this procedure's fees",
                    data: {
                        balance: accountBalance,
                        fees: accountFees,
                    },
                });
            }
        });
    }
}
exports.TransactionQueue = TransactionQueue;
//# sourceMappingURL=TransactionQueue.js.map