"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");
/**
 * @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 { assets, transactions } = agentPermissions;
                agentPermissionsResult = { result: true };
                if ((assets === null || assets === void 0 ? void 0 : assets.length) && (transactions === null || transactions === void 0 ? void 0 : transactions.length)) {
                    assertOnlyOneAsset(assets);
                    identity = yield fetchIdentity();
                    noIdentity = !identity;
                    agentPermissionsResult = yield getAgentPermissionsResult(identity, assets[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 this Procedure's queue. This defines
     *   what will be run by the TransactionQueue when it is started
     *
     * @returns an array of [[PostTransactionValue]]. Each element corresponds to whatever is returned by one of the resolver functions passed as options
     */
    addTransaction(args) {
        const { transaction, args: txArgs, fee, resolvers = [], isCritical = true, paidForBy, feeMultiplier, } = args;
        const { context } = this;
        const postTransactionValues = resolvers.map(resolver => new internal_1.PostTransactionValue(resolver));
        const signer = context.getSigner();
        this.transactions.push(new internal_1.PolymeshTransaction({
            transaction: transaction,
            args: txArgs,
            postTransactionValues,
            isCritical,
            signer,
            fee,
            feeMultiplier,
            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 this Procedure's queue. This defines
     *   what will be run by the TransactionQueue when it is started
     *
     * @returns an array of [[PostTransactionValue]]. Each element corresponds to whatever is returned by one of the resolver functions passed as options
     */
    addBatchTransaction(args) {
        const { transactions, fee, resolvers = [], isCritical = true, paidForBy, } = args;
        const { context } = this;
        const postTransactionValues = resolvers.map(resolver => new internal_1.PostTransactionValue(resolver));
        const signer = context.getSigner();
        // if only a single transaction is added to the batch, we don't use a batch
        if (transactions.length === 1) {
            const [{ transaction, args: txArgs, feeMultiplier }] = transactions;
            return this.addTransaction({
                transaction: transaction,
                args: txArgs,
                feeMultiplier,
                fee,
                resolvers,
                isCritical,
                paidForBy,
            });
        }
        this.transactions.push(new internal_1.PolymeshTransactionBatch({
            transactions: transactions.map(({ transaction, args: txArgs }) => ({
                transaction,
                args: txArgs,
            })),
            postTransactionValues,
            isCritical,
            signer,
            fee,
            paidForBy,
        }, 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 assertOnlyOneAsset(assets) {
    if (assets.length > 1) {
        throw new internal_1.PolymeshError({
            code: types_1.ErrorCode.FatalError,
            message: 'Procedures cannot require permissions for more than one Asset. Please contact the Polymath team',
        });
    }
}
/**
 * @hidden
 */
function getAgentPermissionsResult(identity, asset, transactions) {
    return __awaiter(this, void 0, void 0, function* () {
        return identity
            ? identity.assetPermissions.checkPermissions({
                asset,
                transactions,
            })
            : { result: false, missingPermissions: transactions };
    });
}
//# sourceMappingURL=Procedure.js.map