"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UniswapV3Backtest = void 0;
const tslib_1 = require("tslib");
// Uniswap V3 Contracts
const color_1 = (0, tslib_1.__importDefault)(require("@oclif/color"));
const UniswapV3Factory_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json"));
const UniswapV3Pool_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json"));
const QuoterV2_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/lens/QuoterV2.sol/QuoterV2.json"));
const NFTDescriptor_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/libraries/NFTDescriptor.sol/NFTDescriptor.json"));
const NonfungiblePositionManager_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json"));
const NonfungibleTokenPositionDescriptor_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/NonfungibleTokenPositionDescriptor.sol/NonfungibleTokenPositionDescriptor.json"));
const SwapRouter_json_1 = (0, tslib_1.__importDefault)(require("@uniswap/v3-periphery/artifacts/contracts/SwapRouter.sol/SwapRouter.json"));
const cli_progress_1 = (0, tslib_1.__importDefault)(require("cli-progress"));
const cli_table3_1 = (0, tslib_1.__importDefault)(require("cli-table3"));
const cli_ux_1 = (0, tslib_1.__importDefault)(require("cli-ux"));
const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");
const promises_1 = (0, tslib_1.__importDefault)(require("node:fs/promises"));
const axios_1 = (0, tslib_1.__importDefault)(require("axios"));
const ERC20_json_1 = require("../artifacts/contracts/freeToken.sol/ERC20.json");
const UniLiquidityManager_json_1 = require("../artifacts/contracts/UniLiquidityManager.sol/UniLiquidityManager.json");
const UniMinter_json_1 = require("../artifacts/contracts/UniMinter.json");
// eslint-disable-next-line camelcase
const uniswap_position_quoter_1 = require("@steerprotocol/uniswap-position-quoter");
const change_case_1 = require("change-case");
const network_1 = require("../utils/network");
const constants_1 = require("./../utils/constants");
const replay_1 = require("./replay");
const types_1 = require("./types");
const uniswapv3_1 = require("./uniswapv3");
const publisher_1 = require("./publisher");
const node_os_1 = (0, tslib_1.__importDefault)(require("node:os"));
const chains_1 = require("./chains");
const liquidity_helper_1 = require("./liquidity-helper");
const kill = require('kill-port');
const POOR_ADDRESS = '0x0000000000000000133700000000000000000000';
const RICH_ADDRESS = '0x0000000000000000133800000000000000000000';
const networkData = [
    { chainId: 1, wrappedNativeToken: 'WETH', address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', name: 'Mainnet' },
    { chainId: 3, wrappedNativeToken: 'WETH', address: '0xc778417E063141139Fce010982780140Aa0cD5Ab' },
    { chainId: 4, wrappedNativeToken: 'WETH', address: '0xc778417E063141139Fce010982780140Aa0cD5Ab' },
    { chainId: 5, wrappedNativeToken: 'WETH', address: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6' },
    { chainId: 42, wrappedNativeToken: 'WETH', address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C' },
    { chainId: 10, wrappedNativeToken: 'WETH', address: '0x4200000000000000000000000000000000000006', name: 'Optimism' },
    { chainId: 69, wrappedNativeToken: 'WETH', address: '0x4200000000000000000000000000000000000006' },
    { chainId: 42161, wrappedNativeToken: 'WETH', address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', name: 'Arbitrum' },
    { chainId: 421611, wrappedNativeToken: 'WETH', address: '0xB47e6A5f8b33b3F17603C83a0535A9dcD7E32681' },
    { chainId: 137, wrappedNativeToken: 'WMATIC', address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', name: 'Polygon' },
    { chainId: 56, wrappedNativeToken: 'WBNB', address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', name: 'BSC' },
    { chainId: 80001, wrappedNativeToken: 'WMATIC', address: '0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889' },
];
function getWrappedNativeAddress(chainName, networkName) {
    let network = networkData.find(n => n.chainId === Number(networkName));
    if (!network) {
        network = networkData.find(n => n.name === (chainName));
    }
    if (!network) {
        return null;
    }
    return network.address;
}
// Uniswap v3 artifacts
const artifacts = {
    UniswapV3Factory: UniswapV3Factory_json_1.default,
    SwapRouter: SwapRouter_json_1.default,
    NFTDescriptor: NFTDescriptor_json_1.default,
    NonfungibleTokenPositionDescriptor: NonfungibleTokenPositionDescriptor_json_1.default,
    NonfungiblePositionManager: NonfungiblePositionManager_json_1.default,
    UniswapV3Pool: UniswapV3Pool_json_1.default,
};
class UniswapV3Backtest {
    constructor(uniswapAdapter) {
        this.uniswap = uniswapAdapter;
    }
    // eslint-disable-next-line complexity
    async run(config, strategy, stats) {
        var _a;
        const progressBar = new cli_progress_1.default.SingleBar({}, cli_progress_1.default.Presets.shades_classic);
        stats.status.state = types_1.BacktestState.Running;
        stats.status.progress = 0;
        try {
            const RPC_URL = 'http://localhost:8545';
            // Setup a local RPC Provider for ethers
            const provider = new ethers_1.ethers.providers.JsonRpcProvider(RPC_URL);
            // Parse pool id
            const poolAddress = config.poolId;
            // Uniswap Contract Addresses
            let swapRouterAddress = '0xE592427A0AEce92De3Edee1F18E0157C05861564';
            const NFTManagerAddress = this.uniswap.chain === chains_1.Chain.BSC ? '0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613' : '0xC36442b4a4522E871399CD717aBDD847Ab11FE88';
            const UniswapV3Factory = this.uniswap.chain === chains_1.Chain.BSC ? '0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7' : '0x1F98431c8aD98523631AE4a59f267346ea31F984';
            const chainId = (await provider.getNetwork()).chainId;
            const WETH = getWrappedNativeAddress(this.uniswap.chain, chainId);
            // const quoterAddress = '0x61fFE014bA17989E743c5F6cB21bF9697530B21e' // V3 '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'
            let swapRouterContract;
            // Setup swap router contract
            if (this.uniswap.chain === chains_1.Chain.BSC) {
                const SwapRouterFactory = new ethers_1.ethers.ContractFactory(SwapRouter_json_1.default.abi, SwapRouter_json_1.default.bytecode);
                const SwapRouter = (await SwapRouterFactory.connect(provider.getSigner(0)).deploy(UniswapV3Factory, WETH));
                swapRouterAddress = SwapRouter.address;
            }
            else {
                swapRouterContract = new ethers_1.Contract(swapRouterAddress, new utils_1.Interface(artifacts.SwapRouter.abi)).connect(provider.getSigner(0));
            }
            // Setup pool contract
            const poolContract = new ethers_1.Contract(poolAddress, new utils_1.Interface(artifacts.UniswapV3Pool.abi)).connect(provider.getSigner(0));
            // Parse historical data
            const historicalData = JSON.parse(await promises_1.default.readFile(config.location, 'utf-8'));
            // Copy the historical data, so we can mutate the data
            let txEventsArray = [...historicalData];
            // Sort the events by timestamp so we replay them in proper order
            txEventsArray = txEventsArray.sort(function (a, b) {
                return Number(a.timestamp) - Number(b.timestamp);
            });
            // Load in token info
            const token0Decimals = txEventsArray[0].token0.decimals;
            const token1Decimals = txEventsArray[0].token1.decimals;
            const token0Address = txEventsArray[0].token0.id;
            const token1Address = txEventsArray[0].token1.id;
            const token0Symbol = txEventsArray[0].token0.symbol;
            const token1Symbol = txEventsArray[0].token1.symbol;
            const _token0 = new ethers_1.Contract(token0Address, constants_1.ERC20_ABI, provider);
            const _token1 = new ethers_1.Contract(token1Address, constants_1.ERC20_ABI, provider);
            const poolToken0Balance = await _token0.balanceOf(poolAddress);
            const poolToken1Balance = await _token1.balanceOf(poolAddress);
            // Pull the pool fee for the current pool we are working with
            // This is using for mint, burn, and setup of the associated vault contract
            const poolFee = await poolContract.fee();
            // Token symbols
            const t0Symbol = await _token0.symbol();
            const t1Symbol = await _token1.symbol();
            console.log('Acquiring assets for backtest...');
            const { mainActor, mainSigner, token0, token1 } = (await provideTokensToAccounts([txEventsArray[0].token0.id, txEventsArray[0].token1.id], poolAddress, poolToken0Balance, poolToken1Balance, provider));
            // Process approvals for the swap router and the nft manager
            // (which mints and burns liquidity)
            await token0
                .connect(mainSigner)
                .approve(swapRouterAddress, (0, utils_1.parseUnits)('999999999999', token0Decimals));
            await token1
                .connect(mainSigner)
                .approve(swapRouterAddress, (0, utils_1.parseUnits)('999999999999', token1Decimals));
            await token0
                .connect(mainSigner)
                .approve(poolAddress, (0, utils_1.parseUnits)('999999999999', token0Decimals));
            await token1
                .connect(mainSigner)
                .approve(poolAddress, (0, utils_1.parseUnits)('999999999999', token1Decimals));
            await token0
                .connect(mainSigner)
                .approve(NFTManagerAddress, (0, utils_1.parseUnits)('999999999999', token0Decimals));
            await token1
                .connect(mainSigner)
                .approve(NFTManagerAddress, (0, utils_1.parseUnits)('999999999999', token1Decimals));
            // const token0Balance = await token0.balanceOf(mainSigner.address)
            // const token1Balance = await token1.balanceOf(mainSigner.address)
            // console.log(`Token 0 balance: ${ethers.utils.formatUnits(token0Balance, await token0.decimals())}`)
            // console.log(`Token 1 balance: ${ethers.utils.formatUnits(token1Balance, await token1.decimals())}`)
            // Create contract factory for the UniLiquidityManger
            // eslint-disable-next-line camelcase
            const ManagerFactory = new ethers_1.ethers.ContractFactory(UniLiquidityManager_json_1.abi, UniLiquidityManager_json_1.bytecode);
            const QuoterFactory = new ethers_1.ethers.ContractFactory(QuoterV2_json_1.default.abi, QuoterV2_json_1.default.bytecode);
            const DeployedQuoter = await QuoterFactory.connect(mainSigner).deploy(UniswapV3Factory, WETH);
            const DeployedQuoterAddress = DeployedQuoter.address;
            const quoterContract = new ethers_1.Contract(DeployedQuoterAddress, new utils_1.Interface(QuoterV2_json_1.default.abi)).connect(mainSigner);
            const PositionQuoter = new uniswap_position_quoter_1.CustomRouter(quoterContract);
            // Deploy a new instance of the UniLiquidityManager
            const DeployedManager = await ManagerFactory.connect(mainSigner).deploy();
            // Next we need to encode the input params of the vault, this is abi encoded as different
            // vault will have different params
            const initParams = encodeInitUniV3VaultParams(token0Address, token1Address, poolFee, ethers_1.BigNumber.from(10).pow(36), 1000, 45);
            // Now that we have encoded the vault params, we can initialize the vault
            const txV3 = await DeployedManager.connect(mainSigner).initialize(mainActor.address, // Orchestrator
            mainActor.address, // Steer Address
            mainActor.address, // Governance
            0, initParams);
            // Wait for the tx to be mined
            await txV3.wait();
            const fee = await (await poolContract.fee()).toLocaleString('fullwide', { useGrouping: false });
            const txSetPool = await DeployedManager.setPool(UniswapV3Factory, fee);
            await txSetPool.wait();
            // Deploy a UniMinter helper contract
            const UniMinterFactory = new ethers_1.ContractFactory(UniMinter_json_1.abi, UniMinter_json_1.bytecode, mainSigner);
            const deployedUniMinter = await UniMinterFactory.deploy(token0.address, token1.address, poolAddress);
            // Now that we have a clean wallet to work from we need
            // to setup some tokens so it can begin trading. First,
            // we provide it ETH
            await provider.send('hardhat_setBalance', [
                POOR_ADDRESS,
                (0, utils_1.parseEther)('1000').toHexString(),
            ]);
            // Next, we should impersonate the poor actor and approve for the
            // uni liq vault
            const [, poorSigner] = await getImpersonationSigner(POOR_ADDRESS, provider);
            // Then we convert the initial amounts the user provided into their native values
            const t0initialNative = (0, utils_1.parseUnits)(config.token0InitialAmount, token0Decimals);
            const t1initialNative = (0, utils_1.parseUnits)(config.token1InitialAmount, token1Decimals);
            // We dont want to have tokens yet, dump them
            const t0PreTransferBalance = await token0.balanceOf(POOR_ADDRESS);
            const t1PreTransferBalance = await token1.balanceOf(POOR_ADDRESS);
            // Send existing balance back to rich
            await token0.connect(poorSigner).transfer(mainSigner.address, t0PreTransferBalance);
            await token1.connect(poorSigner).transfer(mainSigner.address, t1PreTransferBalance);
            // Transfer the tokens from the combined top token0 address to the poor actor
            await token0.connect(mainSigner).transfer(POOR_ADDRESS, t0initialNative);
            await token1.connect(mainSigner).transfer(POOR_ADDRESS, t1initialNative);
            const t0PostTransferBalance = await token0.balanceOf(POOR_ADDRESS);
            const t1PostTransferBalance = await token1.balanceOf(POOR_ADDRESS);
            // Approve tokens
            await token0
                .connect(poorSigner)
                .approve(DeployedManager.address, (0, utils_1.parseEther)('999999999999').toHexString());
            await token1
                .connect(poorSigner)
                .approve(DeployedManager.address, (0, utils_1.parseEther)('999999999999').toHexString());
            // Next we want to bootstrap the vault by supplying tokens from the
            // poor actor to the uni liq vault
            const depTx = await DeployedManager.connect(poorSigner).deposit(t0initialNative, t1initialNative, 0, 0, POOR_ADDRESS);
            // Wait for the tx to be mined
            await depTx.wait();
            const t0PostDepositBal = await token0.balanceOf(POOR_ADDRESS);
            const t1PostDepositBal = await token1.balanceOf(POOR_ADDRESS);
            // Track actual deposit amounts based on what
            // was taken from the poor actor's wallet
            const initDept0 = t0PostTransferBalance.sub(t0PostDepositBal);
            const initDept1 = t1PostTransferBalance.sub(t1PostDepositBal);
            const currentSlot = await poolContract.slot0();
            // ratio * 1e18
            const X192 = ethers_1.BigNumber.from(2).pow(192);
            const initPrice = (currentSlot.sqrtPriceX96.pow(2));
            const initToken0In1 = initDept0.mul(initPrice);
            const initTot1 = initDept1.add(initToken0In1.div(X192));
            // process.env.DEBUG && console.log(t1data.data.id);
            const unixStartTime = txEventsArray[0].timestamp;
            let dateMS = new Date(unixStartTime * 1000);
            // let options = {day:'2-digit',month:'2-digit'}
            let dateEN = dateMS
                .toLocaleDateString('en-GB', {
                day: '2-digit',
                month: '2-digit',
                year: 'numeric',
            })
                .replace('/', '-')
                .replace('/', '-');
            const t1DataInitialUSD = await axios_1.default.get(`https://coins.llama.fi/prices/historical/${txEventsArray[0].timestamp}/${this.mapNetwork()}:${token1Address}`, {
                params: {
                    searchWidth: '5m',
                },
            });
            const initT1toUSD = t1DataInitialUSD.data.coins[`${this.mapNetwork()}:${token1Address}`].price;
            // Adjust for decimals and multiply by nonNative usd price
            const _usdInitValPrecBN = ethers_1.BigNumber.from(Math.round(initT1toUSD * 1000000).toString());
            const initVal = Number(initTot1.mul(_usdInitValPrecBN).div(ethers_1.BigNumber.from(10).pow(token1Decimals)).toString()) / 1000000;
            // process.env.DEBUG && console.log(
            //   'Total value deposited measured in USD (accurate to day): $' +
            //     initVal.toFixed(2),
            // )
            // Create a counter for failed transactions
            let txsFailed = 0;
            let cumulativeGas = ethers_1.BigNumber.from(0);
            let nextEpochTrigger = txEventsArray[0].timestamp - 1;
            const epochLength = Number(config.epochLength);
            const backtestDataArray = [];
            const priceMovementDataArray = [];
            let currentSqrtPriceX96;
            // Time the backtesting
            const startTime = Date.now();
            const poolObj = new ethers_1.Contract(poolAddress, UniswapV3Pool_json_1.default.abi, mainSigner);
            progressBar.start(100, 0);
            // -----Main event loop-----
            for (const event of txEventsArray) {
                // console.log(event.type, event.timestamp)
                const percentChange = (txEventsArray.indexOf(event) / txEventsArray.length) * 100;
                // Update progress bar
                const percent = Number(Number(percentChange).toFixed(2));
                progressBar.update(percent);
                stats.status.progress = percent;
                const slot0 = (await poolContract.slot0());
                const currentTick = slot0.tick;
                currentSqrtPriceX96 = slot0.sqrtPriceX96;
                // EPOCH STRATEGY
                // check if this next strategy is past the epoch trigger
                if (event.timestamp >= nextEpochTrigger) {
                    // let results = [];
                    let output;
                    try {
                        const epochCandles = this.uniswap.getCandles(priceMovementDataArray.map(val => ({
                            index: Number(val.timestamp) * 1000,
                            value: String(1.0001 ** Number(val.tick)),
                        })), config.candleSize);
                        output = await strategy.execute(epochCandles);
                    }
                    catch (error) {
                        console.log('Strategy execution bundle error:', error);
                        console.log('Make sure your bundle is working properly and accepts candles as data input.');
                    }
                    const newPositions = [];
                    // Output will be parsed result
                    // eslint-disable-next-line eqeqeq, no-eq-null
                    if (output == null || output.functionName == null) {
                        console.log('Bin failure, skipping tend (possbile problem with input params)');
                        // console.log(output)
                        nextEpochTrigger += epochLength;
                        continue;
                    }
                    // Make position array
                    for (let i = 0; i < output.valuesArray[1][0].length; i++) {
                        const temp = {
                            lowerTick: Number(output.valuesArray[1][0][i]),
                            upperTick: Number(output.valuesArray[1][1][i]),
                            weight: Number(output.valuesArray[1][2][i]),
                        };
                        newPositions.push(temp);
                    }
                    console.log('newPositions', newPositions);
                    // Strategy returns array of start end weight
                    if (newPositions && newPositions.length > 0 && newPositions[0].weight === 0) {
                        // console.log('no positions, skipping epoch')
                        nextEpochTrigger += epochLength;
                        continue;
                    }
                    await DeployedManager.poke();
                    const [balt0, balt1,] = await DeployedManager.getTotalAmounts();
                    const pos = [];
                    for (const newPos of newPositions) {
                        pos.push({
                            lowerTick: Number(newPos.lowerTick),
                            upperTick: Number(newPos.upperTick),
                            weight: Number(newPos.weight),
                        });
                    }
                    // Here we tend to take everything out so the calculations dont use our liquidity when reaching exact precision
                    const _LiqPos = {
                        lowerTick: [0],
                        upperTick: [600],
                        relativeWeight: [0],
                    };
                    const encoder = new utils_1.AbiCoder();
                    const _timeSensitive = encoder.encode(['int256', 'uint160'], [0, ethers_1.BigNumber.from(0)]);
                    await (0, network_1.setNextBlockTimestamp)(120, provider);
                    try {
                        const tendTx = await DeployedManager.connect(mainSigner).tend(0, _LiqPos, _timeSensitive, {
                            gasLimit: 30000000,
                        });
                        await tendTx.wait();
                    }
                    catch (error) {
                        console.log('🚨 Tend Failure!');
                        throw error;
                    }
                    let swapResult;
                    try {
                        swapResult = await PositionQuoter.getSwapAmount(poolObj, pos, balt0, balt1, token0Address, token1Address, poolFee, 20);
                    }
                    catch (error) {
                        console.log('Swap estimate error:', error);
                    }
                    if (!swapResult) {
                        console.log('🚨 🚨 Skipping tend due to swap result failure');
                    }
                    else if (swapResult) {
                        // Slippage check ~ can be configured, but not necessarily a concern with assumptions
                        const sqrtPrice = swapResult.zeroForOne ? ethers_1.BigNumber.from('50').mul(currentSlot.sqrtPriceX96).div('100') : ethers_1.BigNumber.from('150').mul(currentSlot.sqrtPriceX96).div('100');
                        // convert pos into LiquidityPositionsStruct
                        const lowerTick = [];
                        const upperTick = [];
                        const relativeWeight = [];
                        // eslint-disable-next-line max-depth
                        for (const isoPos of newPositions) {
                            lowerTick.push(Number(isoPos.lowerTick));
                            upperTick.push(Number(isoPos.upperTick));
                            relativeWeight.push(Number(isoPos.weight));
                        }
                        const LiqPos = {
                            lowerTick: lowerTick,
                            upperTick: upperTick,
                            relativeWeight: relativeWeight,
                        };
                        await (0, network_1.setNextBlockTimestamp)(120, provider);
                        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                        // @ts-ignore
                        await (0, replay_1.mineBlock)(provider);
                        let _tendTx;
                        const totalWeight = output.valuesArray[0];
                        const timeSensitive = encoder.encode(['int256', 'uint160'], [(_a = swapResult === null || swapResult === void 0 ? void 0 : swapResult.amountToSwap) === null || _a === void 0 ? void 0 : _a.mul((swapResult === null || swapResult === void 0 ? void 0 : swapResult.zeroForOne) ? 1 : -1), sqrtPrice]);
                        // eslint-disable-next-line max-depth
                        try {
                            const tendTx = await DeployedManager.connect(mainSigner).tend(totalWeight, LiqPos, timeSensitive, {
                                gasLimit: 30000000,
                            });
                            _tendTx = await tendTx.wait();
                        }
                        catch (error) {
                            console.log('🚨 Tend Failure! Make sure your strategy is making positions on initializable ticks (Relative to pool fee)');
                            throw error;
                        }
                        cumulativeGas = cumulativeGas.add(_tendTx.gasUsed);
                        const rebalance = {
                            timestamp: nextEpochTrigger,
                            positions: newPositions,
                            currentTick,
                            balt0: balt0.toString(),
                            balt1: balt1.toString(),
                            gasUsed: _tendTx.gasUsed.toString(),
                        };
                        // add data
                        backtestDataArray.push(rebalance);
                        stats.rebalances.push(rebalance);
                        // set next trigger
                        nextEpochTrigger += epochLength;
                    }
                }
                if (event.type === uniswapv3_1.TransactionType.SWAP) {
                    // Number might be negative here, so we check for '-'
                    const swapParams = (event.amount0[0] === '-') ?
                        {
                            tokenIn: token1Address,
                            tokenOut: token0Address,
                            fee: poolFee,
                            recipient: mainSigner.address,
                            deadline: 1920170954,
                            amountIn: (0, utils_1.parseUnits)(event.amount1, token1Decimals),
                            amountOutMinimum: 0,
                            sqrtPriceLimitX96: ethers_1.BigNumber.from('0'),
                        } :
                        {
                            // token0 in 1 out
                            tokenIn: token0Address,
                            tokenOut: token1Address,
                            fee: poolFee,
                            recipient: mainSigner.address,
                            deadline: 1920170954,
                            amountIn: (0, utils_1.parseUnits)(event.amount0, token0Decimals),
                            amountOutMinimum: 0,
                            sqrtPriceLimitX96: ethers_1.BigNumber.from('0'),
                        };
                    try {
                        const swapTx = await swapRouterContract
                            .connect(mainSigner)
                            .exactInputSingle(swapParams, { gasLimit: 30000000 });
                        await swapTx.wait();
                        // tx10 = await tx9.wait() as ContractReceipt;
                    }
                    catch (error) {
                        process.env.DEBUG && console.log(error);
                        txsFailed += 1;
                    }
                    const priceMovement = {
                        timestamp: event.timestamp,
                        tick: currentTick,
                    };
                    priceMovementDataArray.push(priceMovement);
                    stats.poolStates.push(priceMovement);
                }
                if (event.type === uniswapv3_1.TransactionType.MINT) {
                    process.env.DEBUG && console.log('🍃');
                    if (String(event.amount0).includes('.') || String(event.amount1).includes('.')) {
                        continue;
                    }
                    // const mintParams: MintParamsStruct = {
                    //   token0: token0Address,
                    //   token1: token1Address,
                    //   fee: poolFee,
                    //   tickLower: event.tickLower,
                    //   tickUpper: event.tickUpper,
                    //   amount0Desired: BigNumber.from(event.amount0),    // amounts here are in native precision
                    //   amount1Desired: BigNumber.from(event.amount1),
                    //   amount0Min: 0, // BigNumber.from(event.AMOUNT0),
                    //   amount1Min: 0, // BigNumber.from(event.AMOUNT1),
                    //   recipient: mainSigner.address,
                    //   deadline: 1_920_170_954,
                    // }
                    try {
                        const lowerSqrtPriceX96 = (0, liquidity_helper_1.getSqrtRatioAtTick)(Number(event.tickLower));
                        const upperSqrtPriceX96 = (0, liquidity_helper_1.getSqrtRatioAtTick)(Number(event.tickUpper));
                        const [mintAmount0, mintAmount1] = (0, liquidity_helper_1.getAmountsForLiquidity)(currentSqrtPriceX96, lowerSqrtPriceX96, upperSqrtPriceX96, ethers_1.BigNumber.from(event.amount));
                        // const mintTx = await nftManagerContract.connect(mainSigner).mint(mintParams)
                        try {
                            const mintTx = await deployedUniMinter.mint(mainSigner.address, event.tickLower, event.tickUpper, mintAmount0, mintAmount1);
                            const mintTxDone = await mintTx.wait();
                            console.log('Mint done:', mintTxDone.transactionHash);
                        }
                        catch {
                            const [, tempSigner] = await getImpersonationSigner(event.owner, provider);
                            await provider.send('hardhat_setBalance', [
                                event.owner,
                                '0x999999999000000000000000000',
                            ]);
                            await token0.transfer(deployedUniMinter.address, ethers_1.BigNumber.from(event.amount0).mul(150).div(100));
                            await token1.transfer(deployedUniMinter.address, ethers_1.BigNumber.from(event.amount1).mul(150).div(100));
                            const mintTx = await deployedUniMinter.connect(tempSigner).mint(mainSigner.address, event.tickLower, event.tickUpper, mintAmount0, mintAmount1);
                            const mintTxDone = await mintTx.wait();
                            console.log('Mint done:', mintTxDone.transactionHash);
                        }
                    }
                    catch (error) {
                        txsFailed += 1;
                        process.env.DEBUG && console.log(error);
                    }
                }
                if (event.type === uniswapv3_1.TransactionType.BURN) {
                    process.env.DEBUG && console.log('🔥');
                    // Any pokes are picked up by the burn
                    if (Number(event.amount) === 0)
                        continue;
                    let _error;
                    // See if the token was made by us, or if we should imitate
                    try {
                        try {
                            // We first try the actual owner so we dont drain an unintentional position
                            const [, tempSigner] = await getImpersonationSigner(
                            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                            // @ts-ignore
                            event.owner, provider);
                            await provider.send('hardhat_setBalance', [
                                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                                // @ts-ignore
                                event.owner,
                                '0x999999999000000000000000000',
                            ]);
                            const txDec = await poolContract.connect(tempSigner).burn(event.tickLower, event.tickUpper, event.amount);
                            await txDec.wait();
                            // -- swapped
                            // const txDec = await poolContract.connect(mainSigner).burn(event.tickLower, event.tickUpper, event.amount)
                            // await txDec.wait()
                        }
                        catch (error) {
                            // eslint-disable-next-line max-depth
                            try {
                                const txDec = await poolContract.connect(mainSigner).burn(event.tickLower, event.tickUpper, event.amount);
                                await txDec.wait();
                                _error = error;
                            }
                            catch (error_) {
                                // eslint-disable-next-line max-depth
                                try {
                                    // Try Uniswap V3 NFT manager
                                    const [, tempSigner] = await getImpersonationSigner(
                                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                                    // @ts-ignore
                                    NFTManagerAddress, provider);
                                    await provider.send('hardhat_setBalance', [
                                        NFTManagerAddress,
                                        '0x999999999000000000000000000',
                                    ]);
                                    const txDec = await poolContract.connect(tempSigner).burn(event.tickLower, event.tickUpper, event.amount);
                                    await txDec.wait();
                                    _error = error_;
                                }
                                catch {
                                    const [, tempSigner] = await getImpersonationSigner(
                                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                                    // @ts-ignore
                                    deployedUniMinter.address, provider);
                                    await provider.send('hardhat_setBalance', [
                                        deployedUniMinter.address,
                                        '0x999999999000000000000000000',
                                    ]);
                                    const txDec = await poolContract.connect(tempSigner).burn(event.tickLower, event.tickUpper, event.amount);
                                    await txDec.wait();
                                    _error = error_;
                                }
                            }
                        }
                    }
                    catch (error) {
                        txsFailed += 1;
                        process.env.DEBUG && console.log(error);
                        process.env.DEBUG && console.log('_error', _error);
                    }
                }
            } //  --END event loop--
            const endTime = Date.now();
            process.env.DEBUG && console.log('🥇');
            const txLength = txEventsArray.length;
            const successPercent = Math.round(((txLength - txsFailed) * 10000) / txLength) / 100;
            const simRunTime = (endTime - startTime) / (1000 * 60);
            // console.log('Simulation ran in', simRunTime.toFixed(2), 'minutes')
            const simulatedTime = txEventsArray[txEventsArray.length - 1].timestamp -
                txEventsArray[0].timestamp;
            const YEARInSeconds = 31536000;
            // const compoundsYearly = YEARInSeconds / epochLength
            // @todo withdraw funds, compare and analyze
            const finShareBal = await DeployedManager.balanceOf(POOR_ADDRESS);
            // console.log('ending share bal:', finShareBal.toString())
            await (0, network_1.setNextBlockTimestamp)(5, provider);
            console.log('\n__________');
            await DeployedManager.connect(poorSigner).withdraw(finShareBal, 0, 0, POOR_ADDRESS);
            // The difference of how much principle we started with vs how much was deposited
            // const t0offset = initDept0.gt(t0initialNative) ? BigNumber.from(0) : t0initialNative.sub(initDept0)
            // const t1offset = initDept1.gt(t1initialNative) ? BigNumber.from(0) : t1initialNative.sub(initDept1)
            // Final balances compared to amount deposited (initDept0)
            const finalBal0 = (await token0.balanceOf(POOR_ADDRESS)).sub(t0PostDepositBal);
            const finalBal1 = (await token1.balanceOf(POOR_ADDRESS)).sub(t1PostDepositBal);
            // This helps us get some acceptable precision with various decimal lengths
            const highPoint0 = finalBal0.gt(initDept0) ? finalBal0 : initDept0;
            const highPoint1 = finalBal1.gt(initDept1) ? finalBal1 : initDept1;
            // Here we take (endingBal - Deposit) * precision * 10000 / precision / Deposit / 100 giving us a the percentage change, same for next line Number(finalBal1.toString())
            const fint0change = (finalBal0
                .sub(initDept0)
                .mul(highPoint0.mul(10000))
                .div(highPoint0)
                .div(initDept0)
                .toNumber() / 100).toFixed(2);
            const fint1change = (finalBal1
                .sub(initDept1)
                .mul(highPoint1.mul(10000))
                .div(highPoint1)
                .div(initDept1)
                .toNumber() / 100)
                .toFixed(2);
            // Calculate the current ratio
            // This value might be quite high or very 0<n<1
            const finPrice = (currentSlot.sqrtPriceX96.pow(2));
            const finToken0In1 = (finalBal0).mul(finPrice);
            const finTot1 = (finalBal1).add(finToken0In1.div(X192));
            // Here we can abandon precision, these estimates will only be precise up to 17 digits
            const difference = (finTot1.sub(initTot1)).mul(10000);
            const numDiff = difference.div(initTot1);
            const periodChange = numDiff.toNumber() / 10000;
            // Here we get the day the test ended to call the price of ETH and t1 that day
            dateMS = new Date(txEventsArray[txEventsArray.length - 1].timestamp * 1000);
            dateEN = dateMS
                .toLocaleDateString('en-GB', {
                day: '2-digit',
                month: '2-digit',
                year: 'numeric',
            })
                .replace('/', '-')
                .replace('/', '-');
            const t1DataFinalUSD = await axios_1.default.get(`https://coins.llama.fi/prices/historical/${txEventsArray[txEventsArray.length - 1].timestamp}/${this.mapNetwork()}:${token1Address}`, {
                params: {
                    searchWidth: '5m',
                },
            });
            // WETH
            const ethFinalUSDCG = await axios_1.default.get(`https://coins.llama.fi/prices/historical/${txEventsArray[txEventsArray.length - 1].timestamp}/coingecko:ethereum`, {
                params: {
                    searchWidth: '5m',
                },
            });
            let ethFinalUSD;
            let finT1toUSD;
            try {
                ethFinalUSD = ethFinalUSDCG.data.coins['coingecko:ethereum'].price;
            }
            catch {
                console.log('Error fetching final eth price, eth dollar values will be incorrect');
                ethFinalUSD = 1;
            }
            try {
                finT1toUSD = t1DataFinalUSD.data.coins[`${this.mapNetwork()}:${token1Address}`].price;
            }
            catch {
                console.log('Error fetching the final T1 price relative, dollar estimates will be incorrect');
                finT1toUSD = 1;
            }
            const _usdFinValPrecBN = ethers_1.BigNumber.from(Math.round(finT1toUSD * 100000000).toString());
            const finalVal = Number((finTot1.mul(_usdFinValPrecBN).div(ethers_1.BigNumber.from(10).pow(token1Decimals))).toString()) / 100000000;
            // gains over time * times run in year, doesn't factor compunding
            const staticAPY = (YEARInSeconds / simulatedTime) * periodChange * 100;
            // console.log('Linearized APY (less accurate):', staticAPY.toString() + '%')
            // See how many times the funds were compounded in this time period
            const numEpochs = simulatedTime / epochLength;
            const epochPYear = (YEARInSeconds / simulatedTime);
            const APY = ((1 + periodChange) ** epochPYear) - 1;
            // Note true compounding (by epoch length) was retired because it smooths results too much to give unrealistic apys
            // Calculate what the return would have been with compounding each epoch
            // const avgEpochReturn = numEpochs * (((periodChange + 1) ** (1 / numEpochs)) - 1)
            // Take this rate and apply it for a whole year
            // const compoundedAPY = ((1 + avgEpochReturn) ** compoundsYearly) - 1
            // Vs hodl
            const finHodlComparison = Number(initTot1.mul(_usdFinValPrecBN).div(ethers_1.BigNumber.from(10).pow(token1Decimals)).toString()) / 100000000;
            // Gas and epoch info
            const cumGasAdj = ethers_1.ethers.utils.formatEther(cumulativeGas.mul(10 ** 9).mul(Number(config.gasPrice).toFixed(0)));
            const gasUSD = Number(cumGasAdj) * ethFinalUSD;
            // upload to pinata
            await strategy.uploadToPinata();
            // prepare result object to be pushed
            const resultObj = {};
            console.log('** note USD values are from 18:00 UTC from the start & end dates of the backtest period **');
            console.log(color_1.default.yellow('\nGeneral Run Info'));
            const _table1 = new cli_table3_1.default({
                head: ['Backtest Runtime', 'TX Count', 'Failed TXs', 'Success Rate'],
                colWidths: [20, 10, 20, 20],
            });
            // run info
            resultObj.simulationRunTime = simRunTime.toFixed(2) + ' minutes';
            resultObj.txCount = txLength;
            resultObj.failedTxCount = txsFailed;
            resultObj.successPercent = successPercent;
            resultObj.poolAddress = poolAddress;
            resultObj.backtestDate = Date.now();
            _table1.push([(simRunTime.toFixed(2) + ' minutes'), txLength, txsFailed, successPercent + '%']);
            const runInfo = _table1.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table1.options.head[index])]: _table1[index],
                };
                return acc;
            }, {});
            console.log(_table1.toString());
            console.log(color_1.default.green('Initial Balance Info'));
            const _table2 = new cli_table3_1.default({
                head: ['Initial Balance ' + t0Symbol, 'Initial Balance ' + t1Symbol, 'Total Principle in ' + t1Symbol, 'Initial Value in USD'],
                colWidths: [25, 25, 35, 25],
            });
            // initial balance info
            resultObj.token0Name = t0Symbol;
            resultObj.token1Name = t1Symbol;
            resultObj.initialBalanceToken0 = ethers_1.ethers.utils.formatUnits(initDept0, token0Decimals);
            resultObj.initialBalanceToken1 = ethers_1.ethers.utils.formatUnits(initDept1, token1Decimals);
            resultObj.totalPrincipleToken1 = ethers_1.ethers.utils.formatUnits(initTot1, token1Decimals);
            resultObj.initialValueInUsd = initVal;
            // get current system name from os in typescript
            const systemName = node_os_1.default.type();
            resultObj.systemName = systemName;
            // get current system version from os
            const systemVersion = node_os_1.default.release();
            resultObj.systemVersion = systemVersion;
            // get current system architecture from os
            const systemArch = node_os_1.default.arch();
            resultObj.systemArch = systemArch;
            // get current system platform from os
            const systemPlatform = node_os_1.default.platform();
            resultObj.systemPlatform = systemPlatform;
            // get current system hostname from os
            const systemHostname = node_os_1.default.hostname();
            resultObj.systemHostname = systemHostname;
            _table2.push([ethers_1.ethers.utils.formatUnits(initDept0, token0Decimals), ethers_1.ethers.utils.formatUnits(initDept1, token1Decimals), ethers_1.ethers.utils.formatUnits(initTot1, token1Decimals), '$' + initVal.toFixed(2)]);
            console.log(_table2.toString());
            const initialBalanceInfo = _table2.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table2.options.head[index])]: _table2[index],
                };
                return acc;
            }, {});
            console.log(color_1.default.blue('Strategy Info'));
            const _table3 = new cli_table3_1.default({
                head: ['Strategy Runs', 'Gas Spent', 'ETH spent on gas', 'Gas Spent (USD) - end of period price'],
                colWidths: [20, 20, 20, 45],
            });
            // strategy info
            resultObj.strategyRuns = Math.floor(numEpochs);
            resultObj.gasSpent = cumulativeGas.toString();
            resultObj.ethSpentOnGas = cumGasAdj.toString();
            resultObj.gasSpentUsd = gasUSD;
            _table3.push([Math.floor(numEpochs).toString(), cumulativeGas.toString(), cumGasAdj.toString(), '$' + gasUSD.toFixed(2)]);
            console.log(_table3.toString());
            const strategyInfo = _table3.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table3.options.head[index])]: _table3[index],
                };
                return acc;
            }, {});
            console.log(color_1.default.green('Final Balance Info'));
            const _table4 = new cli_table3_1.default({
                head: ['Final Balance ' + t0Symbol, 'Change in ' + t0Symbol + ' Holdings', 'Final Balance ' + t1Symbol, 'Change in ' + t1Symbol + ' Holdings', 'Final Total in ' + t1Symbol, 'Final Value in USD'],
                colWidths: [25, 25, 25, 30, 25, 25],
            });
            // final balance info
            resultObj.finalBalanceToken0 = ethers_1.ethers.utils.formatUnits(finalBal0, token0Decimals);
            resultObj.finalBalanceToken1 = ethers_1.ethers.utils.formatUnits(finalBal1, token1Decimals);
            resultObj.finalTotalInToken1 = ethers_1.ethers.utils.formatUnits(finTot1, token1Decimals);
            resultObj.finalValueInUsd = finalVal;
            resultObj.changeInToken0Holdings = fint0change;
            resultObj.changeInToken1Holdings = fint1change;
            _table4.push([ethers_1.ethers.utils.formatUnits(finalBal0, token0Decimals), fint0change + '%', ethers_1.ethers.utils.formatUnits(finalBal1, token1Decimals), fint1change + '%', ethers_1.ethers.utils.formatUnits(finTot1, token1Decimals), '$' + finalVal.toFixed(2)]);
            console.log(_table4.toString());
            const finalBalanceInfo = _table4.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table4.options.head[index])]: _table4[index],
                };
                return acc;
            }, {});
            console.log(color_1.default.magenta('Results'));
            const _table5 = new cli_table3_1.default({
                head: ['Return from Backtest', 'Net USD Adjusted for Gas', 'Compounded APY', 'Linearized APY', 'Hold Change in Value'],
                colWidths: [25, 30, 20, 20],
            });
            // results
            resultObj.returnFromBacktest = periodChange * 100;
            resultObj.netUsdAdjustedForGas = finalVal - initVal - gasUSD;
            resultObj.compoundedApy = 100 * APY;
            resultObj.linearizedApy = staticAPY;
            resultObj.holdChangeInValue = (finHodlComparison / initVal - 1) * 100;
            resultObj.ipfsHash = strategy.ipfsHash;
            _table5.push([(periodChange * 100).toFixed(4) + '%', '$' + (finalVal - initVal - gasUSD).toFixed(2), (100 * APY).toFixed(2) + '%', staticAPY.toFixed(4) + '%', (((finHodlComparison / initVal) - 1) * 100).toFixed(2) + '%']);
            console.log(_table5.toString());
            const results = _table5.options.head.reduce((acc, _curr, index) => {
                acc = {
                    ...acc,
                    [(0, change_case_1.camelCase)(_table5.options.head[index])]: _table5[index],
                };
                return acc;
            }, {});
            stats.results = {
                runInfo,
                initialBalanceInfo,
                strategyInfo,
                finalBalanceInfo,
                results,
            };
            resultObj.strategyConfig = JSON.stringify(strategy.configParams);
            const publisher = new publisher_1.Publisher();
            await publisher.publish(resultObj);
        }
        catch (error) {
            progressBar.stop();
            await kill(8545);
            setTimeout(() => {
                throw error;
            }, 10000);
        }
        stats.status.progress = 100;
        stats.status.state = types_1.BacktestState.Finished;
        // ----END TEST----
    }
    getTopHolder(holders, tokenAddress, type, provider) {
        return holders.find(async (holder) => {
            const swapInToken = new ethers_1.Contract(tokenAddress, constants_1.ERC20_ABI, provider);
            const balance = await swapInToken.balanceOf(holder.address);
            return holder.address_type === type && balance.gt(0);
        });
    }
    // convert Chain to network name
    mapNetwork() {
        switch (this.uniswap.chain) {
            case chains_1.Chain.Mainnet:
                return 'ethereum';
            case chains_1.Chain.Polygon:
                return 'polygon';
            case chains_1.Chain.Arbitrum:
                return 'arbitrum';
            case chains_1.Chain.Optimism:
                return 'optimism';
            case chains_1.Chain.BSC:
                return 'bsc';
        }
    }
    displayTargetEntityDetails(poolDetails) {
        cli_ux_1.default.table([poolDetails], {
            id: {
                header: 'Address',
                get: (row) => row.id,
            },
            pair: {
                header: 'Pair',
                get: (row) => `${row.token0.name}/${row.token1.name}`,
            },
            feeTier: {
                header: 'Feed Tier',
                get: (row) => `${Number(row.feeTier) / 10000}%`,
            },
            createdAtTimestamp: {
                header: 'Created At',
                get: (row) => new Date(Number(row.createdAtTimestamp) * 1000).toDateString(),
            },
            Liquidity: {
                header: 'Liquidity',
                get: (row) => Number(row.totalValueLockedUSD).toFixed(2),
            },
        });
    }
}
exports.UniswapV3Backtest = UniswapV3Backtest;
const getImpersonationSigner = async (address, provider) => {
    const actor = new network_1.Impersonator(address);
    const signer = await actor.impersonate(provider);
    return [actor, signer];
};
const encodeInitUniV3VaultParams = (token0, token1, fee, maxTotalSupply, _maxTickChange, _twapInterval) => {
    // Handle optional params
    if (maxTotalSupply === undefined) {
        maxTotalSupply = ethers_1.BigNumber.from(10).pow(36);
    }
    if (typeof _maxTickChange === 'undefined' || _maxTickChange === null) {
        _maxTickChange = 250;
    }
    if (typeof _twapInterval === 'undefined') {
        _twapInterval = 45;
    }
    return ethers_1.utils.defaultAbiCoder.encode([
        'address',
        'address',
        'uint24',
        'uint256',
        'int24',
        'int32',
    ], [
        token0,
        token1,
        fee,
        maxTotalSupply,
        _maxTickChange,
        _twapInterval, // How much time
    ]);
};
// eslint-disable-next-line max-params
const provideTokensToAccounts = async (tokens, poolAddress, pool0Bal, pool1Bal, provider) => {
    // replace bytecode with free mint code
    await provider.send('hardhat_setCode', [
        tokens[0],
        ERC20_json_1.deployedBytecode,
    ]);
    // replace bytecode with free mint code
    await provider.send('hardhat_setCode', [
        tokens[1],
        ERC20_json_1.deployedBytecode,
    ]);
    // Setup an impersonation account for the first holder
    // in the service provider response
    const [mainActor, mainSigner] = await getImpersonationSigner(RICH_ADDRESS, provider);
    await provider.send('hardhat_setBalance', [
        mainActor.address,
        (0, utils_1.parseEther)('1000').toHexString(),
    ]);
    const freedToken0 = new ethers_1.Contract(tokens[0], ERC20_json_1.abi, provider);
    const freedToken1 = new ethers_1.Contract(tokens[1], ERC20_json_1.abi, provider);
    // console.log(await freedToken0.balanceOf(RICH_ADDRESS))
    await freedToken0.connect(mainSigner).mint(RICH_ADDRESS, (0, utils_1.parseUnits)('10', 36));
    await freedToken1.connect(mainSigner).mint(RICH_ADDRESS, (0, utils_1.parseUnits)('10', 36));
    // Check balance of pool address and see if the storage is valid, if so dont mint
    const _poolBal0 = await freedToken0.balanceOf(poolAddress);
    const _poolBal1 = await freedToken1.balanceOf(poolAddress);
    if (_poolBal0.eq(0))
        await freedToken0.connect(mainSigner).mint(poolAddress, pool0Bal);
    if (_poolBal1.eq(0))
        await freedToken1.connect(mainSigner).mint(poolAddress, pool1Bal);
    // console.log(await freedToken0.balanceOf(RICH_ADDRESS))
    // Once we have the op holders, we can setup the tokens
    // and connect their signers for token0 and token1
    const token0 = new ethers_1.Contract(tokens[0], constants_1.ERC20_ABI, mainSigner);
    const token1 = new ethers_1.Contract(tokens[1], constants_1.ERC20_ABI, mainSigner);
    return { mainActor, mainSigner, token0, token1 };
};
