@drift-labs/sdk-browser
Version:
SDK for Drift Protocol
921 lines (920 loc) • 139 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.User = void 0;
const types_1 = require("./types");
const position_1 = require("./math/position");
const numericConstants_1 = require("./constants/numericConstants");
const bigNum_1 = require("./factory/bigNum");
const anchor_1 = require("@coral-xyz/anchor");
const position_2 = require("./math/position");
const market_1 = require("./math/market");
const margin_1 = require("./math/margin");
const spotMarket_1 = require("./math/spotMarket");
const utils_1 = require("./math/utils");
const spotBalance_1 = require("./math/spotBalance");
const trade_1 = require("./math/trade");
const types_2 = require("./types");
const orders_1 = require("./math/orders");
const websocketProgramUserAccountSubscriber_1 = require("./accounts/websocketProgramUserAccountSubscriber");
const spotBalance_2 = require("./math/spotBalance");
const margin_2 = require("./math/margin");
const pollingUserAccountSubscriber_1 = require("./accounts/pollingUserAccountSubscriber");
const webSocketUserAccountSubscriber_1 = require("./accounts/webSocketUserAccountSubscriber");
const spotPosition_1 = require("./math/spotPosition");
const oracles_1 = require("./math/oracles");
const tiers_1 = require("./math/tiers");
const strictOraclePrice_1 = require("./oracles/strictOraclePrice");
const fuel_1 = require("./math/fuel");
const grpcUserAccountSubscriber_1 = require("./accounts/grpcUserAccountSubscriber");
const marginCalculation_1 = require("./marginCalculation");
class User {
get isSubscribed() {
return this._isSubscribed && this.accountSubscriber.isSubscribed;
}
set isSubscribed(val) {
this._isSubscribed = val;
}
constructor(config) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
this._isSubscribed = false;
this.driftClient = config.driftClient;
this.userAccountPublicKey = config.userAccountPublicKey;
if (((_a = config.accountSubscription) === null || _a === void 0 ? void 0 : _a.type) === 'polling') {
this.accountSubscriber = new pollingUserAccountSubscriber_1.PollingUserAccountSubscriber(config.driftClient.connection, config.userAccountPublicKey, config.accountSubscription.accountLoader, this.driftClient.program.account.user.coder.accounts.decodeUnchecked.bind(this.driftClient.program.account.user.coder.accounts));
}
else if (((_b = config.accountSubscription) === null || _b === void 0 ? void 0 : _b.type) === 'custom') {
this.accountSubscriber = config.accountSubscription.userAccountSubscriber;
}
else if (((_c = config.accountSubscription) === null || _c === void 0 ? void 0 : _c.type) === 'grpc') {
if (config.accountSubscription.grpcMultiUserAccountSubscriber) {
this.accountSubscriber =
config.accountSubscription.grpcMultiUserAccountSubscriber.forUser(config.userAccountPublicKey);
}
else {
this.accountSubscriber = new grpcUserAccountSubscriber_1.grpcUserAccountSubscriber(config.accountSubscription.grpcConfigs, config.driftClient.program, config.userAccountPublicKey, {
resubTimeoutMs: (_d = config.accountSubscription) === null || _d === void 0 ? void 0 : _d.resubTimeoutMs,
logResubMessages: (_e = config.accountSubscription) === null || _e === void 0 ? void 0 : _e.logResubMessages,
});
}
}
else {
if (((_f = config.accountSubscription) === null || _f === void 0 ? void 0 : _f.type) === 'websocket' &&
((_g = config.accountSubscription) === null || _g === void 0 ? void 0 : _g.programUserAccountSubscriber)) {
this.accountSubscriber = new websocketProgramUserAccountSubscriber_1.WebSocketProgramUserAccountSubscriber(config.driftClient.program, config.userAccountPublicKey, config.accountSubscription.programUserAccountSubscriber);
}
else {
this.accountSubscriber = new webSocketUserAccountSubscriber_1.WebSocketUserAccountSubscriber(config.driftClient.program, config.userAccountPublicKey, {
resubTimeoutMs: (_h = config.accountSubscription) === null || _h === void 0 ? void 0 : _h.resubTimeoutMs,
logResubMessages: (_j = config.accountSubscription) === null || _j === void 0 ? void 0 : _j.logResubMessages,
}, (_k = config.accountSubscription) === null || _k === void 0 ? void 0 : _k.commitment);
}
}
this.eventEmitter = this.accountSubscriber.eventEmitter;
}
/**
* Subscribe to User state accounts
* @returns SusbcriptionSuccess result
*/
async subscribe(userAccount) {
this.isSubscribed = await this.accountSubscriber.subscribe(userAccount);
return this.isSubscribed;
}
/**
* Forces the accountSubscriber to fetch account updates from rpc
*/
async fetchAccounts() {
await this.accountSubscriber.fetch();
}
async unsubscribe() {
await this.accountSubscriber.unsubscribe();
this.isSubscribed = false;
}
getUserAccount() {
return this.accountSubscriber.getUserAccountAndSlot().data;
}
async forceGetUserAccount() {
await this.fetchAccounts();
return this.accountSubscriber.getUserAccountAndSlot().data;
}
getUserAccountAndSlot() {
return this.accountSubscriber.getUserAccountAndSlot();
}
getPerpPositionForUserAccount(userAccount, marketIndex) {
return this.getActivePerpPositionsForUserAccount(userAccount).find((position) => position.marketIndex === marketIndex);
}
/**
* Gets the user's current position for a given perp market. If the user has no position returns undefined
* @param marketIndex
* @returns userPerpPosition
*/
getPerpPosition(marketIndex) {
const userAccount = this.getUserAccount();
return this.getPerpPositionForUserAccount(userAccount, marketIndex);
}
getPerpPositionOrEmpty(marketIndex) {
var _a;
const userAccount = this.getUserAccount();
return ((_a = this.getPerpPositionForUserAccount(userAccount, marketIndex)) !== null && _a !== void 0 ? _a : this.getEmptyPosition(marketIndex));
}
getPerpPositionAndSlot(marketIndex) {
const userAccount = this.getUserAccountAndSlot();
const perpPosition = this.getPerpPositionForUserAccount(userAccount.data, marketIndex);
return {
data: perpPosition,
slot: userAccount.slot,
};
}
getSpotPositionForUserAccount(userAccount, marketIndex) {
return userAccount.spotPositions.find((position) => position.marketIndex === marketIndex);
}
/**
* Gets the user's current position for a given spot market. If the user has no position returns undefined
* @param marketIndex
* @returns userSpotPosition
*/
getSpotPosition(marketIndex) {
const userAccount = this.getUserAccount();
return this.getSpotPositionForUserAccount(userAccount, marketIndex);
}
getSpotPositionAndSlot(marketIndex) {
const userAccount = this.getUserAccountAndSlot();
const spotPosition = this.getSpotPositionForUserAccount(userAccount.data, marketIndex);
return {
data: spotPosition,
slot: userAccount.slot,
};
}
getEmptySpotPosition(marketIndex) {
return {
marketIndex,
scaledBalance: numericConstants_1.ZERO,
balanceType: types_2.SpotBalanceType.DEPOSIT,
cumulativeDeposits: numericConstants_1.ZERO,
openAsks: numericConstants_1.ZERO,
openBids: numericConstants_1.ZERO,
openOrders: 0,
};
}
/**
* Returns the token amount for a given market. The spot market precision is based on the token mint decimals.
* Positive if it is a deposit, negative if it is a borrow.
*
* @param marketIndex
*/
getTokenAmount(marketIndex) {
const spotPosition = this.getSpotPosition(marketIndex);
if (spotPosition === undefined) {
return numericConstants_1.ZERO;
}
const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
return (0, spotBalance_1.getSignedTokenAmount)((0, spotBalance_2.getTokenAmount)(spotPosition.scaledBalance, spotMarket, spotPosition.balanceType), spotPosition.balanceType);
}
getEmptyPosition(marketIndex) {
return {
baseAssetAmount: numericConstants_1.ZERO,
remainderBaseAssetAmount: 0,
lastCumulativeFundingRate: numericConstants_1.ZERO,
marketIndex,
quoteAssetAmount: numericConstants_1.ZERO,
quoteEntryAmount: numericConstants_1.ZERO,
quoteBreakEvenAmount: numericConstants_1.ZERO,
openOrders: 0,
openBids: numericConstants_1.ZERO,
openAsks: numericConstants_1.ZERO,
settledPnl: numericConstants_1.ZERO,
lpShares: numericConstants_1.ZERO,
lastBaseAssetAmountPerLp: numericConstants_1.ZERO,
lastQuoteAssetAmountPerLp: numericConstants_1.ZERO,
perLpBase: 0,
maxMarginRatio: 0,
isolatedPositionScaledBalance: numericConstants_1.ZERO,
positionFlag: 0,
};
}
isPositionEmpty(position) {
return position.baseAssetAmount.eq(numericConstants_1.ZERO) && position.openOrders === 0;
}
getIsolatePerpPositionTokenAmount(perpMarketIndex) {
var _a;
const perpPosition = this.getPerpPosition(perpMarketIndex);
if (!perpPosition)
return numericConstants_1.ZERO;
const perpMarket = this.driftClient.getPerpMarketAccount(perpMarketIndex);
const spotMarket = this.driftClient.getSpotMarketAccount(perpMarket.quoteSpotMarketIndex);
if (perpPosition === undefined) {
return numericConstants_1.ZERO;
}
return (0, spotBalance_2.getTokenAmount)((_a = perpPosition.isolatedPositionScaledBalance) !== null && _a !== void 0 ? _a : numericConstants_1.ZERO, //TODO remove ? later
spotMarket, types_2.SpotBalanceType.DEPOSIT);
}
getClonedPosition(position) {
const clonedPosition = Object.assign({}, position);
return clonedPosition;
}
getOrderForUserAccount(userAccount, orderId) {
return userAccount.orders.find((order) => order.orderId === orderId);
}
/**
* @param orderId
* @returns Order
*/
getOrder(orderId) {
const userAccount = this.getUserAccount();
return this.getOrderForUserAccount(userAccount, orderId);
}
getOrderAndSlot(orderId) {
const userAccount = this.getUserAccountAndSlot();
const order = this.getOrderForUserAccount(userAccount.data, orderId);
return {
data: order,
slot: userAccount.slot,
};
}
getOrderByUserIdForUserAccount(userAccount, userOrderId) {
return userAccount.orders.find((order) => order.userOrderId === userOrderId);
}
/**
* @param userOrderId
* @returns Order
*/
getOrderByUserOrderId(userOrderId) {
const userAccount = this.getUserAccount();
return this.getOrderByUserIdForUserAccount(userAccount, userOrderId);
}
getOrderByUserOrderIdAndSlot(userOrderId) {
const userAccount = this.getUserAccountAndSlot();
const order = this.getOrderByUserIdForUserAccount(userAccount.data, userOrderId);
return {
data: order,
slot: userAccount.slot,
};
}
getOpenOrdersForUserAccount(userAccount) {
return userAccount === null || userAccount === void 0 ? void 0 : userAccount.orders.filter((order) => (0, types_1.isVariant)(order.status, 'open'));
}
getOpenOrders() {
const userAccount = this.getUserAccount();
return this.getOpenOrdersForUserAccount(userAccount);
}
getOpenOrdersAndSlot() {
const userAccount = this.getUserAccountAndSlot();
const openOrders = this.getOpenOrdersForUserAccount(userAccount.data);
return {
data: openOrders,
slot: userAccount.slot,
};
}
getUserAccountPublicKey() {
return this.userAccountPublicKey;
}
async exists() {
const userAccountRPCResponse = await this.driftClient.connection.getParsedAccountInfo(this.userAccountPublicKey);
return userAccountRPCResponse.value !== null;
}
/**
* calculates the total open bids/asks in a perp market (including lps)
* @returns : open bids
* @returns : open asks
*/
getPerpBidAsks(marketIndex) {
const position = this.getPerpPosition(marketIndex);
const totalOpenBids = position.openBids;
const totalOpenAsks = position.openAsks;
return [totalOpenBids, totalOpenAsks];
}
/**
* calculates Buying Power = free collateral / initial margin ratio
* @returns : Precision QUOTE_PRECISION
*/
getPerpBuyingPower(marketIndex, collateralBuffer = numericConstants_1.ZERO, enterHighLeverageMode = undefined, maxMarginRatio = undefined, positionType = 'cross') {
const perpPosition = this.getPerpPositionOrEmpty(marketIndex);
const perpMarket = this.driftClient.getPerpMarketAccount(marketIndex);
const oraclePriceData = this.getOracleDataForPerpMarket(marketIndex);
const worstCaseBaseAssetAmount = perpPosition
? (0, margin_2.calculateWorstCaseBaseAssetAmount)(perpPosition, perpMarket, oraclePriceData.price)
: numericConstants_1.ZERO;
let freeCollateral;
if (positionType === 'isolated' && this.isPositionEmpty(perpPosition)) {
const { totalAssetValue: quoteSpotMarketAssetValue, totalLiabilityValue: quoteSpotMarketLiabilityValue, } = this.getSpotMarketAssetAndLiabilityValue(perpMarket.quoteSpotMarketIndex, 'Initial', undefined, undefined, true);
freeCollateral = quoteSpotMarketAssetValue.sub(quoteSpotMarketLiabilityValue);
}
else {
freeCollateral = this.getFreeCollateral('Initial', enterHighLeverageMode, positionType === 'isolated' ? marketIndex : undefined).sub(collateralBuffer);
}
return this.getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount(marketIndex, freeCollateral, worstCaseBaseAssetAmount, enterHighLeverageMode, maxMarginRatio || perpPosition.maxMarginRatio);
}
getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount(marketIndex, freeCollateral, baseAssetAmount, enterHighLeverageMode = undefined, perpMarketMaxMarginRatio = undefined) {
const maxMarginRatio = Math.max(perpMarketMaxMarginRatio, this.getUserAccount().maxMarginRatio);
const marginRatio = (0, market_1.calculateMarketMarginRatio)(this.driftClient.getPerpMarketAccount(marketIndex), baseAssetAmount, 'Initial', maxMarginRatio, enterHighLeverageMode || this.isHighLeverageMode('Initial'));
return freeCollateral.mul(numericConstants_1.MARGIN_PRECISION).div(new anchor_1.BN(marginRatio));
}
/**
* calculates Free Collateral = Total collateral - margin requirement
* @returns : Precision QUOTE_PRECISION
*/
getFreeCollateral(marginCategory = 'Initial', enterHighLeverageMode = false, perpMarketIndex) {
const calc = this.getMarginCalculation(marginCategory, {
enteringHighLeverage: enterHighLeverageMode,
strict: marginCategory === 'Initial',
});
if (perpMarketIndex !== undefined) {
return calc.getIsolatedFreeCollateral(perpMarketIndex);
}
else {
return calc.getCrossFreeCollateral();
}
}
getMarginRequirement(marginCategory, liquidationBuffer, strict, includeOpenOrders, enteringHighLeverage, perpMarketIndex) {
const liquidationBufferMap = new Map();
if (liquidationBuffer && perpMarketIndex !== undefined) {
liquidationBufferMap.set(perpMarketIndex, liquidationBuffer);
}
else if (liquidationBuffer) {
liquidationBufferMap.set('cross', liquidationBuffer);
}
const marginCalc = this.getMarginCalculation(marginCategory, {
strict,
includeOpenOrders,
enteringHighLeverage,
liquidationBufferMap,
});
// If perpMarketIndex is provided, compute only for that market index
if (perpMarketIndex !== undefined) {
const isolatedMarginCalculation = marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
if (!isolatedMarginCalculation)
return numericConstants_1.ZERO;
const { marginRequirement, marginRequirementPlusBuffer } = isolatedMarginCalculation;
if (liquidationBuffer === null || liquidationBuffer === void 0 ? void 0 : liquidationBuffer.gt(numericConstants_1.ZERO)) {
return marginRequirementPlusBuffer;
}
return marginRequirement;
}
// Default: Cross margin requirement
if (liquidationBuffer === null || liquidationBuffer === void 0 ? void 0 : liquidationBuffer.gt(numericConstants_1.ZERO)) {
return marginCalc.marginRequirementPlusBuffer;
}
return marginCalc.marginRequirement;
}
/**
* @returns The initial margin requirement in USDC. : QUOTE_PRECISION
*/
getInitialMarginRequirement(enterHighLeverageMode = false, perpMarketIndex) {
return this.getMarginRequirement('Initial', undefined, true, undefined, enterHighLeverageMode, perpMarketIndex);
}
/**
* @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION
*/
getMaintenanceMarginRequirement(liquidationBuffer, perpMarketIndex) {
return this.getMarginRequirement('Maintenance', liquidationBuffer, false, // strict default
true, // includeOpenOrders default
false, // enteringHighLeverage default
perpMarketIndex);
}
getActivePerpPositionsForUserAccount(userAccount) {
return userAccount.perpPositions.filter((pos) => {
var _a;
return !pos.baseAssetAmount.eq(numericConstants_1.ZERO) ||
!pos.quoteAssetAmount.eq(numericConstants_1.ZERO) ||
!(pos.openOrders == 0) ||
((_a = pos.isolatedPositionScaledBalance) === null || _a === void 0 ? void 0 : _a.gt(numericConstants_1.ZERO));
});
}
getActivePerpPositions() {
const userAccount = this.getUserAccount();
return this.getActivePerpPositionsForUserAccount(userAccount);
}
getActivePerpPositionsAndSlot() {
const userAccount = this.getUserAccountAndSlot();
const positions = this.getActivePerpPositionsForUserAccount(userAccount.data);
return {
data: positions,
slot: userAccount.slot,
};
}
getActiveSpotPositionsForUserAccount(userAccount) {
return userAccount.spotPositions.filter((pos) => !(0, spotPosition_1.isSpotPositionAvailable)(pos));
}
getActiveSpotPositions() {
const userAccount = this.getUserAccount();
return this.getActiveSpotPositionsForUserAccount(userAccount);
}
getActiveSpotPositionsAndSlot() {
const userAccount = this.getUserAccountAndSlot();
const positions = this.getActiveSpotPositionsForUserAccount(userAccount.data);
return {
data: positions,
slot: userAccount.slot,
};
}
/**
* calculates unrealized position price pnl
* @returns : Precision QUOTE_PRECISION
*/
getUnrealizedPNL(withFunding, marketIndex, withWeightMarginCategory, strict = false, liquidationBuffer) {
return this.getActivePerpPositions()
.filter((pos) => marketIndex !== undefined ? pos.marketIndex === marketIndex : true)
.reduce((unrealizedPnl, perpPosition) => {
const market = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex);
const oraclePriceData = this.getMMOracleDataForPerpMarket(market.marketIndex);
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(market.quoteSpotMarketIndex);
const quoteOraclePriceData = this.getOracleDataForSpotMarket(market.quoteSpotMarketIndex);
let positionUnrealizedPnl = (0, position_2.calculatePositionPNL)(market, perpPosition, withFunding, oraclePriceData);
let quotePrice;
if (strict && positionUnrealizedPnl.gt(numericConstants_1.ZERO)) {
quotePrice = anchor_1.BN.min(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min);
}
else if (strict && positionUnrealizedPnl.lt(numericConstants_1.ZERO)) {
quotePrice = anchor_1.BN.max(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min);
}
else {
quotePrice = quoteOraclePriceData.price;
}
positionUnrealizedPnl = positionUnrealizedPnl
.mul(quotePrice)
.div(numericConstants_1.PRICE_PRECISION);
if (withWeightMarginCategory !== undefined) {
if (positionUnrealizedPnl.gt(numericConstants_1.ZERO)) {
positionUnrealizedPnl = positionUnrealizedPnl
.mul((0, market_1.calculateUnrealizedAssetWeight)(market, quoteSpotMarket, positionUnrealizedPnl, withWeightMarginCategory, oraclePriceData))
.div(new anchor_1.BN(numericConstants_1.SPOT_MARKET_WEIGHT_PRECISION));
}
if (liquidationBuffer && positionUnrealizedPnl.lt(numericConstants_1.ZERO)) {
positionUnrealizedPnl = positionUnrealizedPnl.add(positionUnrealizedPnl.mul(liquidationBuffer).div(numericConstants_1.MARGIN_PRECISION));
}
}
return unrealizedPnl.add(positionUnrealizedPnl);
}, numericConstants_1.ZERO);
}
/**
* calculates unrealized funding payment pnl
* @returns : Precision QUOTE_PRECISION
*/
getUnrealizedFundingPNL(marketIndex) {
return this.getUserAccount()
.perpPositions.filter((pos) => marketIndex !== undefined ? pos.marketIndex === marketIndex : true)
.reduce((pnl, perpPosition) => {
const market = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex);
return pnl.add((0, position_1.calculateUnsettledFundingPnl)(market, perpPosition));
}, numericConstants_1.ZERO);
}
getFuelBonus(now, includeSettled = true, includeUnsettled = true, givenUserStats) {
const userAccount = this.getUserAccount();
const result = {
insuranceFuel: numericConstants_1.ZERO,
takerFuel: numericConstants_1.ZERO,
makerFuel: numericConstants_1.ZERO,
depositFuel: numericConstants_1.ZERO,
borrowFuel: numericConstants_1.ZERO,
positionFuel: numericConstants_1.ZERO,
};
const userStats = givenUserStats !== null && givenUserStats !== void 0 ? givenUserStats : this.driftClient.getUserStats();
const userStatsAccount = userStats.getAccount();
if (includeSettled) {
result.takerFuel = result.takerFuel.add(new anchor_1.BN(userStatsAccount.fuelTaker));
result.makerFuel = result.makerFuel.add(new anchor_1.BN(userStatsAccount.fuelMaker));
result.depositFuel = result.depositFuel.add(new anchor_1.BN(userStatsAccount.fuelDeposits));
result.borrowFuel = result.borrowFuel.add(new anchor_1.BN(userStatsAccount.fuelBorrows));
result.positionFuel = result.positionFuel.add(new anchor_1.BN(userStatsAccount.fuelPositions));
}
if (includeUnsettled) {
const fuelBonusNumerator = anchor_1.BN.max(now.sub(anchor_1.BN.max(new anchor_1.BN(userAccount.lastFuelBonusUpdateTs), numericConstants_1.FUEL_START_TS)), numericConstants_1.ZERO);
if (fuelBonusNumerator.gt(numericConstants_1.ZERO)) {
for (const spotPosition of this.getActiveSpotPositions()) {
const spotMarketAccount = this.driftClient.getSpotMarketAccount(spotPosition.marketIndex);
const tokenAmount = this.getTokenAmount(spotPosition.marketIndex);
const oraclePriceData = this.getOracleDataForSpotMarket(spotPosition.marketIndex);
const twap5min = (0, oracles_1.calculateLiveOracleTwap)(spotMarketAccount.historicalOracleData, oraclePriceData, now, numericConstants_1.FIVE_MINUTE // 5MIN
);
const strictOraclePrice = new strictOraclePrice_1.StrictOraclePrice(oraclePriceData.price, twap5min);
const signedTokenValue = (0, spotBalance_1.getStrictTokenValue)(tokenAmount, spotMarketAccount.decimals, strictOraclePrice);
if (signedTokenValue.gt(numericConstants_1.ZERO)) {
result.depositFuel = result.depositFuel.add((0, fuel_1.calculateSpotFuelBonus)(spotMarketAccount, signedTokenValue, fuelBonusNumerator));
}
else {
result.borrowFuel = result.borrowFuel.add((0, fuel_1.calculateSpotFuelBonus)(spotMarketAccount, signedTokenValue, fuelBonusNumerator));
}
}
for (const perpPosition of this.getActivePerpPositions()) {
const oraclePriceData = this.getMMOracleDataForPerpMarket(perpPosition.marketIndex);
const perpMarketAccount = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex);
const baseAssetValue = this.getPerpPositionValue(perpPosition.marketIndex, oraclePriceData, false);
result.positionFuel = result.positionFuel.add((0, fuel_1.calculatePerpFuelBonus)(perpMarketAccount, baseAssetValue, fuelBonusNumerator));
}
}
}
result.insuranceFuel = userStats.getInsuranceFuelBonus(now, includeSettled, includeUnsettled);
return result;
}
getSpotMarketAssetAndLiabilityValue(marketIndex, marginCategory, liquidationBuffer, includeOpenOrders, strict = false, now) {
now = now || new anchor_1.BN(new Date().getTime() / 1000);
let netQuoteValue = numericConstants_1.ZERO;
let totalAssetValue = numericConstants_1.ZERO;
let totalLiabilityValue = numericConstants_1.ZERO;
for (const spotPosition of this.getUserAccount().spotPositions) {
const countForBase = marketIndex === undefined || spotPosition.marketIndex === marketIndex;
const countForQuote = marketIndex === undefined ||
marketIndex === numericConstants_1.QUOTE_SPOT_MARKET_INDEX ||
(includeOpenOrders && spotPosition.openOrders !== 0);
if ((0, spotPosition_1.isSpotPositionAvailable)(spotPosition) ||
(!countForBase && !countForQuote)) {
continue;
}
const spotMarketAccount = this.driftClient.getSpotMarketAccount(spotPosition.marketIndex);
const oraclePriceData = this.getOracleDataForSpotMarket(spotPosition.marketIndex);
let twap5min;
if (strict) {
twap5min = (0, oracles_1.calculateLiveOracleTwap)(spotMarketAccount.historicalOracleData, oraclePriceData, now, numericConstants_1.FIVE_MINUTE // 5MIN
);
}
const strictOraclePrice = new strictOraclePrice_1.StrictOraclePrice(oraclePriceData.price, twap5min);
if (spotPosition.marketIndex === numericConstants_1.QUOTE_SPOT_MARKET_INDEX &&
countForQuote) {
const tokenAmount = (0, spotBalance_1.getSignedTokenAmount)((0, spotBalance_2.getTokenAmount)(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType), spotPosition.balanceType);
if ((0, types_1.isVariant)(spotPosition.balanceType, 'borrow')) {
const weightedTokenValue = this.getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs();
netQuoteValue = netQuoteValue.sub(weightedTokenValue);
}
else {
const weightedTokenValue = this.getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory);
netQuoteValue = netQuoteValue.add(weightedTokenValue);
}
continue;
}
if (!includeOpenOrders && countForBase) {
if ((0, types_1.isVariant)(spotPosition.balanceType, 'borrow')) {
const tokenAmount = (0, spotBalance_1.getSignedTokenAmount)((0, spotBalance_2.getTokenAmount)(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType), types_2.SpotBalanceType.BORROW);
const liabilityValue = this.getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs();
totalLiabilityValue = totalLiabilityValue.add(liabilityValue);
continue;
}
else {
const tokenAmount = (0, spotBalance_2.getTokenAmount)(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType);
const assetValue = this.getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory);
totalAssetValue = totalAssetValue.add(assetValue);
continue;
}
}
const { tokenAmount: worstCaseTokenAmount, ordersValue: worstCaseQuoteTokenAmount, } = (0, spotPosition_1.getWorstCaseTokenAmounts)(spotPosition, spotMarketAccount, strictOraclePrice, marginCategory, this.getUserAccount().maxMarginRatio);
if (worstCaseTokenAmount.gt(numericConstants_1.ZERO) && countForBase) {
const baseAssetValue = this.getSpotAssetValue(worstCaseTokenAmount, strictOraclePrice, spotMarketAccount, marginCategory);
totalAssetValue = totalAssetValue.add(baseAssetValue);
}
if (worstCaseTokenAmount.lt(numericConstants_1.ZERO) && countForBase) {
const baseLiabilityValue = this.getSpotLiabilityValue(worstCaseTokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs();
totalLiabilityValue = totalLiabilityValue.add(baseLiabilityValue);
}
if (worstCaseQuoteTokenAmount.gt(numericConstants_1.ZERO) && countForQuote) {
netQuoteValue = netQuoteValue.add(worstCaseQuoteTokenAmount);
}
if (worstCaseQuoteTokenAmount.lt(numericConstants_1.ZERO) && countForQuote) {
let weight = numericConstants_1.SPOT_MARKET_WEIGHT_PRECISION;
if (marginCategory === 'Initial') {
weight = anchor_1.BN.max(weight, new anchor_1.BN(this.getUserAccount().maxMarginRatio));
}
const weightedTokenValue = worstCaseQuoteTokenAmount
.abs()
.mul(weight)
.div(numericConstants_1.SPOT_MARKET_WEIGHT_PRECISION);
netQuoteValue = netQuoteValue.sub(weightedTokenValue);
}
totalLiabilityValue = totalLiabilityValue.add(new anchor_1.BN(spotPosition.openOrders).mul(numericConstants_1.OPEN_ORDER_MARGIN_REQUIREMENT));
}
if (marketIndex === undefined || marketIndex === numericConstants_1.QUOTE_SPOT_MARKET_INDEX) {
if (netQuoteValue.gt(numericConstants_1.ZERO)) {
totalAssetValue = totalAssetValue.add(netQuoteValue);
}
else {
totalLiabilityValue = totalLiabilityValue.add(netQuoteValue.abs());
}
}
return { totalAssetValue, totalLiabilityValue };
}
getSpotMarketLiabilityValue(marketIndex, marginCategory, liquidationBuffer, includeOpenOrders, strict = false, now) {
const { totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(marketIndex, marginCategory, liquidationBuffer, includeOpenOrders, strict, now);
return totalLiabilityValue;
}
getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer) {
return (0, spotBalance_2.getSpotLiabilityValue)(tokenAmount, strictOraclePrice, spotMarketAccount, this.getUserAccount().maxMarginRatio, marginCategory, liquidationBuffer);
}
getSpotMarketAssetValue(marketIndex, marginCategory, includeOpenOrders, strict = false, now) {
const { totalAssetValue } = this.getSpotMarketAssetAndLiabilityValue(marketIndex, marginCategory, undefined, includeOpenOrders, strict, now);
return totalAssetValue;
}
getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory) {
return (0, spotBalance_2.getSpotAssetValue)(tokenAmount, strictOraclePrice, spotMarketAccount, this.getUserAccount().maxMarginRatio, marginCategory);
}
getSpotPositionValue(marketIndex, marginCategory, includeOpenOrders, strict = false, now) {
const { totalAssetValue, totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(marketIndex, marginCategory, undefined, includeOpenOrders, strict, now);
return totalAssetValue.sub(totalLiabilityValue);
}
getNetSpotMarketValue(withWeightMarginCategory) {
const { totalAssetValue, totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(undefined, withWeightMarginCategory);
return totalAssetValue.sub(totalLiabilityValue);
}
/**
* calculates TotalCollateral: collateral + unrealized pnl
* @returns : Precision QUOTE_PRECISION
*/
getTotalCollateral(marginCategory = 'Initial', strict = false, includeOpenOrders = true, liquidationBuffer, perpMarketIndex) {
const liquidationBufferMap = (() => {
if (liquidationBuffer && perpMarketIndex !== undefined) {
return new Map([[perpMarketIndex, liquidationBuffer]]);
}
else if (liquidationBuffer) {
return new Map([['cross', liquidationBuffer]]);
}
return new Map();
})();
const marginCalc = this.getMarginCalculation(marginCategory, {
strict,
includeOpenOrders,
liquidationBufferMap,
});
if (perpMarketIndex !== undefined) {
const { totalCollateral, totalCollateralBuffer } = marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
if (liquidationBuffer === null || liquidationBuffer === void 0 ? void 0 : liquidationBuffer.gt(numericConstants_1.ZERO)) {
return totalCollateralBuffer;
}
return totalCollateral;
}
if (liquidationBuffer === null || liquidationBuffer === void 0 ? void 0 : liquidationBuffer.gt(numericConstants_1.ZERO)) {
return marginCalc.totalCollateralBuffer;
}
return marginCalc.totalCollateral;
}
getLiquidationBuffer() {
const liquidationBufferMap = new Map();
if (this.isBeingLiquidated()) {
liquidationBufferMap.set('cross', new anchor_1.BN(this.driftClient.getStateAccount().liquidationMarginBufferRatio));
}
for (const position of this.getActivePerpPositions()) {
if (position.positionFlag &
(types_2.PositionFlag.BeingLiquidated | types_2.PositionFlag.Bankruptcy)) {
liquidationBufferMap.set(position.marketIndex, new anchor_1.BN(this.driftClient.getStateAccount().liquidationMarginBufferRatio));
}
}
return liquidationBufferMap;
}
/**
* calculates User Health by comparing total collateral and maint. margin requirement
* @returns : number (value from [0, 100])
*/
getHealth(perpMarketIndex) {
if (this.isCrossMarginBeingLiquidated() && !perpMarketIndex) {
return 0;
}
if (perpMarketIndex &&
this.isIsolatedPositionBeingLiquidated(perpMarketIndex)) {
return 0;
}
const marginCalc = this.getMarginCalculation('Maintenance');
let totalCollateral;
let maintenanceMarginReq;
if (perpMarketIndex) {
const isolatedMarginCalc = marginCalc.isolatedMarginCalculations.get(perpMarketIndex);
if (isolatedMarginCalc) {
totalCollateral = isolatedMarginCalc.totalCollateral;
maintenanceMarginReq = isolatedMarginCalc.marginRequirement;
}
}
else {
totalCollateral = marginCalc.totalCollateral;
maintenanceMarginReq = marginCalc.marginRequirement;
}
let health;
if (maintenanceMarginReq.eq(numericConstants_1.ZERO) && totalCollateral.gte(numericConstants_1.ZERO)) {
health = 100;
}
else if (totalCollateral.lte(numericConstants_1.ZERO)) {
health = 0;
}
else {
health = Math.round(Math.min(100, Math.max(0, (1 - maintenanceMarginReq.toNumber() / totalCollateral.toNumber()) *
100)));
}
return health;
}
calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict = false, enteringHighLeverage = undefined) {
const market = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex);
let valuationPrice = this.getOracleDataForPerpMarket(market.marketIndex).price;
if ((0, types_1.isVariant)(market.status, 'settlement')) {
valuationPrice = market.expiryPrice;
}
let baseAssetAmount;
let liabilityValue;
if (includeOpenOrders) {
const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = (0, margin_1.calculateWorstCasePerpLiabilityValue)(perpPosition, market, valuationPrice);
baseAssetAmount = worstCaseBaseAssetAmount;
liabilityValue = worstCaseLiabilityValue;
}
else {
baseAssetAmount = perpPosition.baseAssetAmount;
liabilityValue = (0, margin_1.calculatePerpLiabilityValue)(baseAssetAmount, valuationPrice, (0, types_1.isVariant)(market.contractType, 'prediction'));
}
if (marginCategory) {
const userCustomMargin = Math.max(perpPosition.maxMarginRatio, this.getUserAccount().maxMarginRatio);
let marginRatio = new anchor_1.BN((0, market_1.calculateMarketMarginRatio)(market, baseAssetAmount.abs(), marginCategory, enteringHighLeverage === false
? Math.max(market.marginRatioInitial, userCustomMargin)
: userCustomMargin, this.isHighLeverageMode(marginCategory) ||
enteringHighLeverage === true));
if (liquidationBuffer !== undefined) {
marginRatio = marginRatio.add(liquidationBuffer);
}
if ((0, types_1.isVariant)(market.status, 'settlement')) {
marginRatio = numericConstants_1.ZERO;
}
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(market.quoteSpotMarketIndex);
const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(numericConstants_1.QUOTE_SPOT_MARKET_INDEX);
let quotePrice;
if (strict) {
quotePrice = anchor_1.BN.max(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min);
}
else {
quotePrice = quoteOraclePriceData.price;
}
liabilityValue = liabilityValue
.mul(quotePrice)
.div(numericConstants_1.PRICE_PRECISION)
.mul(marginRatio)
.div(numericConstants_1.MARGIN_PRECISION);
if (includeOpenOrders) {
liabilityValue = liabilityValue.add(new anchor_1.BN(perpPosition.openOrders).mul(numericConstants_1.OPEN_ORDER_MARGIN_REQUIREMENT));
}
}
return liabilityValue;
}
/**
* calculates position value of a single perp market in margin system
* @returns : Precision QUOTE_PRECISION
*/
getPerpMarketLiabilityValue(marketIndex, marginCategory, liquidationBuffer, includeOpenOrders, strict = false) {
const perpPosition = this.getPerpPosition(marketIndex);
return this.calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict);
}
/**
* calculates sum of position value across all positions in margin system
* @returns : Precision QUOTE_PRECISION
*/
getTotalPerpPositionLiability(marginCategory, liquidationBuffer, includeOpenOrders, strict = false, enteringHighLeverage = undefined) {
return this.getActivePerpPositions().reduce((totalPerpValue, perpPosition) => {
const baseAssetValue = this.calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict, enteringHighLeverage);
return totalPerpValue.add(baseAssetValue);
}, numericConstants_1.ZERO);
}
/**
* calculates position value based on oracle
* @returns : Precision QUOTE_PRECISION
*/
getPerpPositionValue(marketIndex, oraclePriceData, includeOpenOrders = false) {
const userPosition = this.getPerpPositionOrEmpty(marketIndex);
const market = this.driftClient.getPerpMarketAccount(userPosition.marketIndex);
return (0, margin_2.calculateBaseAssetValueWithOracle)(market, userPosition, oraclePriceData, includeOpenOrders);
}
/**
* calculates position liabiltiy value in margin system
* @returns : Precision QUOTE_PRECISION
*/
getPerpLiabilityValue(marketIndex, oraclePriceData, includeOpenOrders = false) {
const userPosition = this.getPerpPositionOrEmpty(marketIndex);
const market = this.driftClient.getPerpMarketAccount(userPosition.marketIndex);
if (includeOpenOrders) {
return (0, margin_1.calculateWorstCasePerpLiabilityValue)(userPosition, market, oraclePriceData.price).worstCaseLiabilityValue;
}
else {
return (0, margin_1.calculatePerpLiabilityValue)(userPosition.baseAssetAmount, oraclePriceData.price, (0, types_1.isVariant)(market.contractType, 'prediction'));
}
}
getPositionSide(currentPosition) {
if (currentPosition.baseAssetAmount.gt(numericConstants_1.ZERO)) {
return types_2.PositionDirection.LONG;
}
else if (currentPosition.baseAssetAmount.lt(numericConstants_1.ZERO)) {
return types_2.PositionDirection.SHORT;
}
else {
return undefined;
}
}
/**
* calculates average exit price (optionally for closing up to 100% of position)
* @returns : Precision PRICE_PRECISION
*/
getPositionEstimatedExitPriceAndPnl(position, amountToClose, useAMMClose = false) {
const market = this.driftClient.getPerpMarketAccount(position.marketIndex);
const entryPrice = (0, position_1.calculateEntryPrice)(position);
const oraclePriceData = this.getMMOracleDataForPerpMarket(position.marketIndex);
if (amountToClose) {
if (amountToClose.eq(numericConstants_1.ZERO)) {
return [(0, market_1.calculateReservePrice)(market, oraclePriceData), numericConstants_1.ZERO];
}
position = {
baseAssetAmount: amountToClose,
lastCumulativeFundingRate: position.lastCumulativeFundingRate,
marketIndex: position.marketIndex,
quoteAssetAmount: position.quoteAssetAmount,
};
}
let baseAssetValue;
if (useAMMClose) {
baseAssetValue = (0, position_2.calculateBaseAssetValue)(market, position, oraclePriceData);
}
else {
baseAssetValue = (0, margin_2.calculateBaseAssetValueWithOracle)(market, position, oraclePriceData);
}
if (position.baseAssetAmount.eq(numericConstants_1.ZERO)) {
return [numericConstants_1.ZERO, numericConstants_1.ZERO];
}
const exitPrice = baseAssetValue
.mul(numericConstants_1.AMM_TO_QUOTE_PRECISION_RATIO)
.mul(numericConstants_1.PRICE_PRECISION)
.div(position.baseAssetAmount.abs());
const pnlPerBase = exitPrice.sub(entryPrice);
const pnl = pnlPerBase
.mul(position.baseAssetAmount)
.div(numericConstants_1.PRICE_PRECISION)
.div(numericConstants_1.AMM_TO_QUOTE_PRECISION_RATIO);
return [exitPrice, pnl];
}
/**
* calculates current user leverage which is (total liability size) / (net asset value)
* @returns : Precision TEN_THOUSAND
*/
getLeverage(includeOpenOrders = true, perpMarketIndex) {
return this.calculateLeverageFromComponents(this.getLeverageComponents(includeOpenOrders, undefined, perpMarketIndex));
}
calculateLeverageFromComponents({ perpLiabilityValue, perpPnl, spotAssetValue, spotLiabilityValue, }) {
const totalLiabilityValue = perpLiabilityValue.add(spotLiabilityValue);
const totalAssetValue = spotAssetValue.add(perpPnl);
const netAssetValue = totalAssetValue.sub(spotLiabilityValue);
if (netAssetValue.eq(numericConstants_1.ZERO)) {
return numericConstants_1.ZERO;
}
return totalLiabilityValue.mul(numericConstants_1.TEN_THOUSAND).div(netAssetValue);
}
getLeverageComponents(includeOpenOrders = true, marginCategory = undefined, perpMarketIndex) {
var _a;
if (perpMarketIndex) {
const perpPosition = this.getPerpPositionOrEmpty(perpMarketIndex);
const perpLiability = this.calculateWeightedPerpPositionLiability(perpPosition, marginCategory, undefined, includeOpenOrders);
const perpMarket = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex);
const oraclePriceData = this.getOracleDataForPerpMarket(perpPosition.marketIndex);
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(perpMarket.quoteSpotMarketIndex);
const quoteOraclePriceData = this.getOracleDataForSpotMarket(perpMarket.quoteSpotMarketIndex);
const strictOracle = new strictOraclePrice_1.StrictOraclePrice(quoteOraclePriceData.price, quoteOraclePriceData.twap);
const positionUnrealizedPnl = (0, position_2.calculatePositionPNL)(perpMarket, perpPosition, true, oraclePriceData);
const tokenAmount = (0, spotBalance_2.getTokenAmount)((_a = perpPosition.isolatedPositionScaledBalance) !== null && _a !== void 0 ? _a : numericConstants_1.ZERO, quoteSpotMarket, types_2.SpotBalanceType.DEPOSIT);
const spotAssetValue = (0, spotBalance_1.getStrictTokenValue)(tokenAmount, quoteSpotMarket.decimals, strictOracle);
return {
perpLiabilityValue: perpLiability,
perpPnl: positionUnrealizedPnl,
spotAssetValue,
spotLiabilityValue: numericConstants_1.ZERO,
};
}
const perpLiability = this.getTotalPerpPositionLiability(marginCategory, undefined, includeOpenOrders);
const perpPnl = this.getUnrealizedPNL(true, undefined, marginCategory);
const { totalAssetValue: spotAssetValue, totalLiabilityValue: spotLiabilityValue, } = this.getSpotMarketAssetAndLiabilityValue(undefined, marginCategory, undefined, includeOpenOrders);
return {
perpLiabilityValue: perpLiability,
perpPnl,
spotAssetValue,
spotLiabilityValue,
};
}
isDustDepositPosition(spotMarketAccount) {
const marketIndex = spotMarketAccount.marketIndex;
const spotPosition = this.getSpotPosition(spotMarketAccount.marketIndex);
if ((0, spotPosition_1.isSpotPositionAvailable)(spotPosition)) {
return false;
}
const depositAmount = this.getTokenAmount(spotMarketAccount.marketIndex);
if (depositAmount.lte(numericConstants_1.ZERO)) {
return false;
}
const oraclePriceData = this.getOracleDataForSpotMarket(marketIndex);
const strictOraclePrice = new strictOraclePrice_1.StrictOraclePrice(oraclePriceData.price, oraclePriceData.twap);
const balanceValue = this.getSpotAssetValue(depositAmount, strictOraclePrice, spotMarketAccount);
if (balanceValue.lt(numericConstants_1.DUST_POSITION_SIZE)) {
return true;
}
return false;
}
getSpotMarketAccountsWithDustPosition() {
const spotMarketAccounts = this.driftClient.getSpotMarketAccounts();
const dustPositionAccounts = [];
for (const spotMarketAccount of spotMarketAccounts) {
const isDust = this.isDustDepositPosition(spotMarketAccount);
if (isDust) {
dustPositionAccounts.push(spotMarketAccount);
}
}
return dustPositionAccounts;
}
getTotalLiabilityValue(marginCategory) {
return this.getTotalPerpPositionLiability(marginCategory, undefined, true).add(this.getSpotMarketLiabilityValue(undefined, marginCategory, undefined, true));
}
getTotalAssetValue(marginCategory) {
return this.getSpotMarketAssetValue(undefined, marginCategory, true).add(this.getUnrealizedPNL(true, undefined, marginCategory));
}
getNetUsdValue() {
const netSpotValue = this.getNetSpotMarketValue();