UNPKG

@tracer-protocol/pools-js

Version:

Javascript library for interacting with Tracer's Perpetual Pools

540 lines (474 loc) 23.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _bignumber = _interopRequireDefault(require("bignumber.js")); var _token = _interopRequireDefault(require("./token")); var _ethers = require("ethers"); var _types = require("@tracer-protocol/perpetual-pools-contracts/types"); var _multicall = require("@0xsequence/multicall"); var _poolToken = _interopRequireDefault(require("./poolToken")); var _committer = _interopRequireDefault(require("./committer")); var _ = require(".."); var _utils = require("../utils"); var _smaOracle = _interopRequireDefault(require("./smaOracle")); var _poolStateHelper = _interopRequireDefault(require("./poolStateHelper")); var _tokenList = require("../data/tokenList"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * LeveragedPool class initiated with an an `address` and an `rpcURL`. * Stores relevant LeveragedPool information. * It is optional for the user to provide additional pool information, reducing * the number of RPC calls. This optional info is static information * of the pool, such as names and addresses * The constructor is private so must be instantiated with {@linkcode Pool.Create} */ class Pool { // these errors are because there is nothing initialised in constructor // percentage decimal in wei eg 1 * 10 ^18 === 100% /** * Private constructor to initialise a Pool instance * @param address LeveragedPool contract address * @param provider ethers RPC provider * @private */ constructor() { this.address = ''; this.provider = undefined; this.multicallProvider = undefined; this.chainId = undefined; this.name = ''; this.updateInterval = new _bignumber.default(0); this.frontRunningInterval = new _bignumber.default(0); this.fee = new _bignumber.default(0); this.leverage = 1; this.fee = new _bignumber.default(0); this.keeper = ''; this.committer = _committer.default.CreateDefault(); this.shortToken = _poolToken.default.CreateDefault(); this.longToken = _poolToken.default.CreateDefault(); this.settlementToken = _token.default.CreateDefault(); this.lastUpdate = new _bignumber.default(0); this.lastPrice = new _bignumber.default(0); this.shortBalance = new _bignumber.default(0); this.longBalance = new _bignumber.default(0); this.oraclePrice = new _bignumber.default(0); // default to simple moving average, can be overridden in `Create` this.oraclePriceTransformer = _utils.movingAveragePriceTransformer; this.oracle = _smaOracle.default.CreateDefault(); this.poolStateHelper = _poolStateHelper.default.CreateDefault(); } /** * Replacement constructor pattern to support async initialisations * @param poolInfo {@link IPool| IPool interface props} * @returns a Promise containing an initialised Pool class ready to be used */ static Create = async poolInfo => { const pool = new Pool(); await pool.init(poolInfo); return pool; }; /** * Creates an empty pool that can be used as a default */ static CreateDefault = () => { // trick typescript to take undefined const pool = new Pool(); return pool; }; /** * TODO */ /** * Private initialisation function called in {@link Pool.Create} * @private * @param poolInfo {@link IPool | IPool interface props} */ init = async poolInfo => { this.address = poolInfo.address; this.provider = poolInfo.provider; this.multicallProvider = new _multicall.providers.MulticallProvider(poolInfo.provider); const contract = _types.LeveragedPool__factory.connect(poolInfo.address, poolInfo.provider); this._contract = contract; const [lastUpdate, committer, keeper, updateInterval, frontRunningInterval, name, oracleWrapper] = await Promise.all([contract.lastPriceTimestamp(), poolInfo?.committer?.address ? poolInfo?.committer?.address : contract.poolCommitter(), poolInfo?.keeper ? poolInfo?.keeper : contract.keeper(), poolInfo?.updateInterval ? poolInfo?.updateInterval : contract.updateInterval(), poolInfo?.frontRunningInterval ? poolInfo?.frontRunningInterval : contract.frontRunningInterval(), poolInfo?.name ? poolInfo?.name : contract.poolName(), poolInfo?.oracle ? poolInfo.oracle : contract.oracleWrapper()]); const network = await this.multicallProvider.getNetwork(); this.chainId = network.chainId; const [fee, leverage] = await this.getFeeAndLeverage(poolInfo, this.chainId, name, this.provider, this._contract); this.fee = new _bignumber.default(_ethers.ethers.utils.formatEther(fee)); this.leverage = parseInt(leverage.toString()); const [longTokenAddress, shortTokenAddress, settlementTokenAddress] = await Promise.all([poolInfo.longToken?.address ? poolInfo.longToken?.address : contract.tokens(0), poolInfo.shortToken?.address ? poolInfo.shortToken?.address : contract.tokens(1), poolInfo.settlementToken?.address ? poolInfo?.settlementToken?.address : contract.settlementToken()]); const [shortToken, longToken, settlementToken] = await Promise.all([_poolToken.default.Create({ ...poolInfo.shortToken, pool: this.address, address: shortTokenAddress, provider: this.multicallProvider, side: _.SideEnum.short }), _poolToken.default.Create({ ...poolInfo.longToken, pool: this.address, address: longTokenAddress, provider: this.multicallProvider, side: _.SideEnum.long }), _token.default.Create({ ...poolInfo.settlementToken, address: settlementTokenAddress, provider: this.multicallProvider })]); this.shortToken = shortToken; this.longToken = longToken; this.settlementToken = settlementToken; const poolCommitter = await _committer.default.Create({ address: committer, provider: this.multicallProvider, settlementTokenDecimals: settlementToken.decimals, ...poolInfo.committer }); this.committer = poolCommitter; // TODO handle other types of oracles const smaOracle = await _smaOracle.default.Create({ address: oracleWrapper, provider: this.multicallProvider }); this.oracle = smaOracle; const keeperInstance = _types.PoolKeeper__factory.connect(keeper, this.multicallProvider); this._keeper = keeperInstance; await Promise.all([this.fetchPoolBalances(), this.fetchOraclePrice(), this.fetchLastPrice()]); this.name = name; this.keeper = keeper; this.updateInterval = new _bignumber.default(updateInterval); this.frontRunningInterval = new _bignumber.default(frontRunningInterval); this.lastUpdate = new _bignumber.default(lastUpdate.toString()); this.oraclePriceTransformer = poolInfo.oraclePriceTransformer ?? _utils.movingAveragePriceTransformer; if (_utils.POOL_STATE_HELPER_BY_NETWORK[this.chainId]) { const poolStateHelper = await _poolStateHelper.default.Create({ address: _utils.POOL_STATE_HELPER_BY_NETWORK[this.chainId], provider: this.multicallProvider, poolAddress: poolInfo.address, committerAddress: committer }); this.poolStateHelper = poolStateHelper; } }; /** * Calculates the pools next value transfer in quote token units (eg USD). * Uses {@link getNextValueTransfer}. * @returns and object containing short and long value transfer. * The values will be a negation of eachother but this way reads better than * returning a winning side as well as a value */ getNextValueTransfer = () => { const longBalanceAfterFee = this.longBalance.minus(this.longBalance.times(this.fee)); const shortBalanceAfterFee = this.shortBalance.minus(this.longBalance.times(this.fee)); return (0, _.calcNextValueTransfer)(this.lastPrice, this.oraclePrice, new _bignumber.default(this.leverage), longBalanceAfterFee, shortBalanceAfterFee); }; /** * Calculates and returns the long token price. * Uses {@link calcTokenPrice}. * @returns the long token price in quote token units (eg USD) */ getLongTokenPrice = () => (0, _.calcTokenPrice)(this.longBalance, this.longToken.supply.plus(this.committer.pendingLong.burn)); /** * Calculates and returns the fee and leverage for the current Pool address. * Compares to a list of known deprecated Pool addresses and uses old method if necessary. * @returns the fee and leverage amounts */ getFeeAndLeverage = async (poolInfo, chainId, name, provider, contract) => { if (_tokenList.deprecatedTokenAddresses.includes(this.address)) { let leverage; if (poolInfo?.leverage) { leverage = poolInfo.leverage; } else if (!_utils.POOL_SWAP_LIBRARY_BY_NETWORK[chainId]) { // temp fix since the fetched leverage is in IEEE 128 bit. Get leverage amount from name leverage = parseInt(name.split('-')?.[0] ?? 1); } else { try { const poolSwapLibrary = _types.PoolSwapLibrary__factory.connect(_utils.POOL_SWAP_LIBRARY_BY_NETWORK[chainId], provider); const leverageAmountBytes = await contract.leverageAmount(); const convertedLeverage = await poolSwapLibrary.convertDecimalToUInt(leverageAmountBytes); leverage = convertedLeverage.toNumber(); } catch (error) { leverage = parseInt(name.split('-')?.[0] ?? 1); } } return ['0', leverage]; } else { return [poolInfo?.fee && parseInt(_ethers.ethers.utils.formatEther(poolInfo.fee)) <= 1 ? poolInfo?.fee : await contract.getFee(), // passed fee cant be over 100% poolInfo?.leverage ? poolInfo.leverage : await contract.getLeverage()]; } }; /** * Calculates and returns the short token price. * Uses {@link calcTokenPrice}. * @returns the long token price in quote token units (eg USD) */ getShortTokenPrice = () => (0, _.calcTokenPrice)(this.shortBalance, this.shortToken.supply.plus(this.committer.pendingShort.burn)); /** * Calculates and returns the long token price as if the rebalance occured at t = now. * Uses {@link calcTokenPrice}. * @returns the long token price in quote token units (eg USD) */ getNextLongTokenPrice = () => { // value transfer is +- const { longValueTransfer } = this.getNextValueTransfer(); const longBalanceAfterFee = this.longBalance.minus(this.fee.times(this.shortBalance)); return (0, _.calcTokenPrice)(longBalanceAfterFee.plus(longValueTransfer), this.longToken.supply.plus(this.committer.pendingLong.burn)); }; /** * Calculates and returns the short token price as if the rebalance occured at t = now. * Uses {@link calcTokenPrice}. * @returns the long token price in quote token units (eg USD) */ getNextShortTokenPrice = () => { // value transfer is +- const { shortValueTransfer } = this.getNextValueTransfer(); const shortBalanceAfterFee = this.shortBalance.minus(this.fee.times(this.shortBalance)); return (0, _.calcTokenPrice)(shortBalanceAfterFee.plus(shortValueTransfer), this.shortToken.supply.plus(this.committer.pendingShort.burn)); }; /** * Calculates the pools current skew between long and short balances. * This is the ratio between the long and short pools * @returns the pool skew */ getSkew = () => (0, _.calcSkew)(this.shortBalance, this.longBalance); /** * Fetches and sets the pools long and short balances from the contract state * @returns the fetched long and short balances */ fetchPoolBalances = async () => { if (!this._contract) { throw Error("Failed to update pool balances: this._contract undefined"); } const [longBalance_, shortBalance_] = await Promise.all([this._contract.longBalance(), this._contract.shortBalance()]).catch(error => { throw Error("Failed to update pool balances: " + error?.message ?? error); }); const shortBalance = new _bignumber.default(_ethers.ethers.utils.formatUnits(shortBalance_, this.settlementToken.decimals)); const longBalance = new _bignumber.default(_ethers.ethers.utils.formatUnits(longBalance_, this.settlementToken.decimals)); this.setLongBalance(longBalance); this.setShortBalance(shortBalance); return { longBalance: longBalance, shortBalance: shortBalance }; }; /** * Sets and gets the most up to date oraclePrice */ fetchOraclePrice = async () => { if (!this._contract) { throw Error("Failed to fetch the pools oracle price: this._contract undefined"); } const price_ = await this._contract.getOraclePrice().catch(error => { throw Error("Failed to fetch pools oralce price: " + error?.message ?? error); }); const price = new _bignumber.default(_ethers.ethers.utils.formatEther(price_)); this.setOraclePrice(price); return price; }; /** * Sets and gets the most up to date pool price. * This is the price the pool used last upkeep */ fetchLastPrice = async () => { if (!this._keeper) { throw Error("Failed to fetch pools last price: this._keeper undefined"); } const price_ = await this._keeper.executionPrice(this.address).catch(error => { throw Error("Failed to fetch pools last price: " + error?.message); }); const price = new _bignumber.default(_ethers.ethers.utils.formatEther(price_)); this.setLastPrice(price); return price; }; /** * Sets and gets the most up to date pool price. * This is the price the pool used last upkeep */ fetchLastPriceTimestamp = async () => { if (!this._contract) { throw Error("Failed to fetch pools last price timestamp: this._contract undefined"); } const timestamp_ = await this._contract?.lastPriceTimestamp().catch(error => { throw Error("Failed to fetch pools last price timestamp: " + error?.message); }); const timestamp = new _bignumber.default(timestamp_.toString()); this.setLastPriceTimestamp(timestamp); return timestamp; }; /** * @deprecated * get all total pool commitments between now and `now + frontRunningInterval` * @returns promise resolving to an array of `TotalPoolCommitmentsBN`s */ getPendingCommitsInFrontRunningInterval = async () => { if (!this.committer._contract) { throw Error("Failed to fetch pending commits in front running interval: this.committer._contract undefined"); } const updateIntervalId = (await this.committer._contract.updateIntervalId()).toNumber(); if (this.frontRunningInterval.lt(this.updateInterval)) { // simple case, commits will be executed either in next upkeep // or one after if committed within the front running interval const pendingCommitsThisInterval = await this.committer._contract.totalPoolCommitments(updateIntervalId); return [(0, _utils.pendingCommitsToBN)(pendingCommitsThisInterval, this.settlementToken.decimals)]; } const upkeepsPerFrontRunningInterval = Math.floor(this.frontRunningInterval.div(this.updateInterval).toNumber()); const pendingCommitPromises = []; // the last update interval that will be executed in the frontrunning interval as of now const maxIntervalId = updateIntervalId + upkeepsPerFrontRunningInterval; for (let i = updateIntervalId; i <= maxIntervalId; i++) { pendingCommitPromises.push(this.committer._contract.totalPoolCommitments(i).then(totalPoolCommitments => (0, _utils.pendingCommitsToBN)(totalPoolCommitments, this.settlementToken.decimals))); } return Promise.all(pendingCommitPromises); }; /** * @deprecated * get total pool commitments between now and `now + updateInterval` * @returns promise resolving to a `TotalPoolCommitmentsBN` object */ getPendingCommitsInUpdateInterval = async () => { if (!this.committer._contract) { throw Error("Failed to fetch pending commits in update interval: this.committer._contract undefined"); } const updateIntervalId = (await this.committer._contract.updateIntervalId()).toNumber(); const pendingCommitsThisInterval = await this.committer._contract.totalPoolCommitments(updateIntervalId); return (0, _utils.pendingCommitsToBN)(pendingCommitsThisInterval); }; /** * @deprecated * @param atEndOf whether to fetch preview for end of update interval or front running interval * @param forceRefreshInputs if `true`, will refresh * `this.longBalance`, * `this.shortBalance`, * `this.longToken.supply`, * `this.shortToken.supply`, * `this.lastPrice` and `this.oraclePrice` before calculating pool state preview * @returns */ getPoolStatePreview = async (atEndOf, forceRefreshInputs) => { if (!this.committer._contract) { throw Error("Failed to get pool state preview after front running interval: this.committer._contract undefined"); } const [longBalance, shortBalance] = await Promise.all([forceRefreshInputs ? (await this.fetchPoolBalances()).longBalance : this.longBalance, forceRefreshInputs ? (await this.fetchPoolBalances()).shortBalance : this.shortBalance]); const [pendingCommits, pendingLongTokenBurn, pendingShortTokenBurn, longTokenSupply, shortTokenSupply, lastPrice, oraclePrice] = await Promise.all([atEndOf === 'frontRunningInterval' ? this.getPendingCommitsInFrontRunningInterval() : [await this.getPendingCommitsInUpdateInterval()], this.committer._contract.pendingLongBurnPoolTokens(), this.committer._contract.pendingShortBurnPoolTokens(), forceRefreshInputs ? this.longToken.fetchSupply() : this.longToken.supply, forceRefreshInputs ? this.shortToken.fetchSupply() : this.shortToken.supply, forceRefreshInputs ? this.fetchLastPrice() : this.lastPrice, forceRefreshInputs ? this.fetchOraclePrice() : this.oraclePrice]); const poolStatePreview = (0, _.calcPoolStatePreview)({ leverage: new _bignumber.default(this.leverage), fee: this.fee, longBalance, shortBalance, longTokenSupply: longTokenSupply, shortTokenSupply: shortTokenSupply, pendingLongTokenBurn: (0, _utils.ethersBNtoBN)(pendingLongTokenBurn), pendingShortTokenBurn: (0, _utils.ethersBNtoBN)(pendingShortTokenBurn), lastOraclePrice: lastPrice, currentOraclePrice: oraclePrice, pendingCommits, oraclePriceTransformer: this.oraclePriceTransformer }); return poolStatePreview; }; /** * * calculate expected pool state via on chain state calculations contract * * @param atEndOf whether to fetch preview for end of update interval or front running interval * @returns */ getExpectedPoolState = async atEndOf => { const periods = atEndOf === 'frontRunningInterval' ? this.poolStateHelper.fullCommitPeriod + 1 : 1; const [currentState, expectedState, pendingCommits, currentOraclePrice] = await Promise.all([this.poolStateHelper.getExpectedPoolState({ periods: 0 }), this.poolStateHelper.getExpectedPoolState({ periods }), this.poolStateHelper.getCommitQueue({ periods }), this.oracle.getPrice()]); const effectiveCurrentLongSupply = currentState.longSupply.plus(currentState.pendingLongTokenBurn); const effectiveCurrentShortSupply = currentState.shortSupply.plus(currentState.pendingShortTokenBurn); const effectiveExpectedLongSupply = expectedState.longSupply.plus(expectedState.pendingLongTokenBurn); const effectiveExpectedShortSupply = expectedState.shortSupply.plus(expectedState.pendingShortTokenBurn); const poolStatePreview = { timestamp: Math.floor(Date.now() / 1000), currentSkew: currentState.longBalance.div(currentState.shortBalance.eq(0) ? 1 : currentState.shortBalance), currentLongBalance: currentState.longBalance, currentLongSupply: currentState.longSupply, currentShortBalance: currentState.shortBalance, currentShortSupply: currentState.shortSupply, currentPendingLongTokenBurn: currentState.pendingLongTokenBurn, currentPendingShortTokenBurn: currentState.pendingShortTokenBurn, currentLongTokenPrice: currentState.longBalance.div(effectiveCurrentLongSupply.eq(0) ? 1 : effectiveCurrentLongSupply), currentShortTokenPrice: currentState.shortBalance.div(effectiveCurrentShortSupply.eq(0) ? 1 : effectiveCurrentShortSupply), expectedSkew: expectedState.longBalance.div(expectedState.shortBalance.eq(0) ? 1 : expectedState.shortBalance), expectedLongBalance: expectedState.longBalance, expectedLongSupply: expectedState.longSupply, expectedShortBalance: expectedState.shortBalance, expectedShortSupply: expectedState.shortSupply, totalNetPendingLong: expectedState.longBalance.minus(currentState.longBalance), totalNetPendingShort: expectedState.shortBalance.minus(currentState.shortBalance), expectedLongTokenPrice: expectedState.longBalance.div(effectiveExpectedLongSupply.eq(0) ? 1 : effectiveExpectedLongSupply), expectedShortTokenPrice: expectedState.shortBalance.div(effectiveExpectedShortSupply.eq(0) ? 1 : effectiveExpectedShortSupply), expectedPendingLongTokenBurn: expectedState.pendingLongTokenBurn, expectedPendingShortTokenBurn: expectedState.pendingShortTokenBurn, lastOraclePrice: currentOraclePrice, expectedOraclePrice: expectedState.oraclePrice, pendingCommits }; return poolStatePreview; }; /** * Replaces the provider and connects the contract instance, also connects the * settlementToken, short and long tokens and Committer instance * @param provider The new provider to connect to */ connect = provider => { if (!provider) { throw Error("Failed to connect LeveragedPool: provider cannot be undefined"); } this.provider = provider; this.multicallProvider = new _multicall.providers.MulticallProvider(provider); this._contract = this._contract?.connect(this.multicallProvider); this.committer.connect(this.multicallProvider); this.longToken.connect(this.multicallProvider); this.shortToken.connect(this.multicallProvider); this.settlementToken.connect(this.multicallProvider); }; getAnnualFee = () => { if (this.updateInterval.eq(0)) { return new _bignumber.default(0); } return this.fee.times(_utils.SECONDS_PER_LEAP_YEAR).div(this.updateInterval); }; /** * Sets the pools long balance * @param longBalance balance to set */ setLongBalance = longBalance => { this.longBalance = longBalance; }; /** * Sets the pools short balance * @param shortBalance balance to set */ setShortBalance = shortBalance => { this.shortBalance = shortBalance; }; /** * Sets the pools oracle price * @param price new price to set */ setOraclePrice = price => { this.oraclePrice = price; }; /** * Sets the pools last price * @param price new price to set */ setLastPrice = price => { this.lastPrice = price; }; /** * Sets the pools last price * @param price new price to set */ setLastPriceTimestamp = timestamp => { this.lastUpdate = timestamp; }; } exports.default = Pool;