"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultEscrowAPI = void 0;
const bn_js_1 = __importDefault(require("bn.js"));
const utils_1 = require("../utils");
const types_1 = require("../types");
class DefaultEscrowAPI {
    constructor(api, governanceCurrency, systemAPI, transactionAPI) {
        this.api = api;
        this.governanceCurrency = governanceCurrency;
        this.systemAPI = systemAPI;
        this.transactionAPI = transactionAPI;
    }
    createLock(amount, unlockHeight) {
        return __awaiter(this, void 0, void 0, function* () {
            const tx = this.api.tx.escrow.createLock(amount.toString(amount.currency.rawBase), unlockHeight);
            yield this.transactionAPI.sendLogged(tx, this.api.events.escrow.Deposit, true);
        });
    }
    withdraw() {
        return __awaiter(this, void 0, void 0, function* () {
            const tx = this.api.tx.escrow.withdraw();
            yield this.transactionAPI.sendLogged(tx, this.api.events.escrow.Withdraw, true);
        });
    }
    withdrawRewards() {
        return __awaiter(this, void 0, void 0, function* () {
            const tx = this.api.tx.escrowAnnuity.withdrawRewards();
            yield this.transactionAPI.sendLogged(tx, this.api.events.escrowRewards.WithdrawReward, true);
        });
    }
    increaseAmount(amount) {
        return __awaiter(this, void 0, void 0, function* () {
            const tx = this.api.tx.escrow.increaseAmount(amount.toString(amount.currency.rawBase));
            yield this.transactionAPI.sendLogged(tx, this.api.events.escrow.Deposit, true);
        });
    }
    increaseUnlockHeight(unlockHeight) {
        return __awaiter(this, void 0, void 0, function* () {
            const tx = this.api.tx.escrow.increaseUnlockHeight(unlockHeight);
            yield this.transactionAPI.sendLogged(tx, this.api.events.escrow.Deposit, true);
        });
    }
    getRewards(accountId) {
        return __awaiter(this, void 0, void 0, function* () {
            // Step 1. Get amount in reward pool for the account ID
            const [rewardStake, rewardPerToken, rewardTally] = yield Promise.all([
                this.getEscrowStake(accountId),
                this.getRewardPerToken(),
                this.getRewardTally(accountId),
            ]);
            // Step 2. Calculate the rewards that can be withdrawn at the moment
            // Stake[currencyId, accountId] * RewardPerToken[currencyId] - RewardTally[currencyId, accountId]
            const rewards = rewardStake.mul(rewardPerToken).sub(rewardTally);
            return utils_1.newMonetaryAmount(rewards, this.governanceCurrency);
        });
    }
    getRewardEstimate(accountId, amountToLock, blockLockTimeExtension) {
        return __awaiter(this, void 0, void 0, function* () {
            const [userStake, totalStake, blockReward, stakedBalance, currentBlockNumber, minimumBlockPeriod, maxPeriod] = yield Promise.all([
                this.getEscrowStake(accountId),
                this.getEscrowTotalStake(),
                this.getRewardPerBlock(),
                this.getStakedBalance(accountId),
                this.systemAPI.getCurrentBlockNumber(),
                this.api.consts.timestamp.minimumPeriod,
                this.getMaxPeriod(),
            ]);
            return utils_1.estimateReward(this.governanceCurrency, userStake, totalStake, blockReward, stakedBalance, currentBlockNumber, minimumBlockPeriod.toNumber(), maxPeriod.toNumber(), amountToLock, blockLockTimeExtension);
        });
    }
    getEscrowStake(accountId) {
        return __awaiter(this, void 0, void 0, function* () {
            const rawStake = yield this.api.query.escrowRewards.stake(accountId);
            return utils_1.decodeFixedPointType(rawStake);
        });
    }
    getEscrowTotalStake() {
        return __awaiter(this, void 0, void 0, function* () {
            const rawTotalStake = yield this.api.query.escrowRewards.totalStake();
            return utils_1.decodeFixedPointType(rawTotalStake);
        });
    }
    getRewardTally(accountId) {
        return __awaiter(this, void 0, void 0, function* () {
            const governanceCurrencyId = utils_1.newCurrencyId(this.api, types_1.tickerToCurrencyIdLiteral(this.governanceCurrency.ticker));
            const rawRewardTally = yield this.api.query.escrowRewards.rewardTally(governanceCurrencyId, accountId);
            return utils_1.decodeFixedPointType(rawRewardTally);
        });
    }
    getRewardPerToken() {
        return __awaiter(this, void 0, void 0, function* () {
            const governanceCurrencyId = utils_1.newCurrencyId(this.api, types_1.tickerToCurrencyIdLiteral(this.governanceCurrency.ticker));
            const rawRewardPerToken = yield this.api.query.escrowRewards.rewardPerToken(governanceCurrencyId);
            return utils_1.decodeFixedPointType(rawRewardPerToken);
        });
    }
    getRewardPerBlock() {
        return __awaiter(this, void 0, void 0, function* () {
            const rawRewardPerBlock = yield this.api.query.escrowAnnuity.rewardPerBlock();
            return utils_1.newMonetaryAmount(rawRewardPerBlock.toString(), this.governanceCurrency);
        });
    }
    getStakedBalance(accountId) {
        return __awaiter(this, void 0, void 0, function* () {
            const rawStakedBalance = yield this.api.query.escrow.locked(accountId);
            return types_1.parseEscrowLockedBalance(this.governanceCurrency, rawStakedBalance);
        });
    }
    getTotalStakedBalance() {
        return __awaiter(this, void 0, void 0, function* () {
            const govCcy = this.governanceCurrency;
            const rawStakedBalances = yield this.api.query.escrow.locked.entries();
            const totalCurrentAmount = rawStakedBalances
                .map(([_, rawStakedBalance]) => types_1.parseEscrowLockedBalance(govCcy, rawStakedBalance).amount)
                .reduce((acc, amount) => acc.add(amount), utils_1.newMonetaryAmount(0, govCcy));
            return totalCurrentAmount;
        });
    }
    votingBalance(accountId, blockNumber) {
        return __awaiter(this, void 0, void 0, function* () {
            const height = blockNumber || (yield this.systemAPI.getCurrentBlockNumber());
            const userPointEpoch = yield this.api.query.escrow.userPointEpoch(accountId);
            const lastPoint = yield this.api.query.escrow.userPointHistory(accountId, userPointEpoch);
            const rawBalance = this.rawBalanceAt(types_1.parseEscrowPoint(lastPoint), height);
            // `rawBalance.toString()` is used to convert the BN to a BigSource type
            return utils_1.newMonetaryAmount(rawBalance.toString(), utils_1.toVoting(this.governanceCurrency));
        });
    }
    /*
        Rust reference implementation:
        https://github.com/interlay/interbtc/blob/0302612ae5f8ddf1f556042ca347c6104704ad83/crates/escrow/src/lib.rs#L524
    */
    rawBalanceAt(escrowPoint, height) {
        const heightDiff = this.saturatingSub(new bn_js_1.default(height), escrowPoint.ts);
        return this.saturatingSub(escrowPoint.bias, escrowPoint.slope.mul(heightDiff));
    }
    totalVotingSupply(blockNumber) {
        return __awaiter(this, void 0, void 0, function* () {
            let block;
            let epoch;
            if (blockNumber) {
                block = new bn_js_1.default(blockNumber);
                const maxEpoch = yield this.api.query.escrow.epoch();
                epoch = yield this.findBlockEpoch(block, maxEpoch.toBn());
            }
            else {
                const [currentBlock, currentEpoch] = yield Promise.all([
                    this.systemAPI.getCurrentBlockNumber(),
                    this.api.query.escrow.epoch(),
                ]);
                block = new bn_js_1.default(currentBlock);
                epoch = currentEpoch.toBn();
            }
            return this.totalVotingSupplyAt(block, epoch);
        });
    }
    totalVotingSupplyAt(block, epoch) {
        return __awaiter(this, void 0, void 0, function* () {
            const [span, rawSlopeChanges] = yield Promise.all([
                this.getSpan(),
                this.api.query.escrow.slopeChanges.entries(),
            ]);
            const slopeChanges = new Map();
            rawSlopeChanges.forEach(([id, value]) => slopeChanges.set(utils_1.storageKeyToNthInner(id).toBn(), value.toBn()));
            const lastPoint = yield this.api.query.escrow.pointHistory(epoch);
            const rawSupply = this.rawSupplyAt(types_1.parseEscrowPoint(lastPoint), block, span, slopeChanges);
            // `rawSupply.toString()` is used to convert the BN to a BigSource type
            return utils_1.newMonetaryAmount(rawSupply.toString(), utils_1.toVoting(this.governanceCurrency));
        });
    }
    /*
        Vyper reference implementation:
        https://github.com/curvefi/curve-dao-contracts/blob/4e428823c8ae9c0f8a669d796006fade11edb141/contracts/VotingEscrow.vy#L502
    */
    findBlockEpoch(block, maxEpoch) {
        return __awaiter(this, void 0, void 0, function* () {
            let min = new bn_js_1.default(0);
            let max = maxEpoch;
            for (let i = 0; i < 128; i++) {
                if (min.gte(max)) {
                    break;
                }
                const mid = min.add(max).addn(1).divn(2);
                const point = types_1.parseEscrowPoint(yield this.api.query.escrow.pointHistory(mid));
                if (point.ts.lte(block)) {
                    min = mid;
                }
                else {
                    max = mid.subn(1);
                }
            }
            return min;
        });
    }
    /*
        Rust reference implementation:
        https://github.com/interlay/interbtc/blob/0302612ae5f8ddf1f556042ca347c6104704ad83/crates/escrow/src/lib.rs#L530
    */
    rawSupplyAt(escrowPoint, height, escrowSpan, slopeChanges) {
        const lastPoint = escrowPoint;
        let t_i = this.roundHeight(lastPoint.ts, escrowSpan);
        while (t_i.lt(height)) {
            t_i = t_i.add(escrowSpan);
            // The BN type is handled by polkadot-js in the api call
            let d_slope;
            if (t_i.gt(height)) {
                t_i = height;
                d_slope = new bn_js_1.default(0);
            }
            else {
                d_slope = this.getSlopeChange(slopeChanges, t_i);
            }
            const heightDiff = this.saturatingSub(t_i, lastPoint.ts);
            lastPoint.bias = this.saturatingSub(lastPoint.bias, lastPoint.slope.mul(heightDiff));
            if (t_i.eq(height)) {
                break;
            }
            lastPoint.slope = lastPoint.slope.add(d_slope);
            lastPoint.ts = t_i;
        }
        return lastPoint.bias;
    }
    getSpan() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.api.consts.escrow.span.toBn();
        });
    }
    getMaxPeriod() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.api.consts.escrow.maxPeriod.toBn();
        });
    }
    roundHeight(height, span) {
        return height.div(span).mul(span);
    }
    getSlopeChange(slopeChanges, key) {
        let d_slope = slopeChanges.get(key);
        if (d_slope === undefined) {
            d_slope = new bn_js_1.default(0);
        }
        return d_slope;
    }
    saturatingSub(x, y) {
        return bn_js_1.default.max(x.sub(y), new bn_js_1.default(0));
    }
}
exports.DefaultEscrowAPI = DefaultEscrowAPI;
//# sourceMappingURL=escrow.js.map