UNPKG

client-aftermath-ts-sdk

Version:
380 lines (379 loc) 25.2 kB
"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.FarmsStakedPosition = void 0; const caller_1 = require("../../general/utils/caller"); const farmsStakingPool_1 = require("./farmsStakingPool"); const fixedUtils_1 = require("../../general/utils/fixedUtils"); const utils_1 = require("../../general/utils"); const dayjs_1 = __importDefault(require("dayjs")); const farms_1 = require("./farms"); class FarmsStakedPosition extends caller_1.Caller { // ========================================================================= // Constructor // ========================================================================= constructor(stakedPosition, trueLastHarvestRewardsTimestamp = undefined, config, Provider) { super(config, "farms"); this.stakedPosition = stakedPosition; this.Provider = Provider; // ========================================================================= // Public // ========================================================================= // ========================================================================= // Getters // ========================================================================= this.isLocked = (inputs) => { return !this.isUnlocked(inputs); }; this.isLockDuration = () => { return this.stakedPosition.lockDurationMs > 0; }; this.unlockTimestamp = () => { return (this.stakedPosition.lockStartTimestamp + this.stakedPosition.lockDurationMs); }; this.rewardCoinsToClaimableBalance = (inputs) => { return this.stakedPosition.rewardCoins.reduce((acc, coin) => (Object.assign(Object.assign({}, acc), { [coin.coinType]: this.rewardsEarned(Object.assign(Object.assign({}, inputs), { coinType: coin.coinType })) })), {}); }; this.rewardCoinTypes = () => { return this.stakedPosition.rewardCoins.map((coin) => coin.coinType); }; this.nonZeroRewardCoinTypes = (inputs) => { return Object.entries(this.rewardCoinsToClaimableBalance(inputs)) .filter(([, val]) => val > BigInt(0)) .map(([key]) => key); }; this.rewardCoin = (inputs) => { const foundCoin = this.stakedPosition.rewardCoins.find((coin) => coin.coinType === inputs.coinType); if (!foundCoin) throw new Error("Invalid coin type"); return foundCoin; }; this.hasClaimableRewards = (inputs) => { const { stakingPool } = inputs; return (utils_1.Helpers.sumBigInt(this.rewardCoinTypes().map((coinType) => this.rewardsEarned({ coinType, stakingPool, }))) > BigInt(0)); }; // ========================================================================= // Calculations // ========================================================================= this.rewardsEarned = (inputs) => { if (inputs.stakingPool.rewardCoin(inputs).actualRewards === BigInt(0) || inputs.stakingPool.rewardCoin(inputs).actualRewards < inputs.stakingPool.rewardCoin(inputs).emissionRate) return BigInt(0); this.updatePosition(inputs); const rewardCoin = this.rewardCoin(inputs); const totalRewards = rewardCoin.multiplierRewardsAccumulated + rewardCoin.baseRewardsAccumulated; return totalRewards < farms_1.Farms.constants.minRewardsToClaim || totalRewards > inputs.stakingPool.rewardCoin(inputs).actualRewards ? BigInt(0) : totalRewards; }; // Updates the amount of rewards that can be harvested from `self` + the position's // debt. If the position's lock duration has elapsed, it will be unlocked. this.updatePosition = (inputs) => { const stakingPool = new farmsStakingPool_1.FarmsStakingPool(utils_1.Helpers.deepCopy(inputs.stakingPool.stakingPool), this.config); // If the position's `lock_multiplier` is larger than the `Vault`'s `max_lock_multiplier`; // We want to only cap position's who have a `lock_duration_ms` or `lock_multiplier` that exceeds // the vault's limits. if (this.stakedPosition.lockDurationMs <= stakingPool.stakingPool.maxLockDurationMs && this.stakedPosition.lockMultiplier <= stakingPool.stakingPool.maxLockMultiplier) { } else { // i. Reset the `Vault`'s `total_staked_amount_with_multiplier` that applies to this position. stakingPool.stakingPool.stakedAmountWithMultiplier -= this.stakedPosition.stakedAmountWithMultiplier; // ii. Update the `lock_duration` and `lock_multiplier` related fields. this.stakedPosition.lockDurationMs = stakingPool.stakingPool.maxLockDurationMs; this.stakedPosition.lockMultiplier = stakingPool.stakingPool.maxLockMultiplier; this.stakedPosition.stakedAmountWithMultiplier = (this.stakedPosition.stakedAmount * (this.stakedPosition.lockMultiplier - fixedUtils_1.FixedUtils.fixedOneB)) / fixedUtils_1.FixedUtils.fixedOneB; this.stakedPosition.rewardCoins = [ ...this.stakedPosition.rewardCoins.map((rewardCoin) => (Object.assign(Object.assign({}, rewardCoin), { multiplierRewardsDebt: (() => { let currentDebtPerShare = stakingPool.rewardCoin({ coinType: rewardCoin.coinType, }).rewardsAccumulatedPerShare; return ((this.stakedPosition.stakedAmountWithMultiplier * currentDebtPerShare) / fixedUtils_1.FixedUtils.fixedOneB); })() }))), ]; // iii. Increase the `Vault`'s `total_staked_amount_with_multiplier` to account for the // positions new lock multiplier. stakingPool.stakingPool.stakedAmountWithMultiplier += this.stakedPosition.stakedAmountWithMultiplier; } const currentTimestamp = (0, dayjs_1.default)().valueOf(); // i. Increase the vault's `rewardsAccumulatedPerShare` values. stakingPool.emitRewards(); for (const [rewardCoinIndex, rewardCoin,] of stakingPool.stakingPool.rewardCoins.entries()) { //******************************************************************************************// // debt (i.e. total_rewards_from_time_t0_to_th-1) // // .--- pending_rewards_at_time_th_minus_1 ---| // // |------------------------------------------+-------------------------------------------| // // t0 th-1 now // // '----------------------------- total_rewards_from_time_t0 -----------------------------' // //******************************************************************************************// // NOTE: new reward types might have been added to the vault since this position last called // `update_ pending_rewards`, so we need to be cautious when borrowing from `rewards_debt` // and `rewards_accumulated`. // if (rewardCoinIndex >= this.stakedPosition.rewardCoins.length) { this.stakedPosition.rewardCoins.push({ coinType: rewardCoin.coinType, baseRewardsAccumulated: BigInt(0), baseRewardsDebt: BigInt(0), multiplierRewardsAccumulated: BigInt(0), multiplierRewardsDebt: BigInt(0), }); } const stakedPositionRewardCoin = this.stakedPosition.rewardCoins[rewardCoinIndex]; // NOTE: `total_rewards_from_time_t0` contains the amount of rewards a position would receive // -- from time t0 to the current time -- given the vault's current state when, in reality, // we need to calculate just the rewards that have accumulated since the last time the // position's `rewards_accumulated` was updated [labeled time th-1]. // let [totalBaseRewardsFromTimeT0, totalMultiplierRewardsFromTimeT0] = this.calcTotalRewardsFromTimeT0({ rewardsAccumulatedPerShare: rewardCoin.rewardsAccumulatedPerShare, multiplierRewardsDebt: stakedPositionRewardCoin.multiplierRewardsDebt, emissionEndTimestamp: stakingPool.stakingPool.emissionEndTimestamp, }); // NOTE: Every time a position's `rewards_accumulated` is updated, a snapshot of the total // rewards received from time t0 is taken and stored as the position's `debt` field. Here, // the debt is subtracted from `total_rewards_from_time_t0` in order to calculate the amount // of rewards that have been accumulated since the last time `rewards_accumulated` was // updated. // // `pending_rewards_at_time_th_minus_1` is added to the resulting value to account for all // rewards that had accumulated up until the last time `update_position` was updated. // // iia. Allocate rewards to this position that have been emitted since the last time this // function was called. this.stakedPosition.rewardCoins[rewardCoinIndex].baseRewardsAccumulated = totalBaseRewardsFromTimeT0 - stakedPositionRewardCoin.baseRewardsDebt + stakedPositionRewardCoin.baseRewardsAccumulated; this.stakedPosition.rewardCoins[rewardCoinIndex].multiplierRewardsAccumulated = totalMultiplierRewardsFromTimeT0 - stakedPositionRewardCoin.multiplierRewardsDebt + stakedPositionRewardCoin.multiplierRewardsAccumulated; // iib. Set the position's current debt to the total rewards attributed to this position given // the vault's current state. This allows the next `update_position` call to disregard // all rewards accumulated up until this point -- preventing double emission cases. this.stakedPosition.rewardCoins[rewardCoinIndex].baseRewardsDebt = totalBaseRewardsFromTimeT0; this.stakedPosition.rewardCoins[rewardCoinIndex].multiplierRewardsDebt = totalMultiplierRewardsFromTimeT0; } // iii. Remove the position's lock multiplier + bonus staked amount if the position is no // longer locked. if (this.unlockTimestamp() < currentTimestamp) { this.unlock(); } this.stakedPosition.lastHarvestRewardsTimestamp = currentTimestamp; }; // Removes a positions `lock_duration_ms` and `lock_multiplier`. Updates the vault's // `staked_amount_with_multiplier` to account for the lost `lock_multiplier`. this.unlock = () => { // ia. Remove position's `multiplier_staked_amount` from the pool. // afterburner_vault::decrease_stake_with_multiplier(vault, self.multiplier_staked_amount); this.stakedPosition.stakedAmountWithMultiplier = BigInt(0); // ib. Reset position's lock parameters. this.stakedPosition.lockDurationMs = 0; this.stakedPosition.lockMultiplier = fixedUtils_1.FixedUtils.fixedOneB; }; this.isUnlocked = (inputs) => { const { stakingPool } = inputs; // let emitted_rewards = stakingPool.calc_emitted_rewards(); // let total_rewards = stakingPool.stakingPool.rewardCoins.map( // (coin) => coin.rewards // ); // let length = emitted_rewards.length; // let index = 0; // let no_rewards_are_remaining = true; // while (index < length && no_rewards_are_remaining) { // let emitted = emitted_rewards[index]; // let total = total_rewards[index]; // if (emitted < total) no_rewards_are_remaining = false; // index = index + 1; // } return (this.unlockTimestamp() <= (0, dayjs_1.default)().valueOf() || stakingPool.stakingPool.emissionEndTimestamp <= (0, dayjs_1.default)().valueOf() || // no_rewards_are_remaining stakingPool.stakingPool.isUnlocked); }; // ========================================================================= // Private Helpers // ========================================================================= this.useProvider = () => { var _a; const provider = (_a = this.Provider) === null || _a === void 0 ? void 0 : _a.Farms(); if (!provider) throw new Error("missing AftermathApi Provider"); return provider; }; this.stakedPosition = stakedPosition; this.trueLastHarvestRewardsTimestamp = trueLastHarvestRewardsTimestamp !== null && trueLastHarvestRewardsTimestamp !== void 0 ? trueLastHarvestRewardsTimestamp : stakedPosition.lastHarvestRewardsTimestamp; } // public calcTotalApr = (inputs: { // rewardsUsd: number; // stakeUsd: number; // }): Apr => { // const { rewardsUsd, stakeUsd } = inputs; // dayjs.extend(duration); // const oneYearMs = dayjs.duration(1, "year").asMilliseconds(); // const timeSinceLastHarvestMs = // dayjs().valueOf() - this.trueLastHarvestRewardsTimestamp; // const rewardsUsdOneYear = // timeSinceLastHarvestMs > 0 // ? rewardsUsd * (oneYearMs / timeSinceLastHarvestMs) // : 0; // const apr = stakeUsd > 0 ? rewardsUsdOneYear / stakeUsd : 0; // return apr < 0 ? 0 : isNaN(apr) ? 0 : apr; // }; // ========================================================================= // Transactions // ========================================================================= // ========================================================================= // Staking Transactions // ========================================================================= getDepositPrincipalTransaction(inputs) { return __awaiter(this, void 0, void 0, function* () { return this.useProvider().fetchBuildDepositPrincipalTx(Object.assign(Object.assign({}, inputs), { stakedPositionId: this.stakedPosition.objectId, stakeCoinType: this.stakedPosition.stakeCoinType, stakingPoolId: this.stakedPosition.stakingPoolObjectId })); }); } getUnstakeTransaction(inputs) { return __awaiter(this, void 0, void 0, function* () { return this.useProvider().fetchBuildUnstakeTx(Object.assign(Object.assign({}, inputs), { stakedPositionId: this.stakedPosition.objectId, stakeCoinType: this.stakedPosition.stakeCoinType, stakingPoolId: this.stakedPosition.stakingPoolObjectId, withdrawAmount: this.stakedPosition.stakedAmount, rewardCoinTypes: this.nonZeroRewardCoinTypes(inputs) })); }); } // ========================================================================= // Locking Transactions // ========================================================================= getLockTransaction(inputs) { return __awaiter(this, void 0, void 0, function* () { return this.useProvider().buildLockTx(Object.assign(Object.assign({}, inputs), { stakedPositionId: this.stakedPosition.objectId, stakeCoinType: this.stakedPosition.stakeCoinType, stakingPoolId: this.stakedPosition.stakingPoolObjectId })); }); } getRenewLockTransaction(inputs) { return __awaiter(this, void 0, void 0, function* () { return this.useProvider().buildRenewLockTx(Object.assign(Object.assign({}, inputs), { stakedPositionId: this.stakedPosition.objectId, stakeCoinType: this.stakedPosition.stakeCoinType, stakingPoolId: this.stakedPosition.stakingPoolObjectId })); }); } getUnlockTransaction(inputs) { return __awaiter(this, void 0, void 0, function* () { return this.useProvider().buildUnlockTx(Object.assign(Object.assign({}, inputs), { stakedPositionId: this.stakedPosition.objectId, stakeCoinType: this.stakedPosition.stakeCoinType, stakingPoolId: this.stakedPosition.stakingPoolObjectId })); }); } // ========================================================================= // Reward Harvesting Transactions // ========================================================================= getHarvestRewardsTransaction(inputs) { return __awaiter(this, void 0, void 0, function* () { return this.useProvider().fetchBuildHarvestRewardsTx(Object.assign(Object.assign({}, inputs), { stakedPositionIds: [this.stakedPosition.objectId], stakeCoinType: this.stakedPosition.stakeCoinType, stakingPoolId: this.stakedPosition.stakingPoolObjectId, rewardCoinTypes: this.nonZeroRewardCoinTypes(inputs) })); }); } // ========================================================================= // Private // ========================================================================= // ========================================================================= // Calculations // ========================================================================= // Calculates a position's accrued rewards [from time t0] given a vault's // `rewardsAccumulatedPerShare`. If the position is beyond its lock duration, we need to only // apply the lock multiplier rewards to the time spent locked. calcTotalRewardsFromTimeT0(inputs) { const { rewardsAccumulatedPerShare, multiplierRewardsDebt, emissionEndTimestamp, } = inputs; const currentTimestamp = (0, dayjs_1.default)().valueOf(); const lastRewardTimestamp = this.stakedPosition.lastHarvestRewardsTimestamp; const lockEndTimestamp = this.unlockTimestamp(); const principalStakedAmount = this.stakedPosition.stakedAmount; // Base [e.g. principal] staked amount receives full (unaltered) rewards. const rewardsAttributedToPrincipal = (principalStakedAmount * rewardsAccumulatedPerShare) / fixedUtils_1.FixedUtils.fixedOneB; const totalRewardsAttributedToLockMultiplier = (this.stakedPosition.stakedAmountWithMultiplier * rewardsAccumulatedPerShare) / fixedUtils_1.FixedUtils.fixedOneB; // The position should only receive multiplied rewards for the time that was spent locked since // the last harvest. This case occurs when the user calls `pending_rewards` after the position's // lock duration has expired. const rewardsAttributedToLockMultiplier = (() => { return currentTimestamp <= lockEndTimestamp ? //*********************************************************************************************// // v // // |-------------------------------------------+-------------------------------------------| // // last_reward_timestamp_ms current_timestamp_ms lock_end_timestamp_ms // //*********************************************************************************************// totalRewardsAttributedToLockMultiplier : lockEndTimestamp <= lastRewardTimestamp ? //*********************************************************************************************// // emission_end_timestamp_ms lock_end_timestamp_ms v // // |----------------------------+-----------------------------+----------------------------| // // last_reward_timestamp_ms current_timestamp_ms // //*********************************************************************************************// // // NOTE: if the lock period was longer than the rewards emission, the position receives the full // multiplier rewards multiplierRewardsDebt : emissionEndTimestamp <= lockEndTimestamp ? totalRewardsAttributedToLockMultiplier : (() => { //*********************************************************************************************// // lock_end_timestamp_ms emission_end_timestamp_ms v // // |----------------------------+-----------------------------+----------------------------| // // last_reward_timestamp_ms current_timestamp_ms // //*********************************************************************************************// // // NOTE: there is no enforced ordering of `emission_end_timestamp_ms` and `current_timestamp_ms` // by the time we get to this branch. // Multiplier staked amount receives (altered) rewards dependent on the total time the // position was locked since the last harvest. const timeSpentLockedSinceLastHarvestMs = lockEndTimestamp - lastRewardTimestamp; const timeSinceLastHarvestMs = currentTimestamp - lastRewardTimestamp; // ********************************************************************************************// // / timeSpentLockedSinceLastHarvestMs \ // // | ----------------------------------------- | x totalRewardsAttributedToLockMultiplier // // \ timeSinceLastHarvest / // // ********************************************************************************************// // Only disperse the multiplied rewards that were received while this position was locked. // // We are assigning this value to the total_multiplier_rewards later and this number should // never decrease, so we use max() here. const possibleMultiplierRewardsDebt = (totalRewardsAttributedToLockMultiplier * BigInt(Math.floor(timeSpentLockedSinceLastHarvestMs))) / BigInt(Math.floor(timeSinceLastHarvestMs)); return possibleMultiplierRewardsDebt > multiplierRewardsDebt ? possibleMultiplierRewardsDebt : multiplierRewardsDebt; })(); })(); return [ rewardsAttributedToPrincipal, rewardsAttributedToLockMultiplier, ]; } } exports.FarmsStakedPosition = FarmsStakedPosition;