import { __assign, __awaiter, __generator, __spreadArray } from "tslib";
import assert from "assert";
import * as uma from "@uma/sdk";
import { bnZero, toBNWei, fixedPointAdjustment, calcPeriodicCompoundInterest, calcApr, BigNumber, fromWei, } from "../utils";
import { ethers } from "ethers";
import set from "lodash/set";
import get from "lodash/get";
import has from "lodash/has";
import { calculateInstantaneousRate } from "../lpFeeCalculator";
import { hubPool, acrossConfigStore } from "../contracts";
import { AcceleratingDistributor__factory, MerkleDistributor__factory, } from "../typechain";
var erc20 = uma.clients.erc20;
var loop = uma.utils.loop;
var TransactionManager = uma.across.TransactionManager;
var _a = uma.across.constants, SECONDS_PER_YEAR = _a.SECONDS_PER_YEAR, DEFAULT_BLOCK_DELTA = _a.DEFAULT_BLOCK_DELTA;
var AddressZero = ethers.constants.AddressZero;
var PoolState = /** @class */ (function () {
    function PoolState(contract, address) {
        this.contract = contract;
        this.address = address;
    }
    PoolState.prototype.read = function (l1Token, latestBlock, previousBlock) {
        return __awaiter(this, void 0, void 0, function () {
            var exchangeRatePrevious, exchangeRateCurrent, pooledToken, liquidityUtilizationCurrent;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.exchangeRateAtBlock(l1Token, previousBlock || latestBlock - 1)];
                    case 1:
                        exchangeRatePrevious = _a.sent();
                        return [4 /*yield*/, this.contract.callStatic.exchangeRateCurrent(l1Token)];
                    case 2:
                        exchangeRateCurrent = _a.sent();
                        return [4 /*yield*/, this.contract.pooledTokens(l1Token)];
                    case 3:
                        pooledToken = _a.sent();
                        return [4 /*yield*/, this.contract.callStatic.liquidityUtilizationCurrent(l1Token)];
                    case 4:
                        liquidityUtilizationCurrent = _a.sent();
                        return [2 /*return*/, __assign({ address: this.address, l1Token: l1Token, latestBlock: latestBlock, previousBlock: previousBlock, exchangeRatePrevious: exchangeRatePrevious, exchangeRateCurrent: exchangeRateCurrent, liquidityUtilizationCurrent: liquidityUtilizationCurrent }, pooledToken)];
                }
            });
        });
    };
    PoolState.prototype.exchangeRateAtBlock = function (l1Token, blockTag) {
        return this.contract.callStatic.exchangeRateCurrent(l1Token, { blockTag: blockTag });
    };
    return PoolState;
}());
var PoolEventState = /** @class */ (function () {
    function PoolEventState(contract, startBlock) {
        var _this = this;
        if (startBlock === void 0) { startBlock = 0; }
        this.contract = contract;
        this.startBlock = startBlock;
        this.seen = new Set();
        // maintain ordered unique list of events so we can calculate state
        this.events = [];
        this.makeId = function (params) {
            return uma.oracle.utils.eventKey(params);
        };
        this.filterSeen = function (params) {
            var seen = _this.hasEvent(params);
            if (!seen)
                _this.addEvent(params);
            return !seen;
        };
        this.processEvent = function (event) {
            if (!_this.filterSeen(event))
                return;
            _this.events = uma.oracle.utils.insertOrderedAscending(_this.events, event, _this.makeId);
        };
        this.processEvents = function (events) {
            events.forEach(_this.processEvent);
        };
        this.makeEventFromLog = function (log) {
            var description = _this.iface.parseLog(log);
            return __assign(__assign(__assign({}, log), description), { event: description.name, eventSignature: description.signature });
        };
        this.iface = new ethers.utils.Interface(hubPool.Factory.abi);
    }
    PoolEventState.prototype.hasEvent = function (params) {
        return this.seen.has(this.makeId(params));
    };
    PoolEventState.prototype.addEvent = function (params) {
        this.seen.add(this.makeId(params));
    };
    PoolEventState.prototype.read = function (endBlock, l1TokenAddress, userAddress) {
        return __awaiter(this, void 0, void 0, function () {
            var events, _a, _b, _c, _d;
            return __generator(this, function (_e) {
                switch (_e.label) {
                    case 0:
                        _b = (_a = Promise).all;
                        _c = [[]];
                        return [4 /*yield*/, this.contract.queryFilter(this.contract.filters.LiquidityAdded(l1TokenAddress, undefined, undefined, userAddress), this.startBlock, endBlock)];
                    case 1:
                        _d = [__spreadArray.apply(void 0, _c.concat([(_e.sent()), true]))];
                        return [4 /*yield*/, this.contract.queryFilter(this.contract.filters.LiquidityRemoved(l1TokenAddress, undefined, undefined, userAddress), this.startBlock, endBlock)];
                    case 2: return [4 /*yield*/, _b.apply(_a, [__spreadArray.apply(void 0, _d.concat([(_e.sent()), true]))])];
                    case 3:
                        events = _e.sent();
                        this.processEvents(events);
                        return [2 /*return*/, hubPool.getEventState(this.events)];
                }
            });
        });
    };
    PoolEventState.prototype.getL1TokenFromReceipt = function (receipt) {
        var _this = this;
        var events = receipt.logs
            .filter(function (log) { return ethers.utils.getAddress(log.address) === ethers.utils.getAddress(_this.contract.address); })
            .map(this.makeEventFromLog);
        // save these events
        this.processEvents(events);
        // only process token receipt events, becasue we just want the l1 token involved with this transfer
        var eventState = hubPool.getEventState(events);
        // event state is keyed by l1token address
        var l1Tokens = Object.keys(eventState);
        assert(l1Tokens.length, "Token not found from events");
        assert(l1Tokens.length === 1, "Multiple tokens found from events");
        return l1Tokens[0];
    };
    PoolEventState.prototype.readTxReceipt = function (receipt) {
        var _this = this;
        var events = receipt.logs
            .filter(function (log) { return ethers.utils.getAddress(log.address) === ethers.utils.getAddress(_this.contract.address); })
            .map(this.makeEventFromLog);
        this.processEvents(events);
        return hubPool.getEventState(this.events);
    };
    return PoolEventState;
}());
export { PoolEventState };
var UserState = /** @class */ (function () {
    function UserState(contract, userAddress, startBlock, acceleratingDistributorContractAddress) {
        var _this = this;
        if (startBlock === void 0) { startBlock = 0; }
        if (acceleratingDistributorContractAddress === void 0) { acceleratingDistributorContractAddress = ""; }
        this.contract = contract;
        this.userAddress = userAddress;
        this.startBlock = startBlock;
        this.acceleratingDistributorContractAddress = acceleratingDistributorContractAddress;
        this.seen = new Set();
        this.events = [];
        this.filterSeen = function (params) {
            var seen = _this.hasEvent(params);
            if (!seen)
                _this.addEvent(params);
            return !seen;
        };
    }
    UserState.prototype.makeId = function (params) {
        return uma.oracle.utils.eventKey(params);
    };
    UserState.prototype.hasEvent = function (params) {
        return this.seen.has(this.makeId(params));
    };
    UserState.prototype.addEvent = function (params) {
        this.seen.add(this.makeId(params));
    };
    /**
     * readEvents. Fetch and cache events for the user.
     *
     * @param {number} endBlock
     */
    UserState.prototype.readEvents = function (endBlock) {
        return __awaiter(this, void 0, void 0, function () {
            var userAddress, events, _a, _b, _c, _d;
            var _this = this;
            return __generator(this, function (_e) {
                switch (_e.label) {
                    case 0:
                        if (endBlock <= this.startBlock)
                            return [2 /*return*/, []];
                        userAddress = this.userAddress;
                        _b = (_a = Promise).all;
                        _c = [[]];
                        return [4 /*yield*/, this.contract.queryFilter(this.contract.filters.Transfer(userAddress, undefined), this.startBlock, endBlock)];
                    case 1:
                        _d = [__spreadArray.apply(void 0, _c.concat([(_e.sent()), true]))];
                        return [4 /*yield*/, this.contract.queryFilter(this.contract.filters.Transfer(undefined, userAddress), this.startBlock, endBlock)];
                    case 2: return [4 /*yield*/, _b.apply(_a, [__spreadArray.apply(void 0, _d.concat([(_e.sent()), true]))])];
                    case 3:
                        events = (_e.sent())
                            // filter out events we have seen
                            .filter(this.filterSeen)
                            // filter out mint/burn transfers
                            .filter(function (event) {
                            // ignore mint events
                            return event.args.from !== AddressZero &&
                                // ignore burn events
                                event.args.to !== AddressZero &&
                                // ignore AD transfer events in
                                event.args.to !== _this.acceleratingDistributorContractAddress &&
                                // ignore AD transfer events out
                                event.args.from !== _this.acceleratingDistributorContractAddress &&
                                // ignore self transfer events
                                event.args.from !== event.args.to;
                        })
                            .flat();
                        this.events = this.events.concat(events).sort(function (a, b) {
                            if (a.blockNumber !== b.blockNumber)
                                return a.blockNumber - b.blockNumber;
                            if (a.transactionIndex !== b.transactionIndex)
                                return a.transactionIndex - b.transactionIndex;
                            if (a.logIndex !== b.logIndex)
                                return a.logIndex - b.logIndex;
                            throw new Error("Duplicate events at tx hash: " + a.transactionHash);
                        });
                        // ethers queries are inclusive [start,end] unless start === end, then exclusive (start,end). we increment to make sure we dont see same event twice
                        this.startBlock = endBlock + 1;
                        return [2 /*return*/, this.events];
                }
            });
        });
    };
    /**
     * read. Reads the state for the user, building state from events as well as contract calls.
     *
     * @param {number} endBlock
     */
    UserState.prototype.read = function (endBlock) {
        var _a;
        return __awaiter(this, void 0, void 0, function () {
            var userAddress, transferEvents, state, balanceTransferred;
            var _b;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        userAddress = this.userAddress;
                        return [4 /*yield*/, this.readEvents(endBlock)];
                    case 1:
                        transferEvents = _c.sent();
                        state = uma.clients.erc20.getEventState(transferEvents);
                        balanceTransferred = ((_a = state === null || state === void 0 ? void 0 : state.balances) === null || _a === void 0 ? void 0 : _a[userAddress]) || "0";
                        _b = {
                            transferEvents: transferEvents,
                            balanceTransferred: balanceTransferred,
                            address: userAddress
                        };
                        return [4 /*yield*/, this.contract.balanceOf(userAddress)];
                    case 2: return [2 /*return*/, (_b.balanceOf = _c.sent(),
                            _b)];
                }
            });
        });
    };
    return UserState;
}());
export function calculateRemoval(amountWei, percentWei) {
    var receive = amountWei.mul(percentWei).div(fixedPointAdjustment);
    var remain = amountWei.sub(receive);
    return {
        receive: receive.toString(),
        remain: remain.toString(),
    };
}
// params here mimic the user object type
export function previewRemoval(values, percentFloat) {
    var percentWei = toBNWei(percentFloat);
    return {
        position: __assign({}, calculateRemoval(BigNumber.from(values.totalDeposited), percentWei)),
        fees: __assign({}, calculateRemoval(BigNumber.from(values.feesEarned), percentWei)),
        total: __assign({}, calculateRemoval(BigNumber.from(values.positionValue), percentWei)),
    };
}
function joinUserState(poolState, tokenEventState, userState, transferValue, cumulativeStakeBalance, cumulativeStakeClaimBalance) {
    if (transferValue === void 0) { transferValue = bnZero; }
    if (cumulativeStakeBalance === void 0) { cumulativeStakeBalance = bnZero; }
    if (cumulativeStakeClaimBalance === void 0) { cumulativeStakeClaimBalance = bnZero; }
    var positionValue = BigNumber.from(poolState.exchangeRateCurrent)
        .mul(userState.balanceOf.add(cumulativeStakeBalance))
        .div(fixedPointAdjustment);
    var totalDeposited = BigNumber.from((tokenEventState === null || tokenEventState === void 0 ? void 0 : tokenEventState.tokenBalances[userState.address]) || "0").add(cumulativeStakeClaimBalance);
    var feesEarned = positionValue.sub(totalDeposited.add(transferValue));
    return {
        address: userState.address,
        poolAddress: poolState.address,
        lpTokens: userState.balanceOf.toString(),
        positionValue: positionValue.toString(),
        totalDeposited: totalDeposited.toString(),
        feesEarned: feesEarned.toString(),
    };
}
function joinPoolState(poolState, latestBlock, previousBlock, rateModel) {
    var totalPoolSize = poolState.liquidReserves.add(poolState.utilizedReserves);
    var secondsElapsed = latestBlock.timestamp - previousBlock.timestamp;
    var blocksElapsed = latestBlock.number - previousBlock.number;
    var exchangeRatePrevious = poolState.exchangeRatePrevious.toString();
    var exchangeRateCurrent = poolState.exchangeRateCurrent.toString();
    var liquidityUtilizationCurrent = poolState.liquidityUtilizationCurrent.toString();
    var estimatedApy = calcPeriodicCompoundInterest(exchangeRatePrevious, exchangeRateCurrent, secondsElapsed, SECONDS_PER_YEAR);
    var estimatedApr = calcApr(exchangeRatePrevious, exchangeRateCurrent, secondsElapsed, SECONDS_PER_YEAR);
    var projectedApr = "";
    if (rateModel) {
        projectedApr = fromWei(calculateInstantaneousRate(rateModel, liquidityUtilizationCurrent)
            .mul(liquidityUtilizationCurrent)
            .div(fixedPointAdjustment));
    }
    return {
        address: poolState.address,
        totalPoolSize: totalPoolSize.toString(),
        l1Token: poolState.l1Token,
        lpToken: poolState.lpToken,
        liquidReserves: poolState.liquidReserves.toString(),
        exchangeRateCurrent: poolState.exchangeRateCurrent.toString(),
        exchangeRatePrevious: poolState.exchangeRatePrevious.toString(),
        estimatedApy: estimatedApy,
        estimatedApr: estimatedApr,
        blocksElapsed: blocksElapsed,
        secondsElapsed: secondsElapsed,
        projectedApr: projectedApr,
        utilizedReserves: poolState.utilizedReserves.toString(),
        liquidityUtilizationCurrent: liquidityUtilizationCurrent,
    };
}
var ReadPoolClient = /** @class */ (function () {
    function ReadPoolClient(address, provider) {
        this.address = address;
        this.provider = provider;
        this.contract = hubPool.connect(address, this.provider);
        this.poolState = new PoolState(this.contract, this.address);
    }
    ReadPoolClient.prototype.read = function (tokenAddress, latestBlock) {
        return this.poolState.read(tokenAddress, latestBlock);
    };
    return ReadPoolClient;
}());
export { ReadPoolClient };
export function validateWithdraw(pool, user, lpTokenAmount) {
    var l1TokensToReturn = BigNumber.from(lpTokenAmount).mul(pool.exchangeRateCurrent).div(fixedPointAdjustment);
    assert(BigNumber.from(l1TokensToReturn).gt("0"), "Must withdraw amount greater than 0");
    assert(BigNumber.from(lpTokenAmount).lte(user.lpTokens), "You cannot withdraw more than you have");
    return { lpTokenAmount: lpTokenAmount, l1TokensToReturn: l1TokensToReturn.toString() };
}
var Client = /** @class */ (function () {
    function Client(config, deps, emit) {
        this.config = config;
        this.deps = deps;
        this.emit = emit;
        this.transactionManagers = {};
        this.state = { pools: {}, users: {}, transactions: {} };
        this.erc20s = {};
        this.intervalStarted = false;
        this.exchangeRateTable = {};
        this.userServices = {};
        config.chainId = config.chainId || 1;
        this.hubPool = this.createHubPoolContract(deps.provider);
        this.acceleratingDistributor = this.createAcceleratingDistributorContract(deps.provider);
        this.merkleDistributor = this.createMerkleDistributorContract(deps.provider);
        this.poolEvents = new PoolEventState(this.hubPool, this.config.hubPoolStartBlock);
        this.configStoreClient = new acrossConfigStore.Client(config.configStoreAddress, deps.provider);
    }
    Client.prototype.getOrCreateErc20Contract = function (address) {
        if (this.erc20s[address])
            return this.erc20s[address];
        this.erc20s[address] = erc20.connect(address, this.deps.provider);
        return this.erc20s[address];
    };
    Client.prototype.getOrCreatePoolContract = function () {
        return this.hubPool;
    };
    Client.prototype.createHubPoolContract = function (signerOrProvider) {
        return hubPool.connect(this.config.hubPoolAddress, signerOrProvider);
    };
    Client.prototype.getOrCreatePoolEvents = function () {
        return this.poolEvents;
    };
    Client.prototype.createAcceleratingDistributorContract = function (signerOrProvider) {
        return AcceleratingDistributor__factory.connect(this.config.acceleratingDistributorAddress, signerOrProvider);
    };
    Client.prototype.createMerkleDistributorContract = function (signerOrProvider) {
        return MerkleDistributor__factory.connect(this.config.merkleDistributorAddress, signerOrProvider);
    };
    Client.prototype.getOrCreateAcceleratingDistributorContract = function () {
        return this.acceleratingDistributor;
    };
    Client.prototype.getOrCreateMerkleDistributorContract = function () {
        return this.merkleDistributor;
    };
    Client.prototype.getOrCreateUserService = function (userAddress, tokenAddress) {
        if (has(this.userServices, [tokenAddress, userAddress]))
            return get(this.userServices, [tokenAddress, userAddress]);
        var erc20Contract = this.getOrCreateErc20Contract(tokenAddress);
        var userService = new UserState(erc20Contract, userAddress);
        // this service is stateful now, so needs to be cached
        set(this.userServices, [tokenAddress, userAddress], userService);
        return userService;
    };
    Client.prototype.updateExchangeRateTable = function (l1TokenAddress, exchangeRateTable) {
        if (!this.exchangeRateTable[l1TokenAddress])
            this.exchangeRateTable[l1TokenAddress] = {};
        this.exchangeRateTable[l1TokenAddress] = __assign(__assign({}, this.exchangeRateTable[l1TokenAddress]), exchangeRateTable);
        return this.exchangeRateTable[l1TokenAddress];
    };
    Client.prototype.resolveStakingData = function (lpToken, l1TokenAddress, userState) {
        return __awaiter(this, void 0, void 0, function () {
            var acceleratingDistributorContract, merkleDistributorContract, poolContract, claimList, amountOfLPClaimed, cumulativeBalance;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        assert(this.config.acceleratingDistributorAddress, "Must have the accelerating distributor address");
                        assert(this.config.merkleDistributorAddress, "Must have the merkle distributor address");
                        acceleratingDistributorContract = this.getOrCreateAcceleratingDistributorContract();
                        merkleDistributorContract = this.getOrCreateMerkleDistributorContract();
                        poolContract = this.getOrCreatePoolContract();
                        return [4 /*yield*/, merkleDistributorContract.queryFilter(merkleDistributorContract.filters.Claimed(undefined, undefined, userState.address, undefined, undefined, lpToken))];
                    case 1:
                        claimList = _a.sent();
                        return [4 /*yield*/, Promise.all(claimList.map(function (claim) { return __awaiter(_this, void 0, void 0, function () {
                                var _a, _b;
                                return __generator(this, function (_c) {
                                    switch (_c.label) {
                                        case 0:
                                            _b = (_a = claim.args.amount).mul;
                                            return [4 /*yield*/, poolContract.callStatic.exchangeRateCurrent(l1TokenAddress, { blockTag: claim.blockNumber })];
                                        case 1: return [2 /*return*/, _b.apply(_a, [_c.sent()])];
                                    }
                                });
                            }); }))];
                    case 2:
                        amountOfLPClaimed = (_a.sent()).reduce(function (prev, acc) { return acc.add(prev); }, bnZero);
                        return [4 /*yield*/, acceleratingDistributorContract.getUserStake(lpToken, userState.address)];
                    case 3:
                        cumulativeBalance = (_a.sent()).cumulativeBalance;
                        return [2 /*return*/, {
                                cumulativeBalance: cumulativeBalance,
                                amountAirdropped: amountOfLPClaimed,
                            }];
                }
            });
        });
    };
    // calculates the value of each LP token transfer at the block it was sent. this only works if we have archive node
    Client.prototype.calculateLpTransferValue = function (l1TokenAddress, userState) {
        return __awaiter(this, void 0, void 0, function () {
            var contract, pool, blockNumbers, exchangeRateTable, _a, _b, _c, _d;
            var _this = this;
            return __generator(this, function (_e) {
                switch (_e.label) {
                    case 0:
                        assert(this.config.hasArchive, "Can only calculate historical lp values with archive node");
                        contract = this.getOrCreatePoolContract();
                        pool = new PoolState(contract, this.config.hubPoolAddress);
                        blockNumbers = userState.transferEvents
                            .map(function (x) { return x.blockNumber; })
                            // we are going to lookup exchange rates for block numbers only if we dont already have it
                            // its possible these values do not exist, so to prevent crashing do optional chaining
                            .filter(function (blockNumber) { var _a, _b; return !((_b = (_a = _this.exchangeRateTable) === null || _a === void 0 ? void 0 : _a[l1TokenAddress]) === null || _b === void 0 ? void 0 : _b[blockNumber]); });
                        _a = this.updateExchangeRateTable;
                        _b = [l1TokenAddress];
                        _d = (_c = Object).fromEntries;
                        return [4 /*yield*/, Promise.all(blockNumbers.map(function (blockNumber) { return __awaiter(_this, void 0, void 0, function () {
                                var _a;
                                return __generator(this, function (_b) {
                                    switch (_b.label) {
                                        case 0:
                                            _a = [blockNumber];
                                            return [4 /*yield*/, pool.exchangeRateAtBlock(l1TokenAddress, blockNumber)];
                                        case 1: return [2 /*return*/, _a.concat([_b.sent()])];
                                    }
                                });
                            }); }))];
                    case 1:
                        exchangeRateTable = _a.apply(this, _b.concat([_d.apply(_c, [_e.sent()])]));
                        return [2 /*return*/, userState.transferEvents.reduce(function (result, transfer) {
                                var exchangeRate = exchangeRateTable[transfer.blockNumber];
                                if (transfer.args.to === userState.address) {
                                    return result.add(transfer.args.value.mul(exchangeRate).div(fixedPointAdjustment));
                                }
                                if (transfer.args.from === userState.address) {
                                    return result.sub(transfer.args.value.mul(exchangeRate).div(fixedPointAdjustment));
                                }
                                // we make sure to filter out any transfers where to/from is the same user
                                return result;
                            }, bnZero)];
                }
            });
        });
    };
    Client.prototype.getOrCreateTransactionManager = function (signer, address) {
        var _this = this;
        if (this.transactionManagers[address])
            return this.transactionManagers[address];
        var txman = TransactionManager({ confirmations: this.config.confirmations }, signer, function (event, id, data) {
            if (event === "submitted") {
                _this.state.transactions[id].state = event;
                _this.state.transactions[id].hash = data;
                _this.emit(["transactions", id], __assign({}, _this.state.transactions[id]));
            }
            if (event === "mined") {
                var txReceipt = data;
                _this.state.transactions[id].state = event;
                _this.state.transactions[id].receipt = txReceipt;
                _this.emit(["transactions", id], __assign({}, _this.state.transactions[id]));
                // trigger pool and user update for a known mined transaction
                var tx = _this.state.transactions[id];
                _this.updateUserWithTransaction(tx.fromAddress, txReceipt).catch(function (err) {
                    _this.emit(["error"], err);
                });
            }
            if (event === "error") {
                _this.state.transactions[id].state = event;
                _this.state.transactions[id].error = data;
                _this.emit(["transactions", id], __assign({}, _this.state.transactions[id]));
            }
        });
        this.transactionManagers[address] = txman;
        return txman;
    };
    Client.prototype.addEthLiquidity = function (signer, l1TokenAmount, overrides) {
        if (overrides === void 0) { overrides = {}; }
        return __awaiter(this, void 0, void 0, function () {
            var _a, hubPoolAddress, l1Token, userAddress, contract, txman, request, id;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _a = this.config, hubPoolAddress = _a.hubPoolAddress, l1Token = _a.wethAddress;
                        return [4 /*yield*/, signer.getAddress()];
                    case 1:
                        userAddress = _b.sent();
                        contract = this.getOrCreatePoolContract();
                        txman = this.getOrCreateTransactionManager(signer, userAddress);
                        return [4 /*yield*/, contract.populateTransaction.addLiquidity(l1Token, l1TokenAmount, __assign(__assign({}, overrides), { value: l1TokenAmount }))];
                    case 2:
                        request = _b.sent();
                        id = txman.request(request);
                        this.state.transactions[id] = {
                            id: id,
                            state: "requested",
                            toAddress: hubPoolAddress,
                            fromAddress: userAddress,
                            type: "Add Liquidity",
                            description: "Adding ETH to pool",
                            request: request,
                        };
                        this.emit(["transactions", id], __assign({}, this.state.transactions[id]));
                        return [4 /*yield*/, txman.update()];
                    case 3:
                        _b.sent();
                        return [2 /*return*/, id];
                }
            });
        });
    };
    Client.prototype.addTokenLiquidity = function (signer, l1Token, l1TokenAmount, overrides) {
        if (overrides === void 0) { overrides = {}; }
        return __awaiter(this, void 0, void 0, function () {
            var hubPoolAddress, userAddress, contract, txman, request, id;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        hubPoolAddress = this.config.hubPoolAddress;
                        return [4 /*yield*/, signer.getAddress()];
                    case 1:
                        userAddress = _a.sent();
                        contract = this.getOrCreatePoolContract();
                        txman = this.getOrCreateTransactionManager(signer, userAddress);
                        return [4 /*yield*/, contract.populateTransaction.addLiquidity(l1Token, l1TokenAmount, overrides)];
                    case 2:
                        request = _a.sent();
                        return [4 /*yield*/, txman.request(request)];
                    case 3:
                        id = _a.sent();
                        this.state.transactions[id] = {
                            id: id,
                            state: "requested",
                            toAddress: hubPoolAddress,
                            fromAddress: userAddress,
                            type: "Add Liquidity",
                            description: "Adding Tokens to pool",
                            request: request,
                        };
                        this.emit(["transactions", id], __assign({}, this.state.transactions[id]));
                        return [4 /*yield*/, txman.update()];
                    case 4:
                        _a.sent();
                        return [2 /*return*/, id];
                }
            });
        });
    };
    Client.prototype.validateWithdraw = function (l1Token, userAddress, lpAmount) {
        return __awaiter(this, void 0, void 0, function () {
            var poolState, userState;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.updatePool(l1Token)];
                    case 1:
                        _a.sent();
                        poolState = this.getPoolState(l1Token);
                        if (!!this.hasUserState(l1Token, userAddress)) return [3 /*break*/, 3];
                        return [4 /*yield*/, this.updateUser(l1Token, userAddress)];
                    case 2:
                        _a.sent();
                        _a.label = 3;
                    case 3:
                        userState = this.getUserState(poolState.l1Token, userAddress);
                        return [2 /*return*/, validateWithdraw(poolState, userState, lpAmount)];
                }
            });
        });
    };
    Client.prototype.removeTokenLiquidity = function (signer, l1Token, lpTokenAmount, overrides) {
        if (overrides === void 0) { overrides = {}; }
        return __awaiter(this, void 0, void 0, function () {
            var hubPoolAddress, userAddress, contract, txman, request, id;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        hubPoolAddress = this.config.hubPoolAddress;
                        return [4 /*yield*/, signer.getAddress()];
                    case 1:
                        userAddress = _a.sent();
                        return [4 /*yield*/, this.validateWithdraw(l1Token, userAddress, lpTokenAmount)];
                    case 2:
                        _a.sent();
                        contract = this.getOrCreatePoolContract();
                        txman = this.getOrCreateTransactionManager(signer, userAddress);
                        return [4 /*yield*/, contract.populateTransaction.removeLiquidity(l1Token, lpTokenAmount, false, overrides)];
                    case 3:
                        request = _a.sent();
                        return [4 /*yield*/, txman.request(request)];
                    case 4:
                        id = _a.sent();
                        this.state.transactions[id] = {
                            id: id,
                            state: "requested",
                            toAddress: hubPoolAddress,
                            fromAddress: userAddress,
                            type: "Remove Liquidity",
                            description: "Withdrawing Tokens from pool",
                            request: request,
                        };
                        this.emit(["transactions", id], __assign({}, this.state.transactions[id]));
                        return [4 /*yield*/, txman.update()];
                    case 5:
                        _a.sent();
                        return [2 /*return*/, id];
                }
            });
        });
    };
    Client.prototype.removeEthliquidity = function (signer, lpTokenAmount, overrides) {
        if (overrides === void 0) { overrides = {}; }
        return __awaiter(this, void 0, void 0, function () {
            var _a, hubPoolAddress, l1Token, userAddress, contract, txman, request, id;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _a = this.config, hubPoolAddress = _a.hubPoolAddress, l1Token = _a.wethAddress;
                        return [4 /*yield*/, signer.getAddress()];
                    case 1:
                        userAddress = _b.sent();
                        return [4 /*yield*/, this.validateWithdraw(l1Token, userAddress, lpTokenAmount)];
                    case 2:
                        _b.sent();
                        contract = this.getOrCreatePoolContract();
                        txman = this.getOrCreateTransactionManager(signer, userAddress);
                        return [4 /*yield*/, contract.populateTransaction.removeLiquidity(l1Token, lpTokenAmount, true, overrides)];
                    case 3:
                        request = _b.sent();
                        return [4 /*yield*/, txman.request(request)];
                    case 4:
                        id = _b.sent();
                        this.state.transactions[id] = {
                            id: id,
                            state: "requested",
                            toAddress: hubPoolAddress,
                            fromAddress: userAddress,
                            type: "Remove Liquidity",
                            description: "Withdrawing Eth from pool",
                            request: request,
                        };
                        this.emit(["transactions", id], __assign({}, this.state.transactions[id]));
                        return [4 /*yield*/, txman.update()];
                    case 5:
                        _b.sent();
                        return [2 /*return*/, id];
                }
            });
        });
    };
    Client.prototype.getPoolState = function (l1TokenAddress) {
        return this.state.pools[l1TokenAddress];
    };
    Client.prototype.hasPoolState = function (l1TokenAddress) {
        return Boolean(this.state.pools[l1TokenAddress]);
    };
    Client.prototype.setUserState = function (l1TokenAddress, userAddress, state) {
        set(this.state, ["users", userAddress, l1TokenAddress], state);
        return state;
    };
    Client.prototype.getUserState = function (l1TokenAddress, userAddress) {
        return get(this.state, ["users", userAddress, l1TokenAddress]);
    };
    Client.prototype.hasUserState = function (l1TokenAddress, userAddress) {
        return has(this.state, ["users", userAddress, l1TokenAddress]);
    };
    Client.prototype.hasTxState = function (id) {
        return has(this.state, ["transactions", id]);
    };
    Client.prototype.getTxState = function (id) {
        return get(this.state, ["transactions", id]);
    };
    Client.prototype.updateAndEmitUser = function (userState, poolState, poolEventState) {
        return __awaiter(this, void 0, void 0, function () {
            var l1TokenAddress, lpToken, userAddress, transferValue, _a, stakeData, tokenEventState, newUserState;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        l1TokenAddress = poolState.l1Token, lpToken = poolState.lpToken;
                        userAddress = userState.address;
                        if (!this.config.hasArchive) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.calculateLpTransferValue(l1TokenAddress, userState)];
                    case 1:
                        _a = _b.sent();
                        return [3 /*break*/, 3];
                    case 2:
                        _a = bnZero;
                        _b.label = 3;
                    case 3:
                        transferValue = _a;
                        return [4 /*yield*/, this.resolveStakingData(lpToken, l1TokenAddress, userState)];
                    case 4:
                        stakeData = _b.sent();
                        tokenEventState = poolEventState[l1TokenAddress];
                        newUserState = this.setUserState(l1TokenAddress, userAddress, joinUserState(poolState, tokenEventState, userState, transferValue, stakeData.cumulativeBalance, stakeData.amountAirdropped));
                        this.emit(["users", userAddress, l1TokenAddress], newUserState);
                        return [2 /*return*/];
                }
            });
        });
    };
    Client.prototype.updateUserWithTransaction = function (userAddress, txReceipt) {
        return __awaiter(this, void 0, void 0, function () {
            var latestBlock, getPoolEventState, l1TokenAddress, poolState, poolEventState, lpToken, getUserState, userState;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.deps.provider.getBlock("latest")];
                    case 1:
                        latestBlock = _a.sent();
                        getPoolEventState = this.getOrCreatePoolEvents();
                        l1TokenAddress = getPoolEventState.getL1TokenFromReceipt(txReceipt);
                        return [4 /*yield*/, this.updatePool(l1TokenAddress, latestBlock)];
                    case 2:
                        _a.sent();
                        poolState = this.getPoolState(l1TokenAddress);
                        poolEventState = getPoolEventState.readTxReceipt(txReceipt);
                        lpToken = poolState.lpToken;
                        getUserState = this.getOrCreateUserService(userAddress, lpToken);
                        return [4 /*yield*/, getUserState.read(latestBlock.number)];
                    case 3:
                        userState = _a.sent();
                        return [4 /*yield*/, this.updateAndEmitUser(userState, poolState, poolEventState)];
                    case 4:
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    Client.prototype.updateUser = function (userAddress, l1TokenAddress) {
        return __awaiter(this, void 0, void 0, function () {
            var latestBlock, poolState, lpToken, getPoolEventState, poolEventState, getUserState, userState;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.deps.provider.getBlock("latest")];
                    case 1:
                        latestBlock = _a.sent();
                        return [4 /*yield*/, this.updatePool(l1TokenAddress, latestBlock)];
                    case 2:
                        _a.sent();
                        poolState = this.getPoolState(l1TokenAddress);
                        lpToken = poolState.lpToken;
                        getPoolEventState = this.getOrCreatePoolEvents();
                        return [4 /*yield*/, getPoolEventState.read(latestBlock.number, l1TokenAddress, userAddress)];
                    case 3:
                        poolEventState = _a.sent();
                        getUserState = this.getOrCreateUserService(userAddress, lpToken);
                        return [4 /*yield*/, getUserState.read(latestBlock.number)];
                    case 4:
                        userState = _a.sent();
                        return [4 /*yield*/, this.updateAndEmitUser(userState, poolState, poolEventState)];
                    case 5:
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    Client.prototype.updatePool = function (l1TokenAddress, overrideLatestBlock) {
        return __awaiter(this, void 0, void 0, function () {
            var _a, blockDelta, contract, pool, latestBlock, _b, previousBlock, state, rateModel, err_1;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        _a = this.config.blockDelta, blockDelta = _a === void 0 ? DEFAULT_BLOCK_DELTA : _a;
                        contract = this.getOrCreatePoolContract();
                        pool = new PoolState(contract, this.config.hubPoolAddress);
                        _b = overrideLatestBlock;
                        if (_b) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.deps.provider.getBlock("latest")];
                    case 1:
                        _b = (_c.sent());
                        _c.label = 2;
                    case 2:
                        latestBlock = _b;
                        return [4 /*yield*/, this.deps.provider.getBlock(latestBlock.number - blockDelta)];
                    case 3:
                        previousBlock = _c.sent();
                        return [4 /*yield*/, pool.read(l1TokenAddress, latestBlock.number, previousBlock.number)];
                    case 4:
                        state = _c.sent();
                        rateModel = undefined;
                        _c.label = 5;
                    case 5:
                        _c.trys.push([5, 7, , 8]);
                        return [4 /*yield*/, this.configStoreClient.getRateModel(l1TokenAddress)];
                    case 6:
                        // Use the default rate model (i.e. not any of the routeRateModels to project the Pool's APR). This assumes
                        // that the default rate model is the most often used, but this may change in future if many different
                        // route rate models are set.
                        rateModel = _c.sent();
                        return [3 /*break*/, 8];
                    case 7:
                        err_1 = _c.sent();
                        // we could swallow this error or just log it since getting the rate model is optional,
                        // but we will just emit it to the caller and let them decide what to do with it.
                        this.emit(["error"], err_1);
                        return [3 /*break*/, 8];
                    case 8:
                        this.state.pools[l1TokenAddress] = joinPoolState(state, latestBlock, previousBlock, rateModel);
                        this.emit(["pools", l1TokenAddress], this.state.pools[l1TokenAddress]);
                        return [2 /*return*/];
                }
            });
        });
    };
    Client.prototype.updateTransactions = function () {
        return __awaiter(this, void 0, void 0, function () {
            var _i, _a, txMan, err_2;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _i = 0, _a = Object.values(this.transactionManagers);
                        _b.label = 1;
                    case 1:
                        if (!(_i < _a.length)) return [3 /*break*/, 6];
                        txMan = _a[_i];
                        _b.label = 2;
                    case 2:
                        _b.trys.push([2, 4, , 5]);
                        return [4 /*yield*/, txMan.update()];
                    case 3:
                        _b.sent();
                        return [3 /*break*/, 5];
                    case 4:
                        err_2 = _b.sent();
                        this.emit(["error"], err_2);
                        return [3 /*break*/, 5];
                    case 5:
                        _i++;
                        return [3 /*break*/, 1];
                    case 6: return [2 /*return*/];
                }
            });
        });
    };
    // starts transaction checking intervals, defaults to 30 seconds
    Client.prototype.startInterval = function (delayMs) {
        var _this = this;
        if (delayMs === void 0) { delayMs = 30000; }
        assert(!this.intervalStarted, "Interval already started, try stopping first");
        this.intervalStarted = true;
        loop(function () { return __awaiter(_this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        assert(this.intervalStarted, "HubPool Interval Stopped");
                        return [4 /*yield*/, this.updateTransactions()];
                    case 1:
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        }); }, delayMs).catch(function (err) {
            _this.emit(["error"], err);
        });
    };
    // starts transaction checking intervals
    Client.prototype.stopInterval = function () {
        this.intervalStarted = false;
    };
    return Client;
}());
export { Client };
//# sourceMappingURL=poolClient.js.map