@tracer-protocol/pools-js
Version:
Javascript library for interacting with Tracer's Perpetual Pools
540 lines (474 loc) • 23.1 kB
JavaScript
"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;