UNPKG

@drift-labs/common

Version:

Common functions for Drift

608 lines 24.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.COMMON_UTILS = exports.getTieredSortScore = exports.aprFromApy = exports.MultiSwitch = exports.Counter = exports.Ref = exports.getAnchorEnumString = exports.orderIsNull = exports.getPriceForUIOrderRecord = exports.getPriceForOrderRecord = exports.getPriceForBaseAndQuoteAmount = exports.filterTradeRecordsFromUIOrderRecords = exports.filterTradeRecordsFromOrderActionRecords = exports.uiOrderActionRecordIsTrade = exports.orderActionRecordIsTrade = exports.getUIOrderRecordsLaterThanTarget = exports.getLatestOfTwoOrderRecords = exports.getLatestOfTwoUIOrderRecords = exports.sortOrderRecords = exports.sortUIOrderRecords = exports.sortUIOrderActionRecords = exports.sortUIMatchedOrderRecordAndAction = exports.getSortScoreForOrderActionRecords = exports.getTradeInfoFromActionRecord = exports.getSortScoreForOrderRecords = exports.decodeStringifiableObject = exports.encodeStringifiableObject = exports.sleep = exports.ENUM_UTILS = exports.matchEnum = void 0; const sdk_1 = require("@drift-labs/sdk"); const insuranceFund_1 = require("./insuranceFund"); const matchEnum = (enum1, enum2) => { return JSON.stringify(enum1) === JSON.stringify(enum2); }; exports.matchEnum = matchEnum; function enumToObj(enumStr) { if (!enumStr) return undefined; return { [enumStr !== null && enumStr !== void 0 ? enumStr : '']: {}, }; } function enumToStr(enumStr) { var _a; return (_a = Object.keys(enumStr !== null && enumStr !== void 0 ? enumStr : {})) === null || _a === void 0 ? void 0 : _a[0]; } exports.ENUM_UTILS = { match: exports.matchEnum, toObj: enumToObj, toStr: enumToStr, }; async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } exports.sleep = sleep; const getStringifiableObjectEntry = (value) => { // If BN // if (value instanceof BN) { /* This method would be much safer but don't think it's possible to ensure that instances of classes match when they're in different npm packages */ if (Object.keys(value).sort().join(',') === 'length,negative,red,words') { return [value.toString(), '_bgnm_']; } // If PublicKey // if (value instanceof PublicKey) { { /* This method would be much safer but don't think it's possible to ensure that instances of classes match when they're in different npm packages */ if (Object.keys(value).sort().join(',') === '_bn') { return [value.toString(), '_pbky_']; } if (typeof value === 'object') { return [(0, exports.encodeStringifiableObject)(value), '']; } return [value, '']; }; /** * Converts an objects with potential Pubkeys and BNs in it into a form that can be JSON stringified. When pubkeys get converted a _pbky_ suffix will be added to their key, and _bgnm_ for BNs. * * e.g. * input : { * QuoteAmount: BN * } * * output: { * _bgnm_QuoteAmount: string * } * @param value * @returns */ const encodeStringifiableObject = (value) => { if (typeof value !== 'object') return value; if (Array.isArray(value)) { return value.map((entry) => (0, exports.encodeStringifiableObject)(entry)); } const buildJsonObject = {}; if (!value) return value; Object.entries(value).forEach(([key, val]) => { const [convertedVal, keyTag] = getStringifiableObjectEntry(val); buildJsonObject[`${keyTag}${key}`] = convertedVal; }); return buildJsonObject; }; exports.encodeStringifiableObject = encodeStringifiableObject; /** * Converts a parsed object with potential Pubkeys and BNs in it (in string form) to their proper form. Pubkey values must have a key starting in _pbky_ and BN values must have a key starting in _bnnm_ * * * e.g. * input : { * _bgnm_QuoteAmount: string * } * * output: { * QuoteAmount: BN * } * @param value * @returns */ const decodeStringifiableObject = (value) => { if (typeof value !== 'object') return value; if (Array.isArray(value)) { return value.map((entry) => (0, exports.decodeStringifiableObject)(entry)); } const buildJsonObject = {}; Object.entries(value) .filter((val) => val != undefined && val != null) .forEach(([key, val]) => { if (key.match(/^_pbky_/)) { buildJsonObject[key.replace('_pbky_', '')] = new sdk_1.PublicKey(val); return; } if (key.match(/^_bgnm_/)) { buildJsonObject[key.replace('_bgnm_', '')] = new sdk_1.BN(val); return; } if (typeof val === 'object' && val != undefined && val != null) { buildJsonObject[key] = (0, exports.decodeStringifiableObject)(val); return; } buildJsonObject[key] = val; }); return buildJsonObject; }; exports.decodeStringifiableObject = decodeStringifiableObject; const getChronologicalValueForOrderAction = (action) => { return (0, exports.matchEnum)(action, sdk_1.OrderAction.PLACE) ? 0 : (0, exports.matchEnum)(action, sdk_1.OrderAction.FILL) ? 1 : 2; }; /** * Returns 1 if the first Order is chronologically later than the second Order, -1 if before, 0 if equal * @param orderA * @param orderB * @returns */ const getSortScoreForOrderRecords = (orderA, orderB) => { if (orderA.slot !== orderB.slot) { return orderA.slot > orderB.slot ? 1 : -1; } return 0; }; exports.getSortScoreForOrderRecords = getSortScoreForOrderRecords; const getTradeInfoFromActionRecord = (actionRecord) => { return { ts: actionRecord.ts, baseAssetAmount: actionRecord.taker ? actionRecord.takerOrderBaseAssetAmount : actionRecord.makerOrderBaseAssetAmount, baseAssetAmountFilled: actionRecord.taker ? actionRecord.takerOrderCumulativeBaseAssetAmountFilled : actionRecord.makerOrderCumulativeBaseAssetAmountFilled, quoteAssetAmountFilled: actionRecord.taker ? actionRecord.takerOrderCumulativeQuoteAssetAmountFilled : actionRecord.makerOrderCumulativeQuoteAssetAmountFilled, }; }; exports.getTradeInfoFromActionRecord = getTradeInfoFromActionRecord; /** * Returns 1 if the first Order is chronologically later than the second Order, -1 if before, 0 if equal * @param orderA * @param orderB * @returns */ const getSortScoreForOrderActionRecords = (orderA, orderB) => { if (orderA.slot !== orderB.slot) { return orderA.slot > orderB.slot ? 1 : -1; } if (!(0, exports.matchEnum)(orderA.action, orderB.action)) { // @ts-ignore const orderAActionVal = getChronologicalValueForOrderAction(orderA.action); // @ts-ignore const orderBActionVal = getChronologicalValueForOrderAction(orderB.action); return orderAActionVal > orderBActionVal ? 1 : -1; } // @ts-ignore if (orderA.fillRecordId && orderB.fillRecordId) { if (!orderA.fillRecordId.eq(orderB.fillRecordId)) { // @ts-ignore return orderA.fillRecordId.gt(orderB.fillRecordId) ? 1 : -1; } } return 0; }; exports.getSortScoreForOrderActionRecords = getSortScoreForOrderActionRecords; const sortUIMatchedOrderRecordAndAction = (records, direction = 'desc') => { const ascSortedRecords = records.sort((a, b) => (0, exports.getSortScoreForOrderActionRecords)(a.actionRecord, b.actionRecord)); return direction === 'desc' ? ascSortedRecords.reverse() : ascSortedRecords; }; exports.sortUIMatchedOrderRecordAndAction = sortUIMatchedOrderRecordAndAction; const sortUIOrderActionRecords = (records, direction = 'desc') => { const ascSortedRecords = records.sort(exports.getSortScoreForOrderActionRecords); return direction === 'desc' ? ascSortedRecords.reverse() : ascSortedRecords; }; exports.sortUIOrderActionRecords = sortUIOrderActionRecords; const sortUIOrderRecords = (records, direction = 'desc') => { const ascSortedRecords = records.sort(exports.getSortScoreForOrderRecords); return direction === 'desc' ? ascSortedRecords.reverse() : ascSortedRecords; }; exports.sortUIOrderRecords = sortUIOrderRecords; const sortOrderRecords = (records, direction = 'desc') => { const ascSortedRecords = records.sort(exports.getSortScoreForOrderRecords); return direction === 'desc' ? ascSortedRecords.reverse() : ascSortedRecords; }; exports.sortOrderRecords = sortOrderRecords; const getLatestOfTwoUIOrderRecords = (orderA, orderB) => { return (0, exports.getSortScoreForOrderRecords)(orderA, orderB) === 1 ? orderA : orderB; }; exports.getLatestOfTwoUIOrderRecords = getLatestOfTwoUIOrderRecords; const getLatestOfTwoOrderRecords = (orderA, orderB) => { return (0, exports.getSortScoreForOrderRecords)(orderA, orderB) === 1 ? orderA : orderB; }; exports.getLatestOfTwoOrderRecords = getLatestOfTwoOrderRecords; const getUIOrderRecordsLaterThanTarget = (target, records) => records.filter((record) => (0, exports.getLatestOfTwoUIOrderRecords)(record, target) === record); exports.getUIOrderRecordsLaterThanTarget = getUIOrderRecordsLaterThanTarget; // Trade records are order records which have been filled const orderActionRecordIsTrade = (orderRecord) => orderRecord.baseAssetAmountFilled.gt(sdk_1.ZERO) && // @ts-ignore (0, exports.matchEnum)(orderRecord.action, sdk_1.OrderAction.FILL) && true; exports.orderActionRecordIsTrade = orderActionRecordIsTrade; const uiOrderActionRecordIsTrade = (orderRecord) => orderRecord.baseAssetAmountFilled.gtZero() && (0, exports.matchEnum)(orderRecord.action, sdk_1.OrderAction.FILL) && true; exports.uiOrderActionRecordIsTrade = uiOrderActionRecordIsTrade; // Trade records are order records which have been filled const filterTradeRecordsFromOrderActionRecords = (orderRecords) => orderRecords.filter(exports.orderActionRecordIsTrade); exports.filterTradeRecordsFromOrderActionRecords = filterTradeRecordsFromOrderActionRecords; // Trade records are order records which have been filled const filterTradeRecordsFromUIOrderRecords = (orderRecords) => orderRecords.filter(exports.uiOrderActionRecordIsTrade); exports.filterTradeRecordsFromUIOrderRecords = filterTradeRecordsFromUIOrderRecords; /** * Returns the average price for a given base amount and quote amount. * @param quoteAmount * @param baseAmount * @returns PRICE_PRECISION */ const getPriceForBaseAndQuoteAmount = (quoteAmount, baseAmount) => { return quoteAmount .mul(sdk_1.PRICE_PRECISION) .mul(sdk_1.BASE_PRECISION) .div(sdk_1.QUOTE_PRECISION) .div(sdk_1.BigNum.from(baseAmount, sdk_1.BASE_PRECISION_EXP).val); }; exports.getPriceForBaseAndQuoteAmount = getPriceForBaseAndQuoteAmount; const getPriceForOrderRecord = (orderRecord) => { return (0, exports.getPriceForBaseAndQuoteAmount)( // @ts-ignore orderRecord.quoteAssetAmountFilled, // @ts-ignore orderRecord.baseAssetAmountFilled); }; exports.getPriceForOrderRecord = getPriceForOrderRecord; const getPriceForUIOrderRecord = (orderRecord) => { return orderRecord.quoteAssetAmountFilled .shiftTo(sdk_1.AMM_RESERVE_PRECISION_EXP) .shift(sdk_1.PRICE_PRECISION_EXP) .div(orderRecord.baseAssetAmountFilled.shiftTo(sdk_1.BASE_PRECISION_EXP)) .shiftTo(sdk_1.PRICE_PRECISION_EXP); }; exports.getPriceForUIOrderRecord = getPriceForUIOrderRecord; const orderIsNull = (order, side) => { return side === 'taker' ? !order.taker : !order.maker; }; exports.orderIsNull = orderIsNull; const getAnchorEnumString = (enumVal) => { return Object.keys(enumVal)[0]; }; exports.getAnchorEnumString = getAnchorEnumString; class Ref { constructor(val) { this.val = val; } set(val) { this.val = val; } get() { return this.val; } } exports.Ref = Ref; class Counter { constructor() { this.val = 0; } get() { return this.val; } increment(value = 1) { this.val += value; } reset() { this.val = 0; } } exports.Counter = Counter; /** * A class which allows a group of switches to seperately turn a multiswitch on or off. The base state is the state of the "multiswitch" when all of the constituent switches are off. When any of the switches are "on" then the multiswitch flips to the opposite state * * If baseState is on => any switch being "on" will turn the multiswitch off. * If baseState is off => any switch being "off" will turn the multiswitch off. */ class MultiSwitch { constructor(baseState = 'off') { this.baseState = baseState; this.switches = []; this.switchValue = 0; } getSwitchKey(key) { // If first time using switch, add to list of switches if (!this.switches.includes(key)) { this.switches.push(key); } const switchIndex = this.switches.indexOf(key); return 2 ** switchIndex; } switchOn(key) { if (this.baseState === 'on') { this._switchOff(key); return; } this._switchOn(key); } switchOff(key) { if (this.baseState === 'on') { this._switchOn(key); return; } this._switchOff(key); } _switchOff(key) { const switchKey = this.getSwitchKey(key); this.switchValue &= ~switchKey; } _switchOn(key) { const switchKey = this.getSwitchKey(key); this.switchValue |= switchKey; } get isOn() { // When the base state is on, then if any switch is on the multi-switch is off if (this.baseState === 'on') { return this.switchValue === 0; } if (this.baseState === 'off') { return this.switchValue > 0; } } } exports.MultiSwitch = MultiSwitch; /** * Returns the quote amount of the current open interest for a market, using the current oracle price * @param marketIndex * @param marketType * @param driftClient * @returns */ const getCurrentOpenInterestForMarket = (marketIndex, marketType, driftClient) => { if (exports.ENUM_UTILS.match(marketType, sdk_1.MarketType.PERP)) { const market = driftClient.getPerpMarketAccount(marketIndex); const OI = sdk_1.BigNum.from(market.amm.baseAssetAmountLong.add(market.amm.baseAssetAmountShort.abs()), sdk_1.BASE_PRECISION_EXP); const priceData = driftClient.getOraclePriceDataAndSlot(market.amm.oracle, market.amm.oracleSource); const price = sdk_1.BigNum.from(priceData.data.price, sdk_1.PRICE_PRECISION_EXP); const quoteOIforMarket = price.toNum() * OI.toNum(); return quoteOIforMarket; } else { throw new Error('Invalid market type for Open Interest calculation'); } }; /** * Gets the deposit APR for a spot market, in percent * @param marketIndex * @param marketType * @param driftClient * @returns */ const getDepositAprForMarket = (marketIndex, marketType, driftClient) => { if (exports.ENUM_UTILS.match(marketType, sdk_1.MarketType.SPOT)) { const marketAccount = driftClient.getSpotMarketAccount(marketIndex); const depositApr = sdk_1.BigNum.from((0, sdk_1.calculateDepositRate)(marketAccount), sdk_1.SPOT_MARKET_RATE_PRECISION_EXP); const depositAprPct = depositApr.toNum() * 100; return depositAprPct; } else { throw new Error('Invalid market type for Deposit APR calculation'); } }; /** * Get's the borrow APR for a spot market, in percent * @param marketIndex * @param marketType * @param driftClient * @returns */ const getBorrowAprForMarket = (marketIndex, marketType, driftClient) => { if (exports.ENUM_UTILS.match(marketType, sdk_1.MarketType.SPOT)) { const marketAccount = driftClient.getSpotMarketAccount(marketIndex); const depositApr = sdk_1.BigNum.from((0, sdk_1.calculateBorrowRate)(marketAccount), sdk_1.SPOT_MARKET_RATE_PRECISION_EXP); const depositAprPct = depositApr.toNum() * 100; return depositAprPct; } else { throw new Error('Invalid market type for Borrow APR calculation'); } }; const getTotalBorrowsForMarket = (market, driftClient) => { const marketAccount = driftClient.getSpotMarketAccount(market.marketIndex); const totalBorrowsTokenAmount = (0, sdk_1.getTokenAmount)(marketAccount.borrowBalance, driftClient.getSpotMarketAccount(marketAccount.marketIndex), sdk_1.SpotBalanceType.BORROW); const totalBorrowsAmountBigNum = sdk_1.BigNum.from(totalBorrowsTokenAmount, market.precisionExp); const priceData = driftClient.getOraclePriceDataAndSlot(marketAccount.oracle, marketAccount.oracleSource); const price = sdk_1.BigNum.from(priceData.data.price, sdk_1.PRICE_PRECISION_EXP); const totalBorrowsQuote = price.toNum() * totalBorrowsAmountBigNum.toNum(); return Number(totalBorrowsQuote.toFixed(2)); }; const getTotalDepositsForMarket = (market, driftClient) => { const marketAccount = driftClient.getSpotMarketAccount(market.marketIndex); const totalDepositsTokenAmount = (0, sdk_1.getTokenAmount)(marketAccount.depositBalance, marketAccount, sdk_1.SpotBalanceType.DEPOSIT); const totalDepositsTokenAmountBigNum = sdk_1.BigNum.from(totalDepositsTokenAmount, market.precisionExp); const priceData = driftClient.getOraclePriceDataAndSlot(marketAccount.oracle, marketAccount.oracleSource); const price = sdk_1.BigNum.from(priceData.data.price, sdk_1.PRICE_PRECISION_EXP); const totalDepositsBase = totalDepositsTokenAmountBigNum.toNum(); const totalDepositsQuote = price.toNum() * totalDepositsTokenAmountBigNum.toNum(); return { totalDepositsBase, totalDepositsQuote, }; }; /** * Check if numbers divide exactly, accounting for floating point division annoyingness * @param numerator * @param denominator * @returns */ const dividesExactly = (numerator, denominator) => { const division = numerator / denominator; const remainder = division % 1; if (remainder === 0) return true; // Because of floating point weirdness, we're just going to assume that if the remainder after dividing is less than 1/10^6 then the numbers do divide exactly if (Math.abs(remainder - 1) < 1 / 10 ** 6) return true; return false; }; const toSnakeCase = (str) => str.replace(/[^\w]/g, '_').toLowerCase(); const toCamelCase = (str) => { const words = str.split(/[_\-\s]+/); // split on underscores, hyphens, and spaces const firstWord = words[0].toLowerCase(); const restWords = words .slice(1) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); return [firstWord, ...restWords].join(''); }; const aprFromApy = (apy, compoundsPerYear) => { const compoundedAmount = 1 + apy * 0.01; const estimatedApr = (Math.pow(compoundedAmount, 1 / compoundsPerYear) - 1) * compoundsPerYear; return estimatedApr * 100; }; exports.aprFromApy = aprFromApy; /** * Helper utility to get a sort score for "tiered" parameters. * * Example: Want to sort students by Grade, but fall back to using Age if they are equal. This method will accept an array for each student of [grade, age] and return the appropriate sort score for each. * * @param aScores * @param bScores * @returns */ const getTieredSortScore = (aScores, bScores) => { var _a, _b; const maxIndex = Math.max(aScores.length, bScores.length); for (let i = 0; i < maxIndex; i++) { const aScore = (_a = aScores[i]) !== null && _a !== void 0 ? _a : Number.MIN_SAFE_INTEGER; const bScore = (_b = bScores[i]) !== null && _b !== void 0 ? _b : Number.MIN_SAFE_INTEGER; if (aScore !== bScore) return aScore - bScore; } return 0; }; exports.getTieredSortScore = getTieredSortScore; const normalizeBaseAssetSymbol = (symbol) => { return symbol.replace(/^1M/, ''); }; /** * Returns the number of standard deviations between a target value and the history of values to compare it to. * @param target * @param previousValues * @returns */ const calculateZScore = (target, previousValues) => { const meanValue = calculateMean(previousValues); const standardDeviationValue = calculateStandardDeviation(previousValues, meanValue); const zScore = (target - meanValue) / standardDeviationValue; return zScore; }; const calculateMean = (numbers) => { const sum = numbers.reduce((total, num) => total + num, 0); return sum / numbers.length; }; const calculateMedian = (numbers) => { const sortedNumbers = numbers.sort(); const middleIndex = Math.floor(sortedNumbers.length / 2); if (sortedNumbers.length % 2 === 0) { return (sortedNumbers[middleIndex - 1] + sortedNumbers[middleIndex]) / 2; } else { return sortedNumbers[middleIndex]; } }; const calculateStandardDeviation = (numbers, mean) => { const squaredDifferences = numbers.map((num) => Math.pow(num - mean, 2)); const sumSquaredDifferences = squaredDifferences.reduce((total, diff) => total + diff, 0); const variance = sumSquaredDifferences / numbers.length; return Math.sqrt(variance); }; const glueArray = (size, elements) => { const gluedElements = []; elements.forEach((element, index) => { const gluedIndex = Math.floor(index / size); if (gluedElements[gluedIndex]) { gluedElements[gluedIndex].push(element); } else { gluedElements[gluedIndex] = [element]; } }); return gluedElements; }; const bnMin = (numbers) => { let min = numbers[0]; for (let i = 1; i < numbers.length; i++) { if (numbers[i].lt(min)) { min = numbers[i]; } } return min; }; const bnMax = (numbers) => { let max = numbers[0]; for (let i = 1; i < numbers.length; i++) { if (numbers[i].gt(max)) { max = numbers[i]; } } return max; }; const bnMedian = (numbers) => { const sortedNumbers = numbers.sort((a, b) => a.cmp(b)); const middleIndex = Math.floor(sortedNumbers.length / 2); if (sortedNumbers.length % 2 === 0) { return sortedNumbers[middleIndex - 1] .add(sortedNumbers[middleIndex]) .div(new sdk_1.BN(2)); } else { return sortedNumbers[middleIndex]; } }; const bnMean = (numbers) => { let sum = new sdk_1.BN(0); for (let i = 0; i < numbers.length; i++) { sum = sum.add(numbers[i]); } return sum.div(new sdk_1.BN(numbers.length)); }; const timedPromise = async (promise) => { const start = Date.now(); const promiseResult = await promise; return { promiseTime: Date.now() - start, promiseResult, }; }; const chunks = (array, size) => { return new Array(Math.ceil(array.length / size)) .fill(null) .map((_, index) => index * size) .map((begin) => array.slice(begin, begin + size)); }; const getMultipleAccountsInfoChunked = async (connection, accounts) => { const accountChunks = chunks(accounts, 100); // 100 is limit for getMultipleAccountsInfo const responses = await Promise.all(accountChunks.map((chunk) => connection.getMultipleAccountsInfo(chunk))); return responses.flat(); }; exports.COMMON_UTILS = { getIfVaultBalance: insuranceFund_1.getIfVaultBalance, getIfStakingVaultApr: insuranceFund_1.getIfStakingVaultApr, getCurrentOpenInterestForMarket, getDepositAprForMarket, getBorrowAprForMarket, getTotalBorrowsForMarket, getTotalDepositsForMarket, dividesExactly, toSnakeCase, toCamelCase, getTieredSortScore: exports.getTieredSortScore, normalizeBaseAssetSymbol, calculateZScore, glueArray, timedPromise, chunks, getMultipleAccountsInfoChunked, MATH: { NUM: { mean: calculateMean, median: calculateMedian, }, BN: { bnMax, bnMin, bnMean, bnMedian, }, }, }; //# sourceMappingURL=index.js.map