"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Deployment = void 0;
/* eslint-disable no-console */
/* eslint-disable import/no-extraneous-dependencies */
const ethers_1 = require("ethers");
const lowdb_1 = __importDefault(require("lowdb"));
const FileSync_1 = __importDefault(require("lowdb/adapters/FileSync"));
const LocalStorage_1 = __importDefault(require("lowdb/adapters/LocalStorage"));
const path_1 = __importDefault(require("path"));
const logger_1 = require("../../ts/logger");
const utils_1 = require("../../ts/utils");
const ContractStorage_1 = require("./ContractStorage");
const types_1 = require("./types");
/**
 * @notice Deployment helper class to run sequential deploy using steps and deploy contracts.
 */
class Deployment {
    /**
     * Initialize class properties only once
     */
    constructor(contractNames, hre) {
        /**
         * Get default params from hardhat task
         *
         * @param {IDeployParams} params - hardhat task arguments
         * @returns {Promise<TaskArguments>} params for deploy workflow
         */
        this.getDefaultParams = ({ verify, incremental }) => Promise.resolve({ verify, incremental });
        this.stepCatalog = new Map([
            ["full", {}],
            ["poll", {}],
        ]);
        this.hre = hre;
        this.contractNames = contractNames;
        this.config = (0, lowdb_1.default)(typeof window !== "undefined"
            ? new LocalStorage_1.default("deploy-config")
            : new FileSync_1.default(path_1.default.resolve(process.cwd(), "./deploy-config.json")));
        this.storage = ContractStorage_1.ContractStorage.getInstance();
    }
    /**
     * Get singleton object
     *
     * @returns {ContractStorage} singleton object
     */
    static getInstance({ contractNames = types_1.EContracts, hre = undefined, } = {}) {
        if (!Deployment.INSTANCE) {
            Deployment.INSTANCE = new Deployment(contractNames, hre);
        }
        return Deployment.INSTANCE;
    }
    /**
     * Start deploy with console log information
     *
     * @param catalog - deploy steps catalog
     * @param {IDeployParams} params - deploy params
     * @returns deploy steps for selected catalog
     */
    async start(catolog, { incremental, verify }) {
        const deployer = await this.getDeployer();
        const deployerAddress = await deployer.getAddress();
        const startBalance = await deployer.provider.getBalance(deployer);
        (0, logger_1.logMagenta)({ text: `Deployer address: ${deployerAddress}` });
        (0, logger_1.logMagenta)({ text: `Deployer start balance: ${Number(startBalance / 10n ** 12n) / 1e6}` });
        if (incremental) {
            (0, logger_1.logMagenta)({ text: "======================================================================" });
            (0, logger_1.logMagenta)({ text: "======================================================================" });
            (0, logger_1.logMagenta)({ text: "====================    ATTENTION! INCREMENTAL MODE    ===============" });
            (0, logger_1.logMagenta)({ text: "======================================================================" });
            (0, logger_1.logMagenta)({ text: "====== Delete 'deployed-contracts.json' to start a new deployment ====" });
            (0, logger_1.logMagenta)({ text: "======================================================================" });
            (0, logger_1.logMagenta)({ text: "======================================================================" });
        }
        else {
            this.storage.cleanup(this.hre.network.name);
        }
        (0, logger_1.logMagenta)({ text: (0, logger_1.info)("Deployment started\n") });
        return this.getDeploySteps(catolog, {
            incremental,
            verify,
        });
    }
    /**
     * Run deploy steps
     *
     * @param steps - deploy steps
     * @param skip - skip steps with less or equal index
     */
    async runSteps(steps, skip) {
        // eslint-disable-next-line no-restricted-syntax
        for (const step of steps) {
            const stepId = `0${step.id}`;
            (0, logger_1.logMagenta)({ text: "\n======================================================================" });
            (0, logger_1.logMagenta)({ text: `${stepId.slice(stepId.length - 2)} ${step.name}` });
            (0, logger_1.logMagenta)({ text: "======================================================================\n" });
            if (step.id <= skip) {
                (0, logger_1.logYellow)({ text: (0, logger_1.warning)(`STEP ${step.id} WAS SKIPPED`) });
            }
            else {
                // eslint-disable-next-line no-await-in-loop
                await this.hre.run(step.taskName, step.args);
            }
        }
    }
    /**
     * Print deployment results and check warnings
     *
     * @param strict - fail on warnings is enabled
     * @throws error if strict is enabled and warning is found
     */
    async checkResults(strict) {
        const deployer = await this.getDeployer();
        const deployerAddress = await deployer.getAddress();
        const [entryMap, instanceCount, multiCount] = this.storage.printContracts(deployerAddress, this.hre.network.name);
        let hasWarn = false;
        if (multiCount > 0) {
            (0, logger_1.logYellow)({ text: (0, logger_1.warning)("WARNING: multi-deployed contract(s) detected") });
            hasWarn = true;
        }
        else if (entryMap.size !== instanceCount) {
            (0, logger_1.logYellow)({ text: (0, logger_1.warning)("WARNING: unknown contract(s) detected") });
            hasWarn = true;
        }
        entryMap.forEach((_, key) => {
            if (key.startsWith("Mock")) {
                (0, logger_1.logYellow)({ text: (0, logger_1.warning)(`WARNING: mock contract detected: ${key}`) });
                hasWarn = true;
            }
        });
        if (hasWarn && strict) {
            throw new Error("Warnings are present");
        }
    }
    /**
     * Finish deployment with console log information
     *
     * @param startBalance - start deployer balance
     * @param isSuccess - success or not
     */
    async finish(startBalance, isSuccess) {
        const deployer = await this.getDeployer();
        const { gasPrice } = this.hre.network.config;
        const endBalance = await deployer.provider.getBalance(deployer);
        (0, logger_1.logMagenta)({ text: "======================================================================" });
        (0, logger_1.logMagenta)({ text: `Deployer end balance: ${Number(endBalance / 10n ** 12n) / 1e6}` });
        (0, logger_1.logMagenta)({ text: `Deploy expenses: ${Number((startBalance - endBalance) / 10n ** 12n) / 1e6}` });
        if (gasPrice !== "auto") {
            (0, logger_1.logMagenta)({ text: `Deploy gas: ${(Number(startBalance - endBalance) / gasPrice, "@", gasPrice / 1e9)} gwei` });
        }
        (0, logger_1.logMagenta)({ text: "======================================================================" });
        if (!isSuccess) {
            (0, logger_1.logRed)({ text: (0, logger_1.error)("\nDeployment has failed") });
            await Promise.resolve().then(() => __importStar(require("process"))).then((m) => m.exit(1));
        }
        (0, logger_1.logGreen)({ text: (0, logger_1.success)("\nDeployment has finished") });
    }
    /**
     * Get deployer (first signer) from hardhat runtime environment
     *
     * @returns {Promise<Signer>} - signer
     */
    async getDeployer() {
        this.checkHre();
        const [deployer] = await this.hre.ethers.getSigners();
        return deployer;
    }
    /**
     * Set hardhat runtime environment
     *
     * @param hre - hardhat runtime environment
     */
    setHre(hre) {
        this.hre = hre;
    }
    /**
     * Set contract names
     *
     * @param contractNames - contract names
     */
    setContractNames(contractNames) {
        this.contractNames = contractNames;
    }
    /**
     * Check if hardhat runtime environment is set
     *
     * @throws {Error} error if there is no hardhat runtime environment set
     */
    checkHre() {
        if (!this.hre) {
            throw new Error("Hardhat Runtime Environment is not set");
        }
    }
    /**
     * Register deploy task by updating step catalog and return task definition
     *
     * @param taskName - unique task name
     * @param stepName - task description
     * @param paramsFn - optional function to override default task arguments
     * @returns {Promise<ConfigurableTaskDefinition>} hardhat task definition
     */
    async deployTask(taskName, stepName, paramsFn) {
        const deployType = taskName.substring(0, taskName.indexOf(":"));
        this.addStep(deployType, { name: stepName, taskName, paramsFn: paramsFn || this.getDefaultParams });
        return Promise.resolve().then(() => __importStar(require("hardhat/config"))).then(({ task }) => task(taskName, stepName));
    }
    /**
     * Register deployment step
     *
     * @param deployType - deploy type
     * @param {IDeployStepCatalog} - deploy step catalog name, description and param mapper
     */
    addStep(deployType, { name, taskName, paramsFn }) {
        const steps = this.stepCatalog.get(deployType);
        if (!steps) {
            throw new Error(`Unknown deploy type: ${deployType}`);
        }
        steps[taskName] = { name, taskName, paramsFn };
    }
    /**
     * Get deploy step sequence
     *
     * @param deployType - deploy type
     * @param {IDeployParams} params - deploy params
     * @returns {Promise<IDeployStep[]>} deploy steps
     */
    async getDeploySteps(deployType, params) {
        const stepList = this.stepCatalog.get(deployType);
        if (!stepList) {
            throw new Error(`Unknown deploy type: ${deployType}`);
        }
        const steps = Object.values(stepList);
        return Promise.all(steps.map(({ paramsFn }) => paramsFn(params))).then((stepArgs) => stepArgs.map((args, index) => ({
            id: index + 1,
            name: steps[index].name,
            taskName: steps[index].taskName,
            args: args,
        })));
    }
    /**
     * Deploy contract and return it
     *
     * @param {IDeployContractParams} params - parameters of deploy contract
     * @param args - constructor arguments
     * @returns deployed contract
     */
    async deployContract({ name, abi, bytecode, signer }, ...args) {
        const deployer = signer || (await this.getDeployer());
        const contractFactory = abi && bytecode
            ? new ethers_1.ContractFactory(abi, bytecode, deployer)
            : await Promise.resolve().then(() => __importStar(require("hardhat"))).then(({ ethers }) => ethers.getContractFactory(this.contractNames[name] || name, deployer));
        const [feeData, nonce] = await Promise.all([
            deployer.provider?.getFeeData(),
            deployer.provider?.getTransactionCount(deployer),
        ]);
        const contract = await contractFactory.deploy(...args, {
            gasPrice: feeData?.gasPrice && !feeData.maxFeePerGas ? feeData.gasPrice : undefined,
            maxFeePerGas: feeData?.maxFeePerGas,
            maxPriorityFeePerGas: feeData?.maxPriorityFeePerGas,
            nonce,
        });
        await contract.deploymentTransaction().wait();
        if (signer && signer.isAA) {
            const { hash } = contract.deploymentTransaction();
            const receipt = await signer.provider?.getTransactionReceipt(hash);
            const address = (0, utils_1.getDeployedContractAddressFromContractReceipt)(receipt);
            if (address) {
                // this contract does not have .deploymentTransaction()
                const realContract = contractFactory.attach(address);
                return realContract;
            }
        }
        return contract;
    }
    /**
     * Deploy contract with linked libraries using contract factory
     *
     * @param {IDeployContractWithLinkedLibrariesParams} params - parameters of deploy contract with linked libraries
     * @param args - constructor arguments
     * @returns deployed contract
     */
    async deployContractWithLinkedLibraries({ contractFactory, signer }, ...args) {
        const deployer = signer || (await this.getDeployer());
        const feeData = await deployer.provider?.getFeeData();
        const contract = await contractFactory.deploy(...args, {
            maxFeePerGas: feeData?.maxFeePerGas,
            maxPriorityFeePerGas: feeData?.maxPriorityFeePerGas,
        });
        await contract.deploymentTransaction().wait();
        if (signer && signer.isAA) {
            const { hash } = contract.deploymentTransaction();
            const receipt = await signer.provider?.getTransactionReceipt(hash);
            const address = (0, utils_1.getDeployedContractAddressFromContractReceipt)(receipt);
            if (address) {
                // this contract does not have .deploymentTransaction()
                const realContract = contractFactory.attach(address);
                return realContract;
            }
        }
        return contract;
    }
    /**
     * Creates contract factory from abi and bytecode
     *
     * @param name - contract name
     * @param abi - Contract abi
     * @param bytecode - Contract linked bytecode
     * @param signer - signer
     * @returns contract factory with linked libraries
     */
    async createContractFactory(abi, bytecode, signer) {
        const deployer = signer || (await this.getDeployer());
        return new ethers_1.ContractFactory(abi, bytecode, deployer);
    }
    /**
     * Get deploy config field (see deploy-config.json)
     *
     * @param id - contract name
     * @param field - config field key
     * @returns config field value or null
     */
    getDeployConfigField(id, field, mustGet = false) {
        this.checkHre();
        const value = this.config
            .get(`${this.hre.network.name}.${this.contractNames[id]}.${field}`)
            .value();
        if (mustGet && (value === null || value === undefined)) {
            throw new Error(`Can't find ${this.hre.network.name}.${id}.${field}`);
        }
        return value;
    }
    /**
     * Update deploy config field (see deploy-config.json)
     * @param id - contract name
     * @param field - config field key
     * @param value - config field value
     */
    updateDeployConfig(id, field, value) {
        this.checkHre();
        this.config.set(`${this.hre.network.name}.${this.contractNames[id]}.${field}`, value).write();
    }
    /**
     * Get contract by name and group key
     *
     * @param {IGetContractParams} params - params
     * @returns contract wrapper
     */
    async getContract({ name, key, address, abi, signer }) {
        const deployer = signer || (await this.getDeployer());
        const contractAddress = address || this.storage.mustGetAddress(name, this.hre.network.name, key);
        if (abi) {
            return new ethers_1.BaseContract(contractAddress, abi, deployer);
        }
        const factory = await this.hre?.ethers.getContractAt(name.toString(), contractAddress, deployer);
        if (!factory) {
            throw new Error(`Contract ${name} not found`);
        }
        return factory.connect(deployer);
    }
}
exports.Deployment = Deployment;
//# sourceMappingURL=Deployment.js.map