"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PolymeshTransactionBase = void 0;
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const events_1 = require("events");
const internal_1 = require("../internal");
const types_1 = require("../types");
const conversion_1 = require("../utils/conversion");
const internal_2 = require("../utils/internal");
/**
 * @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_1.TransactionStatus.Idle;
        /**
         * @hidden
         *
         * wrappers for values that will exist after this transaction has executed
         */
        this.postValues = [];
        const { postTransactionValues, signingAddress, signer, isCritical, fee, paidForBy } = transactionSpec;
        if (postTransactionValues) {
            this.postValues = postTransactionValues;
        }
        this.emitter = new events_1.EventEmitter();
        this.signingAddress = signingAddress;
        this.signer = signer;
        this.isCritical = isCritical;
        this.protocolFee = fee;
        this.context = context;
        this.paidForBy = paidForBy;
    }
    /**
     * 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_1.TransactionStatus.Succeeded);
            }
            catch (err) {
                const error = err;
                this.error = err;
                switch (error.code) {
                    case types_1.ErrorCode.TransactionAborted: {
                        this.updateStatus(types_1.TransactionStatus.Aborted);
                        break;
                    }
                    case types_1.ErrorCode.TransactionRejectedByUser: {
                        this.updateStatus(types_1.TransactionStatus.Rejected);
                        break;
                    }
                    case types_1.ErrorCode.TransactionReverted:
                    case types_1.ErrorCode.FatalError:
                    default: {
                        this.updateStatus(types_1.TransactionStatus.Failed);
                        break;
                    }
                }
                throw error;
            }
        });
    }
    /**
     * @hidden
     *
     * Execute the underlying transaction, updating the status where applicable and
     *   throwing any pertinent errors
     */
    internalRun() {
        return __awaiter(this, void 0, void 0, function* () {
            const { signingAddress, signer } = this;
            this.updateStatus(types_1.TransactionStatus.Unapproved);
            return new Promise((resolve, reject) => {
                const txWithArgs = this.composeTx();
                let settingBlockData = Promise.resolve();
                // nonce: -1 takes pending transactions into consideration.
                // More information can be found at: https://polkadot.js.org/docs/api/cookbook/tx/#how-do-i-take-the-pending-tx-pool-into-account-in-my-nonce
                const gettingUnsub = txWithArgs.signAndSend(signingAddress, { nonce: -1, signer }, receipt => {
                    const { status } = receipt;
                    let isLastCallback = false;
                    let unsubscribing = Promise.resolve();
                    let extrinsicFailedEvent;
                    // isCompleted === isFinalized || isInBlock || isError
                    if (receipt.isCompleted) {
                        if (receipt.isInBlock) {
                            const blockHash = status.asInBlock;
                            /*
                             * this must be done to ensure that the block hash and number are set before the success event
                             *   is emitted, and at the same time. We do not resolve or reject the containing promise until this
                             *   one resolves
                             */
                            settingBlockData = (0, internal_2.defusePromise)(this.context.polymeshApi.rpc.chain.getBlock(blockHash).then(({ block }) => {
                                this.blockHash = (0, conversion_1.hashToString)(blockHash);
                                this.blockNumber = (0, conversion_1.u32ToBigNumber)(block.header.number.unwrap());
                                // we know that the index has to be set by the time the transaction is included in a block
                                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                                this.txIndex = new bignumber_js_1.default(receipt.txIndex);
                            }));
                            // if the extrinsic failed due to an on-chain error, we should handle it in a special way
                            extrinsicFailedEvent = receipt.findRecord('system', 'ExtrinsicFailed');
                            // extrinsic failed so we can unsubscribe
                            isLastCallback = !!extrinsicFailedEvent;
                        }
                        else {
                            // isFinalized || isError so we know we can unsubscribe
                            isLastCallback = true;
                        }
                        if (isLastCallback) {
                            unsubscribing = gettingUnsub.then(unsub => {
                                unsub();
                            });
                        }
                        /*
                         * Promise chain that handles all sub-promises in this pass through the signAndSend callback.
                         * Primarily for consistent error handling
                         */
                        let finishing = Promise.resolve();
                        if (extrinsicFailedEvent) {
                            const { event: { data }, } = extrinsicFailedEvent;
                            finishing = Promise.all([settingBlockData, unsubscribing]).then(() => {
                                this.handleExtrinsicFailure(resolve, reject, data[0]);
                            });
                        }
                        else if (receipt.isFinalized) {
                            finishing = Promise.all([settingBlockData, unsubscribing]).then(() => {
                                this.handleExtrinsicSuccess(resolve, reject, receipt);
                            });
                        }
                        else if (receipt.isError) {
                            reject(new internal_1.PolymeshError({ code: types_1.ErrorCode.TransactionAborted }));
                        }
                        finishing.catch((err) => reject(err));
                    }
                });
                gettingUnsub
                    .then(() => {
                    // tx approved by signer
                    this.txHash = txWithArgs.hash.toString();
                    this.updateStatus(types_1.TransactionStatus.Running);
                })
                    .catch((err) => {
                    let error;
                    /* istanbul ignore else */
                    if (err.message.indexOf('Cancelled') > -1) {
                        // tx rejected by signer
                        error = { code: types_1.ErrorCode.TransactionRejectedByUser };
                    }
                    else {
                        // unexpected error
                        error = { code: types_1.ErrorCode.UnexpectedError, 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 fees for the transaction if it was run at this moment, as well as the total amount that can be
     *   charged to it (allowance). A null allowance means that there is no limit to that amount
     *
     * A null return value signifies that the caller Account would pay the fees
     *
     * @note this value might change if, before running the transaction, the caller Account enters (or leaves)
     *   a subsidizer relationship
     */
    getPayingAccount() {
        return __awaiter(this, void 0, void 0, function* () {
            const { paidForBy, context } = this;
            if (this.ignoresSubsidy()) {
                return null;
            }
            if (paidForBy) {
                const { account: primaryAccount } = yield paidForBy.getPrimaryAccount();
                return {
                    type: types_1.PayingAccountType.Other,
                    account: primaryAccount,
                    allowance: null,
                };
            }
            const subsidyWithAllowance = yield context.accountSubsidy();
            if (!subsidyWithAllowance) {
                return null;
            }
            const { subsidy: { subsidizer: account }, allowance, } = subsidyWithAllowance;
            return {
                type: types_1.PayingAccountType.Subsidy,
                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 { signingAddress } = this;
            let { protocolFee: protocol } = this;
            let composedTx;
            try {
                composedTx = this.composeTx();
            }
            catch (err) {
                return null;
            }
            const paymentInfoPromise = composedTx.paymentInfo(signingAddress);
            if (!protocol) {
                protocol = yield this.getProtocolFees();
            }
            const { partialFee } = yield paymentInfoPromise;
            return {
                protocol,
                gas: (0, conversion_1.balanceToBigNumber)(partialFee),
            };
        });
    }
    /**
     * @hidden
     */
    updateStatus(status) {
        this.status = status;
        /* eslint-disable default-case */
        switch (status) {
            case types_1.TransactionStatus.Unapproved:
            case types_1.TransactionStatus.Running:
            case types_1.TransactionStatus.Succeeded: {
                this.emitter.emit(Event.StatusChange, this);
                return;
            }
            case types_1.TransactionStatus.Rejected:
            case types_1.TransactionStatus.Aborted:
            case types_1.TransactionStatus.Failed: {
                this.emitter.emit(Event.StatusChange, this, this.error);
            }
        }
        /* eslint-enable default-case */
    }
    /**
     * @hidden
     *
     * Return whether the transaction ignores any existing subsidizer relationships
     *   and is always paid by the caller
     */
    ignoresSubsidy() {
        /*
         * since we don't know anything about the transaction, a safe default is
         *   to assume it doesn't ignore subsidies
         */
        return false;
    }
    /**
     * @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_1.ErrorCode.TransactionReverted, message, data }));
    }
    /**
     * @hidden
     */
    handleExtrinsicSuccess(resolve, _reject, receipt) {
        resolve(receipt);
    }
}
exports.PolymeshTransactionBase = PolymeshTransactionBase;
//# sourceMappingURL=PolymeshTransactionBase.js.map