"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UniswapV3 = exports.TransactionType = void 0;
const tslib_1 = require("tslib");
/* eslint-disable unicorn/no-array-reduce */
/* eslint-disable no-return-await */
const thegraph_1 = require("./thegraph");
const cli_ux_1 = (0, tslib_1.__importDefault)(require("cli-ux"));
const listr_1 = (0, tslib_1.__importDefault)(require("listr"));
const dayjs_1 = (0, tslib_1.__importDefault)(require("dayjs"));
const pondjs_1 = require("pondjs");
const types_1 = require("pondjs/lib/types");
const chains_1 = require("./chains");
var TransactionType;
(function (TransactionType) {
    TransactionType["SWAP"] = "SWAP";
    TransactionType["MINT"] = "MINT";
    TransactionType["BURN"] = "BURN";
})(TransactionType = exports.TransactionType || (exports.TransactionType = {}));
class UniswapV3 extends thegraph_1.TheGraph {
    constructor(chain) {
        super();
        this.location = {
            [chains_1.Chain.Mainnet]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
            [chains_1.Chain.Polygon]: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswap-v3-polygon',
            [chains_1.Chain.Arbitrum]: 'https://api.thegraph.com/subgraphs/name/ianlapham/arbitrum-dev',
            [chains_1.Chain.Optimism]: 'https://api.thegraph.com/subgraphs/name/ianlapham/optimism-post-regenesis',
            [chains_1.Chain.BSC]: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswap-v3-bsc',
        };
        this.feeTierToTickSpacing = (feeTier) => {
            switch (feeTier) {
                case '10000':
                    return 200;
                case '3000':
                    return 60;
                case '500':
                    return 10;
                case '100':
                    return 1;
                default:
                    throw new Error(`Tick spacing for fee tier ${feeTier} undefined.`);
            }
        };
        this.chain = chain;
    }
    async findTokensBySymbol(tokenName, exact = false) {
        return await this.request(this.location[this.chain], {
            query: `
        query getToken($name: String!){
          tokens(where: {
            ${exact ? 'symbol: $name' : 'symbol_contains: $name'}
          }, orderBy: volumeUSD, orderDirection: desc) {
            id
            name
            symbol
            decimals
          }
        }
      `,
            variables: {
                name: tokenName,
            },
        });
    }
    async findTokens(tokenName) {
        return await this.request(this.location[this.chain], {
            query: `
      query tokens($symbol: String!) {
        tokens(
          where: {or: [{symbol_contains_nocase: $symbol}, {name_contains_nocase: $symbol}]},
          orderBy: volumeUSD
          orderDirection: desc
        ) {
          id
          name
          symbol
          decimals
        }
      }
      `,
            variables: {
                symbol: tokenName,
            },
        });
    }
    async findTokensByAddress(address) {
        return await this.request(this.location[this.chain], {
            query: `
        query getToken($id: String!){
          tokens(where: {
            id: $id
          }) {
            id
            name
            symbol
            decimals
          }
        }
      `,
            variables: {
                id: address,
            },
        });
    }
    async findPoolsByToken0(tokenAddress, complementaryTokenAddresses = []) {
        const response = await this.request(this.location[this.chain], {
            query: `
        query getPools($address: String!){
          pools(skip: 0, first:10, where: {
            token0: $address
            ${complementaryTokenAddresses ?
                `token1_in: [${complementaryTokenAddresses
                    .map(token => `"${token}"`)
                    .join(',')}]` :
                ''}
          }, orderBy: volumeUSD, orderDirection:desc){
            id
            feeTier
            token0 {
              decimals
              id
              name
              symbol
            }
            token1 {
              decimals
              id
              name
              symbol
            }
            createdAtTimestamp
            totalValueLockedUSD
            collectedFeesUSD
          }
        }
      `,
            variables: {
                address: tokenAddress,
            },
        });
        return response;
    }
    async findPoolsByToken1(tokenAddress) {
        const response = await this.request(this.location[this.chain], {
            query: `
        query getPools($address: String!){
          pools(skip: 0, first: 10, where: {
            token1: $address
          }, orderBy: volumeUSD, orderDirection:desc){
            id
            feeTier
            token0 {
              name
              symbol
              decimals
              id
            }
            token1 {
              name
              symbol
              decimals
              id
            }
            createdAtTimestamp
            totalValueLockedUSD
            collectedFeesUSD
          }
        }
      `,
            variables: {
                address: tokenAddress,
            },
        });
        return response;
    }
    async findPoolsByToken(tokenAddress) {
        const token0Pools = await this.findPoolsByToken0(tokenAddress);
        const token1Pools = await this.findPoolsByToken1(tokenAddress);
        return [...token0Pools.data.pools, ...token1Pools.data.pools];
    }
    async getPoolDetails(poolAddress) {
        var _a;
        const response = await this.request(this.location[this.chain], {
            query: `
      query getPoolDetails($address: String!){
        pool(id: $address){
          id
          feeTier
          token0 {
            name
            symbol
            id
            decimals
          }
          token1 {
            name
            symbol
            id
            decimals
          }
          createdAtTimestamp
          createdAtBlockNumber
          totalValueLockedUSD
          collectedFeesUSD
        }
      }
      `,
            variables: {
                address: poolAddress,
            },
        });
        return (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.pool;
    }
    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),
            },
        });
    }
    async getHistoricalData(poolAddress, startDate, endDate, sleep) {
        let data = [];
        const tasks = new listr_1.default([
            {
                title: 'Retrieve swaps',
                task: async (ctx, task) => {
                    const swaps = await this.retrieveSwaps(poolAddress, startDate, endDate, task, sleep);
                    data = swaps;
                    return ctx;
                },
            },
            {
                title: 'Retrieve mints',
                task: async (ctx, task) => {
                    const mints = await this.retrieveMints(poolAddress, startDate, endDate, task, sleep);
                    data.push(
                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                    // @ts-ignore
                    ...mints.map(mint => ({
                        ...mint,
                        type: TransactionType.MINT,
                    })));
                    return ctx;
                },
            },
            {
                title: 'Retrieve burns',
                task: async (ctx, task) => {
                    const burns = await this.retrieveBurns(poolAddress, startDate, endDate, task, sleep);
                    data.push(
                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                    // @ts-ignore
                    ...burns.map(burn => ({
                        ...burn,
                        type: TransactionType.BURN,
                    })));
                    return ctx;
                },
            },
        ], {
            concurrent: false,
        });
        await tasks.run();
        return data;
    }
    async getPreTestData(poolAddress, startDate, endDate, sleep) {
        const data = [];
        const tasks = new listr_1.default([
            {
                title: 'Retrieve candles',
                task: async (ctx, task) => {
                    const swaps = await this.retrieveSwaps(poolAddress, startDate, endDate, task, sleep);
                    data.push(...swaps.map(swap => ({
                        ...swap,
                        type: TransactionType.SWAP,
                    })));
                    return ctx;
                },
            },
        ]);
        await tasks.run();
        return data;
    }
    async fetchUniswapCandlesData(poolAddress, beginingDate, endDate, page, retries = 0) {
        var _a;
        const maxRetries = 5;
        const initialBackoff = 3000;
        const limit = 1000;
        const response = await this.request(this.location[this.chain], {
            query: `
        query getSwaps($address: String!, $startDate: BigInt!, $endDate: BigInt!, $limit: Int!, $skip: Int!){
          swaps(first: $limit, skip: $skip, where: {
            timestamp_gt: $startDate
            timestamp_lt: $endDate
            pool: $address
          }, orderBy: timestamp, orderDirection: asc){
            id
            timestamp
            amount0
            amount1
            transaction {
              id
              blockNumber
            }
            tick
            sqrtPriceX96
            sender
            recipient
            pool {
              liquidity
              feeTier
            }
            token0 {
              decimals
              id
              name
              symbol
            }
            token1 {
              decimals
              id
              name
              symbol
            }
            logIndex
          }
        }
      `,
            variables: {
                address: poolAddress,
                startDate: beginingDate,
                endDate: (endDate.getTime() / 1000).toFixed(0),
                skip: page * limit,
                limit: limit,
            },
        });
        if (((_a = response === null || response === void 0 ? void 0 : response.errors) === null || _a === void 0 ? void 0 : _a.length) > 0) {
            if (retries < maxRetries) {
                retries++;
                await new Promise(resolve => 
                // eslint-disable-next-line no-promise-executor-return
                setTimeout(resolve, initialBackoff * retries));
                return await this.fetchUniswapCandlesData(poolAddress, beginingDate, endDate, page, retries);
            }
            throw new Error('Max retries reached');
        }
        return response;
    }
    // eslint-disable-next-line max-params
    async retrieveSwaps(poolAddress, startDate, endDate, task, sleep = 0) {
        var _a, _b, _c;
        const data = [];
        let page = 0;
        let beginingDate = (startDate.getTime() / 1000).toFixed(0);
        while (new Date(Number(((_a = data[data.length]) === null || _a === void 0 ? void 0 : _a.timestamp) || 0) * 1000) < endDate) {
            // eslint-disable-next-line no-promise-executor-return
            await new Promise(resolve => setTimeout(resolve, 500));
            const response = await this.fetchUniswapCandlesData(poolAddress, Number(beginingDate), endDate, page);
            if (((_c = (_b = response === null || response === void 0 ? void 0 : response.data) === null || _b === void 0 ? void 0 : _b.swaps) === null || _c === void 0 ? void 0 : _c.length) === 0) {
                if (process.env.DEBUG) {
                    // eslint-disable-next-line no-debugger
                    debugger;
                }
                break;
            }
            // adding a type to each swap object
            data.push(...response.data.swaps.map(swap => ({
                ...swap,
                type: TransactionType.SWAP,
            })));
            task.title = `Retrieve swaps (Days left to retrieve: ${Math.abs(Number(this.getTimeDiff(dayjs_1.default.unix(Number(data[data.length - 1].timestamp)).toDate(), endDate, 'day')))}): ${data.length} swaps retrieved`;
            if (page === 5) {
                page = 0;
                beginingDate = (Number(data[data.length - 1].timestamp) + 1).toFixed(0);
            }
            else {
                page++;
            }
            await new Promise(resolve => {
                setTimeout(resolve, Number(sleep || 0.25) * 1000);
            });
        }
        return data;
    }
    // eslint-disable-next-line max-params
    async retrieveBurns(poolAddress, startDate, endDate, task, sleep = 0) {
        var _a;
        const data = [];
        let page = 0;
        const limit = 1000;
        let beginingDate = (startDate.getTime() / 1000).toFixed(0);
        while (new Date(Number(((_a = data[data.length]) === null || _a === void 0 ? void 0 : _a.timestamp) || 0) * 1000) < endDate) {
            const response = await this.request(this.location[this.chain], {
                query: `
          query getBurns($address: String!, $startDate: BigInt!, $endDate: BigInt!, $limit: Int!, $skip: Int!){
            burns(first: $limit, skip: $skip, where: {
              timestamp_gt: $startDate
              timestamp_lt: $endDate
              pool: $address
            }, orderBy: timestamp, orderDirection: asc){
              id
              tickUpper
              tickLower
              amount0
              amount1
              amount
              timestamp
              owner
              pool {
                liquidity
                feeTier
              }
              token0 {
                decimals
                id
                name
                symbol
              }
              token1 {
                decimals
                id
                name
                symbol
              }
            }
          }
        `,
                variables: {
                    address: poolAddress,
                    startDate: beginingDate,
                    endDate: (endDate.getTime() / 1000).toFixed(0),
                    skip: page * limit,
                    limit: limit,
                },
            });
            if (response.data.burns.length === 0) {
                break;
            }
            data.push(...response.data.burns.map(event => {
                return event;
            }));
            task.title = `Retrieve burn liquidity events (Days left to retrieve: ${Math.abs(Number(this.getTimeDiff(dayjs_1.default.unix(Number(data[data.length - 1].timestamp)).toDate(), endDate, 'day')))}): ${data.length} burn liquidity events retrieved`;
            if (page === 5) {
                page = 0;
                beginingDate = (Number(data[data.length - 1].timestamp) + 1).toFixed(0);
            }
            else {
                page++;
            }
            await new Promise(resolve => {
                setTimeout(resolve, Number(sleep) * 1000);
            });
        }
        return data;
    }
    // eslint-disable-next-line max-params
    async retrieveMints(poolAddress, startDate, endDate, task, sleep = 0) {
        var _a;
        const data = [];
        let page = 0;
        const limit = 1000;
        let beginingDate = (startDate.getTime() / 1000).toFixed(0);
        while (new Date(Number(((_a = data[data.length]) === null || _a === void 0 ? void 0 : _a.timestamp) || 0) * 1000) < endDate) {
            const response = await this.request(this.location[this.chain], {
                query: `
          query getMints($address: String!, $startDate: BigInt!, $endDate: BigInt!, $limit: Int!, $skip: Int!){
            mints(first: $limit, skip: $skip, where: {
              timestamp_gt: $startDate
              timestamp_lt: $endDate
              pool: $address
            }, orderBy: timestamp, orderDirection: asc){
              id
              tickUpper
              tickLower
              amount0
              amount1
              amount
              timestamp
              owner
              pool {
                liquidity
                feeTier
              }
              token0 {
                decimals
                id
                name
                symbol
              }
              token1 {
                decimals
                id
                name
                symbol
              }
            }
          }
        `,
                variables: {
                    address: poolAddress,
                    startDate: beginingDate,
                    endDate: (endDate.getTime() / 1000).toFixed(0),
                    skip: page * limit,
                    limit: limit,
                },
            });
            if (response.data.mints.length === 0) {
                break;
            }
            data.push(...response.data.mints.map(event => {
                return event;
            }));
            task.title = `Retrieve increase liquidity events (Days left to retrieve: ${Math.abs(Number(this.getTimeDiff(dayjs_1.default.unix(Number(data[data.length - 1].timestamp)).toDate(), endDate, 'day')))}): ${data.length} increase liquidity events retrieved`;
            if (page === 5) {
                page = 0;
                beginingDate = (Number(data[data.length - 1].timestamp) + 1).toFixed(0);
            }
            else {
                page++;
            }
            await new Promise(resolve => {
                setTimeout(resolve, Number(sleep) * 1000);
            });
        }
        return data;
    }
    getTimeDiff(startDate, endDate, _unit = 'day') {
        return (0, dayjs_1.default)(startDate).diff((0, dayjs_1.default)(endDate), 'day');
    }
    getCandles(data, candleSize) {
        if (data[0] === undefined || Object.keys(data[0]).length === 0) {
            return [];
        }
        const series = (0, pondjs_1.timeSeries)({
            name: 'swaps',
            columns: [...Object.keys(data[0])],
            points: data.map((point) => {
                return [...Object.values(point)].map(value => Number(value));
            }),
        });
        const windowRollup = series
            .fixedWindowRollup({
            window: (0, pondjs_1.window)((0, pondjs_1.duration)(candleSize)),
            aggregation: {
                high: ['value', (0, pondjs_1.max)()],
                low: ['value', (0, pondjs_1.min)()],
                close: ['value', (0, pondjs_1.last)()],
                open: ['value', (0, pondjs_1.first)()],
            },
        })
            .fill({
            fieldSpec: 'value',
            method: 2,
        });
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        return windowRollup
            .toJSON()
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            .points.map((p) => {
            // Convert time range to timestamp of beginning of range
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            p[0] = new pondjs_1.Index(p[0])
                .toTimeRange()
                .toTime(types_1.TimeAlignment.Begin)
                .toDate()
                .getTime();
            return p;
        })
            .map((p) => {
            return {
                timestamp: p[0],
                high: p[1],
                low: p[2],
                close: p[3],
                open: p[4],
            };
        });
    }
}
exports.UniswapV3 = UniswapV3;
