"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.Procedure = void 0;
const internal_1 = require("../internal");
const types_1 = require("../types");
const conversion_1 = require("../utils/conversion");
const internal_2 = require("../utils/internal");
/**
 * @hidden
 *
 * Represents an operation performed on the Polymesh blockchain.
 * A Procedure can be prepared to yield a [[TransactionQueue]] that can be run
 */
class Procedure {
    /**
     * @hidden
     *
     * @param prepareTransactions - function that prepares the transaction queue
     * @param getAuthorization - can be a ProcedureAuthorization object or a function that returns a ProcedureAuthorization object
     */
    constructor(prepareTransactions, getAuthorization = () => __awaiter(this, void 0, void 0, function* () { return ({}); }), prepareStorage = () => __awaiter(this, void 0, void 0, function* () { return ({}); })) {
        this.transactions = [];
        this._storage = null;
        this._context = null;
        this.prepareTransactions = prepareTransactions;
        if (typeof getAuthorization !== 'function') {
            this.getAuthorization = () => getAuthorization;
        }
        else {
            this.getAuthorization = getAuthorization;
        }
        this.prepareStorage = prepareStorage;
    }
    /**
     * @hidden
     * Set the context and storage (if not already set), return the Context
     */
    setup(args, context, opts = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this._context) {
                const ctx = context.clone();
                const { signer } = opts;
                if (signer) {
                    ctx.setPair(conversion_1.signerToString(signer));
                }
                this._context = ctx;
            }
            if (!this._storage) {
                this._storage = yield this.prepareStorage(args);
            }
            return this._context;
        });
    }
    /**
     * @hidden
     * Reset the procedure
     */
    cleanup() {
        this.transactions = [];
        this._context = null;
        this._storage = null;
    }
    /**
     * @hidden
     */
    _checkAuthorization(args, context, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            const ctx = yield this.setup(args, context, opts);
            const checkAuthorizationResult = yield this.getAuthorization(args);
            const { permissions = true, roles = true } = checkAuthorizationResult;
            const { signerPermissions = permissions, agentPermissions = permissions, } = checkAuthorizationResult;
            let identity = null;
            let rolesResult;
            let noIdentity = false;
            const account = ctx.getCurrentAccount();
            const fetchIdentity = () => __awaiter(this, void 0, void 0, function* () { return identity || account.getIdentity(); });
            if (typeof roles === 'boolean') {
                rolesResult = { result: roles };
            }
            else if (typeof roles === 'string') {
                rolesResult = { result: false, message: roles };
            }
            else {
                identity = yield fetchIdentity();
                noIdentity = !identity;
                rolesResult = { result: false, missingRoles: roles };
                if (identity) {
                    rolesResult = yield identity.checkRoles(roles);
                }
            }
            let agentPermissionsResult;
            let signerPermissionsAwaitable;
            const accountFrozenPromise = account.isFrozen();
            if (typeof signerPermissions === 'boolean') {
                signerPermissionsAwaitable = { result: signerPermissions };
            }
            else if (typeof signerPermissions === 'string') {
                signerPermissionsAwaitable = { result: false, message: signerPermissions };
            }
            else {
                signerPermissionsAwaitable = account.checkPermissions(signerPermissions);
            }
            if (typeof agentPermissions === 'boolean') {
                agentPermissionsResult = { result: agentPermissions };
            }
            else if (typeof agentPermissions === 'string') {
                agentPermissionsResult = { result: false, message: agentPermissions };
            }
            else {
                const { tokens, transactions } = agentPermissions;
                agentPermissionsResult = { result: true };
                if ((tokens === null || tokens === void 0 ? void 0 : tokens.length) && (transactions === null || transactions === void 0 ? void 0 : transactions.length)) {
                    assertOnlyOneToken(tokens);
                    identity = yield fetchIdentity();
                    noIdentity = !identity;
                    agentPermissionsResult = yield getAgentPermissionsResult(identity, tokens[0], transactions);
                }
            }
            const hasSignerPermissions = yield signerPermissionsAwaitable;
            const accountFrozen = yield accountFrozenPromise;
            return {
                roles: rolesResult,
                signerPermissions: hasSignerPermissions,
                agentPermissions: agentPermissionsResult,
                accountFrozen,
                noIdentity,
            };
        });
    }
    /**
     * Check if the current user has sufficient authorization to run the procedure
     *
     * @param args - procedure arguments
     */
    checkAuthorization(args, context, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                return yield this._checkAuthorization(args, context, opts);
            }
            finally {
                this.cleanup();
            }
        });
    }
    /**
     * Build a [[TransactionQueue]] that can be run
     *
     * @param args.args - arguments required to prepare the queue
     * @param args.transformer - optional function that transforms the Procedure result
     * @param context - context in which the resulting queue will run
     * @param opts.signer - address that will be used as a signer for this procedure
     *   (it must have already been added to the keyring)
     */
    prepare(args, context, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const { args: procArgs, transformer } = args;
                const ctx = yield this.setup(procArgs, context, opts);
                const { roles, signerPermissions, agentPermissions, accountFrozen, noIdentity, } = yield this._checkAuthorization(procArgs, ctx);
                if (noIdentity) {
                    throw new internal_1.PolymeshError({
                        code: types_1.ErrorCode.NotAuthorized,
                        message: 'This procedure requires the Current Account to have an associated Identity',
                    });
                }
                if (accountFrozen) {
                    throw new internal_1.PolymeshError({
                        code: types_1.ErrorCode.NotAuthorized,
                        message: "Current Account can't execute this procedure because it is frozen",
                    });
                }
                if (!signerPermissions.result) {
                    const { message, missingPermissions } = signerPermissions;
                    throw new internal_1.PolymeshError({
                        code: types_1.ErrorCode.NotAuthorized,
                        message: "Current Account doesn't have the required permissions to execute this procedure",
                        data: {
                            message,
                            missingPermissions,
                        },
                    });
                }
                if (!agentPermissions.result) {
                    const { message, missingPermissions } = agentPermissions;
                    throw new internal_1.PolymeshError({
                        code: types_1.ErrorCode.NotAuthorized,
                        message: "Current Identity doesn't have the required permissions to execute this procedure",
                        data: {
                            message,
                            missingPermissions,
                        },
                    });
                }
                if (!roles.result) {
                    const { message, missingRoles } = roles;
                    throw new internal_1.PolymeshError({
                        code: types_1.ErrorCode.NotAuthorized,
                        message: "Current Identity doesn't have the required roles to execute this procedure",
                        data: {
                            message,
                            missingRoles,
                        },
                    });
                }
                const procedureResult = yield this.prepareTransactions(procArgs);
                return new internal_1.TransactionQueue({ transactions: this.transactions, procedureResult, transformer }, ctx);
            }
            finally {
                this.cleanup();
            }
        });
    }
    /**
     * Appends a transaction into the TransactionQueue's queue. This defines
     *   what will be run by the TransactionQueue when it is started.
     *
     * @param tx - a transaction that will be run in the Procedure's TransactionQueue
     * @param options.fee - value in POLYX of the transaction (should only be set manually in special cases, otherwise it is fetched automatically from the chain)
     * @param options.resolvers - asynchronous callbacks used to return runtime data after
     *   the added transaction has finished successfully
     * @param options.isCritical - whether this transaction failing should make the entire queue fail or not. Defaults to true
     * @param options.signer - address or keyring pair of the account that will sign this transaction. Defaults to the current pair in the context
     * @param options.batchSize - amount of elements in the batch (this is only used for certain transactions whose fees depend on the size of the arg list, like `asset.addDocuments`)
     * @param options.paidForBy - third party Identity that will pay for the transaction fees
     * @param args - arguments to be passed to the transaction method
     *
     * @returns an array of [[PostTransactionValue]]. Each element corresponds to whatever is returned by one of the resolver functions passed as options
     */
    addTransaction(transaction, options, ...args) {
        const { fee = null, resolvers = [], isCritical = true, paidForBy, batchSize = null, } = options;
        const { context } = this;
        const postTransactionValues = resolvers.map(resolver => new internal_1.PostTransactionValue(resolver));
        const signer = context.getSigner();
        const tx = transaction;
        this.transactions.push(new internal_1.PolymeshTransaction({
            tx,
            args,
            postTransactionValues,
            isCritical,
            signer,
            fee,
            batchSize,
            paidForBy,
        }, context));
        return postTransactionValues;
    }
    // eslint-disable-next-line require-jsdoc
    addProcedure(procedure, args = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                procedure._context = this.context;
                const returnValue = yield procedure.prepareTransactions(args);
                const { transactions } = procedure;
                this.transactions = [...this.transactions, ...transactions];
                return returnValue;
            }
            catch (err) {
                throw new internal_1.PolymeshError({
                    code: err.code || types_1.ErrorCode.UnexpectedError,
                    message: err.message,
                });
            }
            finally {
                procedure.cleanup();
            }
        });
    }
    /**
     * Appends a batch of transactions into the TransactionQueue's queue. This defines
     *   what will be run by the TransactionQueue when it is started
     *
     * @note if the argument list is too large, they will be separated into multiple batch transactions
     *
     * @param tx - a transaction that will be run in the Procedure's TransactionQueue
     * @param options.fee - value in POLYX of the transaction (should only be set manually in special cases, otherwise it is fetched automatically from the chain)
     * @param options.resolvers - asynchronous callbacks used to return runtime data after
     *   the added transaction has finished successfully
     * @param options.isCritical - whether this transaction failing should make the entire queue fail or not. Defaults to true
     * @param options.signer - address or keyring pair of the account that will sign this transaction. Defaults to the current pair in the context
     * @param options.paidByThirdParty - if the transaction fees will be paid by a third party. Defaults to false
     * @param options.groupByFn - function that establishes how to group the arguments in case the list is too large and they must be separated internally
     * @param args - arguments to be passed to each method in the batch
     *
     * @returns an array of [[PostTransactionValue]]. Each element corresponds to whatever is returned by one of the resolver functions passed as options.
     *   If the batch is separated into smaller batches, resolvers will be run on the last batch
     */
    addBatchTransaction(transaction, options, args) {
        const { fee = null, resolvers = [], isCritical = true, paidForBy, groupByFn, } = options;
        const { context } = this;
        const postTransactionValues = resolvers.map(resolver => new internal_1.PostTransactionValue(resolver));
        const signer = context.getSigner();
        const tx = transaction;
        const tag = conversion_1.transactionToTxTag(tx);
        const specBase = {
            tx,
            postTransactionValues,
            isCritical,
            signer,
            fee,
            paidForBy,
        };
        const batches = internal_2.batchArguments(args, tag, groupByFn);
        batches.forEach((argumentBatch, index) => {
            let spec = Object.assign({}, specBase);
            if (index === batches.length - 1) {
                spec = Object.assign(Object.assign({}, spec), { postTransactionValues });
            }
            if (argumentBatch.length === 1) {
                // single transaction
                this.transactions.push(new internal_1.PolymeshTransaction(Object.assign(Object.assign({}, spec), { args: argumentBatch[0], batchSize: null }), context));
                return;
            }
            this.transactions.push(new internal_1.PolymeshTransactionBatch(Object.assign(Object.assign({}, spec), { args: argumentBatch }), context));
        });
        return postTransactionValues;
    }
    /**
     * internal data container. Used to store common fetched/processed data that is
     *   used by both `prepareTransactions` and `checkAuthorization`
     */
    get storage() {
        const { _storage: storage } = this;
        if (!storage) {
            throw new internal_1.PolymeshError({
                code: types_1.ErrorCode.FatalError,
                message: 'Attempt to access storage before it was set',
            });
        }
        return storage;
    }
    /**
     * contains the data services, current account, etc. In short, the *context* in which
     *   the Procedure is being run
     */
    get context() {
        const { _context: context } = this;
        if (!context) {
            throw new internal_1.PolymeshError({
                code: types_1.ErrorCode.FatalError,
                message: 'Attempt to access context before it was set',
            });
        }
        return context;
    }
}
exports.Procedure = Procedure;
/**
 * @hidden
 */
function assertOnlyOneToken(tokens) {
    if (tokens.length > 1) {
        throw new internal_1.PolymeshError({
            code: types_1.ErrorCode.FatalError,
            message: 'Procedures cannot require permissions for more than one Security Token. Please contact the Polymath team',
        });
    }
}
/**
 * @hidden
 */
function getAgentPermissionsResult(identity, token, transactions) {
    return __awaiter(this, void 0, void 0, function* () {
        return identity
            ? identity.tokenPermissions.checkPermissions({
                token,
                transactions,
            })
            : { result: false, missingPermissions: transactions };
    });
}
//# sourceMappingURL=Procedure.js.map