"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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PolymeshTransactionBase = void 0;
const events_1 = require("events");
const types_1 = require("../polkadot/types");
const internal_1 = require("../internal");
const types_2 = require("../types");
const conversion_1 = require("../utils/conversion");
/**
 * @hidden
 */
var Event;
(function (Event) {
    Event["StatusChange"] = "StatusChange";
})(Event || (Event = {}));
/**
 * Wrapper class for a Polymesh Transaction
 */
class PolymeshTransactionBase {
    /**
     * @hidden
     */
    constructor(transactionSpec, context) {
        /**
         * current status of the transaction
         */
        this.status = types_2.TransactionStatus.Idle;
        /**
         * @hidden
         *
         * wrappers for values that will exist after this transaction has executed
         */
        this.postValues = [];
        const { postTransactionValues, tx, signer, isCritical, fee, paidForBy } = transactionSpec;
        if (postTransactionValues) {
            this.postValues = postTransactionValues;
        }
        this.emitter = new events_1.EventEmitter();
        this.tx = tx;
        this.tag = conversion_1.transactionToTxTag(tx);
        this.signer = signer;
        this.isCritical = isCritical;
        this.protocolFee = fee;
        this.context = context;
        this.paidForBy = paidForBy;
        this.batchSize = null;
    }
    /**
     * Run the transaction and update its status
     */
    run() {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const receipt = yield this.internalRun();
                this.receipt = receipt;
                yield Promise.all(this.postValues.map(postValue => postValue.run(receipt)));
                this.updateStatus(types_2.TransactionStatus.Succeeded);
            }
            catch (err) {
                const error = err;
                this.error = err;
                switch (error.code) {
                    case types_2.ErrorCode.TransactionAborted: {
                        this.updateStatus(types_2.TransactionStatus.Aborted);
                        break;
                    }
                    case types_2.ErrorCode.TransactionRejectedByUser: {
                        this.updateStatus(types_2.TransactionStatus.Rejected);
                        break;
                    }
                    case types_2.ErrorCode.TransactionReverted:
                    case types_2.ErrorCode.FatalError:
                    default: {
                        this.updateStatus(types_2.TransactionStatus.Failed);
                        break;
                    }
                }
                throw error;
            }
        });
    }
    /**
     * @hidden
     *
     * Execute the underlying transaction, updating the status where applicable and
     *   throwing any pertinent errors
     */
    internalRun() {
        this.updateStatus(types_2.TransactionStatus.Unapproved);
        return new Promise((resolve, reject) => {
            const txWithArgs = this.composeTx();
            const gettingUnsub = txWithArgs.signAndSend(this.signer, receipt => {
                const { status } = receipt;
                let unsubscribe = false;
                if (receipt.isCompleted) {
                    /* isCompleted === isFinalized || isInBlock || isError */
                    // TODO @monitz87: replace with event object when it is auto-generated by the polkadot fork
                    const failed = receipt.findRecord('system', 'ExtrinsicFailed');
                    if (receipt.isInBlock) {
                        // tx included in a block
                        this.blockHash = status.asInBlock.toString();
                        if (failed) {
                            unsubscribe = true;
                            this.handleExtrinsicFailure(resolve, reject, failed.event.data[0]);
                        }
                    }
                    else {
                        unsubscribe = true;
                    }
                    if (unsubscribe) {
                        gettingUnsub.then(unsub => {
                            unsub();
                        });
                    }
                    if (receipt.isFinalized) {
                        // tx finalized
                        this.handleExtrinsicSuccess(resolve, reject, receipt);
                    }
                    if (receipt.isError) {
                        reject(new internal_1.PolymeshError({ code: types_2.ErrorCode.TransactionAborted }));
                    }
                }
            });
            gettingUnsub
                .then(() => {
                // tx approved by signer
                this.updateStatus(types_2.TransactionStatus.Running);
                this.txHash = txWithArgs.hash.toString();
            })
                .catch((err) => {
                let error;
                /* istanbul ignore else */
                if (err.message.indexOf('Cancelled') > -1) {
                    // tx rejected by signer
                    error = { code: types_2.ErrorCode.TransactionRejectedByUser };
                }
                else {
                    // unexpected error
                    error = { code: types_2.ErrorCode.FatalError, message: err.message };
                }
                reject(new internal_1.PolymeshError(error));
            });
        });
    }
    /**
     * Subscribe to status changes
     *
     * @param listener - callback function that will be called whenever the status changes
     *
     * @returns unsubscribe function
     */
    onStatusChange(listener) {
        this.emitter.on(Event.StatusChange, listener);
        return () => {
            this.emitter.removeListener(Event.StatusChange, listener);
        };
    }
    /**
     * Retrieve the Account that would pay for the transaction fees if it was run at this moment, as well as the maximum amount that can be
     *   charged to it. A null allowance means that there is no limit to that amount
     *
     * A null return value signifies that the current Account will pay for the fees
     *
     * @note this value might change if, before running the transaction, the current Account enters (or leaves)
     *   a subsidizer relationship
     */
    getPayingAccount() {
        return __awaiter(this, void 0, void 0, function* () {
            const { paidForBy, context, tag } = this;
            if (paidForBy) {
                const primaryKey = yield paidForBy.getPrimaryKey();
                return {
                    account: primaryKey,
                    allowance: null,
                };
            }
            const subsidy = yield context.accountSubsidy();
            // `relayer.removePayingKey` is always paid by the caller
            if (!subsidy || tag === types_1.TxTags.relayer.RemovePayingKey) {
                return null;
            }
            const { subsidizer: account, allowance } = subsidy;
            return {
                account,
                allowance,
            };
        });
    }
    /**
     * Get all (protocol and gas) fees associated with this transaction. Returns null
     * if the transaction is not ready yet (this can happen if it depends on the execution of a
     * previous transaction in the queue)
     *
     * @note this value might change if the transaction is run at a later time. This can be due to a governance vote
     */
    getFees() {
        return __awaiter(this, void 0, void 0, function* () {
            const { signer, context } = this;
            let { protocolFee } = this;
            let composedTx;
            try {
                composedTx = this.composeTx();
            }
            catch (err) {
                return null;
            }
            const paymentInfoPromise = composedTx.paymentInfo(signer);
            if (!protocolFee) {
                protocolFee = yield context.getTransactionFees(this.tag);
            }
            const { partialFee } = yield paymentInfoPromise;
            return {
                protocol: protocolFee.multipliedBy(this.batchSize || 1),
                gas: conversion_1.balanceToBigNumber(partialFee),
            };
        });
    }
    /**
     * @hidden
     */
    updateStatus(status) {
        this.status = status;
        /* eslint-disable default-case */
        switch (status) {
            case types_2.TransactionStatus.Unapproved:
            case types_2.TransactionStatus.Running:
            case types_2.TransactionStatus.Succeeded: {
                this.emitter.emit(Event.StatusChange, this);
                return;
            }
            case types_2.TransactionStatus.Rejected:
            case types_2.TransactionStatus.Aborted:
            case types_2.TransactionStatus.Failed: {
                this.emitter.emit(Event.StatusChange, this, this.error);
            }
        }
        /* eslint-enable default-case */
    }
    /**
     * @hidden
     */
    handleExtrinsicFailure(_resolve, reject, error, data) {
        // get revert message from event
        let message;
        if (error.isModule) {
            // known error
            const mod = error.asModule;
            const { section, name, docs } = mod.registry.findMetaError(mod);
            message = `${section}.${name}: ${docs.join(' ')}`;
        }
        else if (error.isBadOrigin) {
            message = 'Bad origin';
        }
        else if (error.isCannotLookup) {
            message = 'Could not lookup information required to validate the transaction';
        }
        else {
            message = 'Unknown error';
        }
        reject(new internal_1.PolymeshError({ code: types_2.ErrorCode.TransactionReverted, message, data }));
    }
    /**
     * @hidden
     */
    handleExtrinsicSuccess(resolve, _reject, receipt) {
        resolve(receipt);
    }
}
exports.PolymeshTransactionBase = PolymeshTransactionBase;
//# sourceMappingURL=PolymeshTransactionBase.js.map