UNPKG

@d8x/perpetuals-sdk

Version:

Node TypeScript SDK for D8X Perpetual Futures

992 lines (991 loc) 86.3 kB
import { Contract, JsonRpcProvider, Network, ZeroAddress, } from "ethers"; import { BUY_SIDE, CLOSED_SIDE, COLLATERAL_CURRENCY_BASE, COLLATERAL_CURRENCY_QUOTE, CollaterlCCY, DEFAULT_CONFIG, MASK_CLOSE_ONLY, MASK_KEEP_POS_LEVERAGE, MASK_LIMIT_ORDER, MASK_LOW_LIQUIDITY_MARKET, MASK_MARKET_ORDER, MASK_PREDICTION_MARKET, MASK_STOP_ORDER, MASK_TRADFI_MARKET, MAX_64x64, MULTICALL_ADDRESS, ORDER_MAX_DURATION_SEC, ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET, ORDER_TYPE_STOP_LIMIT, ORDER_TYPE_STOP_MARKET, PERP_STATE_STR, SELL_SIDE, SYMBOL_LIST, ZERO_ADDRESS, ZERO_ORDER_ID, } from "./constants"; import { ERC20__factory, IPerpetualManager__factory, LimitOrderBookFactory__factory, LimitOrderBook__factory, Multicall3__factory, OracleFactory__factory, } from "./contracts"; import { ABDK29ToFloat, ABK64x64ToFloat, calculateLiquidationPriceCollateralBase, calculateLiquidationPriceCollateralQuanto, calculateLiquidationPriceCollateralQuote, div64x64, floatToABK64x64, dec18ToFloat, priceToProb, probToPrice, pmFindLiquidationPrice, pmMaintenanceMarginRate, } from "./d8XMath"; import PriceFeeds from "./priceFeeds"; import { combineFlags, containsFlag, contractSymbolToSymbol, loadConfigAbis, symbol4BToLongSymbol, fromBytes4, to4Chars, } from "./utils"; /** * Parent class for MarketData and WriteAccessHandler that handles * common data and chain operations. */ export default class PerpetualDataHandler { /** * Constructor * @param {NodeSDKConfig} config Configuration object, see * PerpetualDataHandler.readSDKConfig. */ constructor(config) { this.PRICE_UPDATE_FEE_GWEI = 1; this.requiredSymbols = []; // array of symbols in the current perpetual deployment this.proxyContract = null; // limit order book this.lobFactoryContract = null; // multicall this.multicall = null; this.provider = null; this.signerOrProvider = null; this.settlementConfig = require("./config/settlement.json"); this.config = config; this.symbolToPerpStaticInfo = new Map(); this.poolStaticInfos = new Array(); this.symbolToTokenAddrMap = new Map(); this.perpetualIdToSymbol = new Map(); this.nestedPerpetualIDs = new Array(); this.chainId = BigInt(config.chainId); this.network = new Network(config.name || "", this.chainId); this.proxyAddr = config.proxyAddr; this.nodeURL = config.nodeURL; this.proxyABI = config.proxyABI; this.lobFactoryABI = config.lobFactoryABI; this.lobABI = config.lobABI; this.shareTokenABI = config.shareTokenABI; this.symbolList = SYMBOL_LIST; this.priceFeedGetter = new PriceFeeds(this, config.priceFeedConfigNetwork); } async initContractsAndData(signerOrProvider, overrides) { await this.fetchSymbolList(); this.signerOrProvider = signerOrProvider; // check network let network; try { if (signerOrProvider.provider) { network = await signerOrProvider.provider.getNetwork(); } else { throw new Error("Signer has no provider"); // TODO: check } } catch (error) { console.error(error); throw new Error(`Unable to connect to network.`); } if (network.chainId !== this.chainId) { throw new Error(`Provider: chain id ${network.chainId} does not match config (${this.chainId})`); } this.proxyContract = IPerpetualManager__factory.connect(this.proxyAddr, signerOrProvider); this.multicall = Multicall3__factory.connect(this.config.multicall ?? MULTICALL_ADDRESS, this.signerOrProvider); await this._fillSymbolMaps(overrides); } /** * sets the symbollist if a remote config url is specified */ async fetchSymbolList() { if (this.config.configSource == "" || this.config.configSource == undefined) { return; } const res = await fetch(this.config.configSource + "/symbolList.json"); if (res.status !== 200) { throw new Error(`failed to fetch symbolList status code: ${res.status}`); } if (!res.ok) { throw new Error(`failed to fetch config (${res.status}): ${res.statusText} ${this.config.configSource}`); } let symlist = await res.json(); this.symbolList = new Map(Object.entries(symlist)); } /** * Returns the order-book contract for the symbol if found or fails * @param symbol symbol of the form ETH-USD-MATIC * @returns order book contract for the perpetual */ getOrderBookContract(symbol, signerOrProvider) { let orderBookAddr = this.symbolToPerpStaticInfo.get(symbol)?.limitOrderBookAddr; if (orderBookAddr == "" || orderBookAddr == undefined) { throw Error(`no limit order book found for ${symbol} or no signer`); } let lobContract = LimitOrderBook__factory.connect(orderBookAddr, signerOrProvider ?? this.signerOrProvider); return lobContract; } /** * Returns the order-book contract for the symbol if found or fails * @param symbol symbol of the form ETH-USD-MATIC * @returns order book contract for the perpetual */ getOrderBookAddress(symbol) { return this.symbolToPerpStaticInfo.get(symbol)?.limitOrderBookAddr; } /** * Get perpetuals for the given ids from onchain * @param ids perpetual ids * @param overrides optional * @returns array of PerpetualData converted into decimals */ async getPerpetuals(ids, overrides) { if (this.proxyContract == null) { throw new Error("proxy not defined"); } return await PerpetualDataHandler._getPerpetuals(ids, this.proxyContract, this.symbolList, overrides); } /** * Get liquidity pools data * @param fromIdx starting index (>=1) * @param toIdx to index (inclusive) * @param overrides optional * @returns array of LiquidityPoolData converted into decimals */ async getLiquidityPools(fromIdx, toIdx, overrides) { if (this.proxyContract == null) { throw new Error("proxy not defined"); } return await PerpetualDataHandler._getLiquidityPools(fromIdx, toIdx, this.proxyContract, this.symbolList, overrides); } /** * Called when initializing. This function fills this.symbolToTokenAddrMap, * and this.nestedPerpetualIDs and this.symbolToPerpStaticInfo * */ async _fillSymbolMaps(overrides) { if (this.proxyContract == null || this.multicall == null || this.signerOrProvider == null) { throw new Error("proxy or multicall not defined"); } const tokenOverrides = require("./config/tokenOverrides.json"); let poolInfo = await PerpetualDataHandler.getPoolStaticInfo(this.proxyContract, overrides); this.nestedPerpetualIDs = poolInfo.nestedPerpetualIDs; const IERC20 = ERC20__factory.createInterface(); const proxyCalls = poolInfo.poolMarginTokenAddr.map((tokenAddr) => ({ target: tokenAddr, allowFailure: false, callData: IERC20.encodeFunctionData("decimals"), })); proxyCalls.push({ target: this.proxyAddr, allowFailure: false, callData: this.proxyContract.interface.encodeFunctionData("getOrderBookFactoryAddress"), }); proxyCalls.push({ target: this.proxyAddr, allowFailure: false, callData: this.proxyContract.interface.encodeFunctionData("getOracleFactory"), }); // multicall const encodedResults = await this.multicall.aggregate3.staticCall(proxyCalls, overrides || {}); // decimals for (let j = 0; j < poolInfo.nestedPerpetualIDs.length; j++) { const decimals = IERC20.decodeFunctionResult("decimals", encodedResults[j].returnData)[0]; let info = { poolId: j + 1, poolMarginSymbol: "", poolMarginTokenAddr: poolInfo.poolMarginTokenAddr[j], poolMarginTokenDecimals: Number(decimals), poolSettleSymbol: "", poolSettleTokenAddr: poolInfo.poolMarginTokenAddr[j], poolSettleTokenDecimals: Number(decimals), shareTokenAddr: poolInfo.poolShareTokenAddr[j], oracleFactoryAddr: poolInfo.oracleFactory, isRunning: poolInfo.poolShareTokenAddr[j] != ZeroAddress, MgnToSettleTriangulation: ["*", "1"], // correct later }; this.poolStaticInfos.push(info); } //pyth const oracle = OracleFactory__factory.connect(poolInfo.oracleFactory, this.signerOrProvider); this.pythAddr = await oracle.pyth(); if (this.pythAddr == ZERO_ADDRESS) { this.pythAddr = await oracle.onDemandFeed(); } // order book factory this.lobFactoryAddr = this.proxyContract.interface.decodeFunctionResult("getOrderBookFactoryAddress", encodedResults[encodedResults.length - 2].returnData)[0]; this.lobFactoryContract = LimitOrderBookFactory__factory.connect(this.lobFactoryAddr, this.signerOrProvider); // oracle factory this.oraclefactoryAddr = this.proxyContract.interface.decodeFunctionResult("getOracleFactory", encodedResults[encodedResults.length - 1].returnData)[0]; let perpStaticInfos = await PerpetualDataHandler.getPerpetualStaticInfo(this.proxyContract, this.nestedPerpetualIDs, this.symbolList, overrides); let requiredPairs = new Set(); // 1) determine pool currency based on its perpetuals // 2) determine which triangulations we need // 3) fill mapping this.symbolToPerpStaticInf for (let j = 0; j < perpStaticInfos.length; j++) { const perp = perpStaticInfos[j]; if (perp.state != "INVALID" && perp.state != "INITIALIZING") { // we only require price feeds to be available if the perpetual // is in normal state requiredPairs.add(perp.S2Symbol); if (perp.S3Symbol != "") { requiredPairs.add(perp.S3Symbol); } } let poolCCY = this.poolStaticInfos[perp.poolId - 1].poolMarginSymbol; if (poolCCY == "") { // check if token address has an override for its symbol const tokenOverride = tokenOverrides.find(({ tokenAddress }) => tokenAddress === this.poolStaticInfos[perp.poolId - 1].poolMarginTokenAddr); if (tokenOverride) { poolCCY = tokenOverride.newSymbol; } else { // not overriden - infer from perp const [base, quote] = perp.S2Symbol.split("-"); const base3 = perp.S3Symbol.split("-")[0]; // we find out the pool currency by looking at all perpetuals // from the perpetual. if (perp.collateralCurrencyType == COLLATERAL_CURRENCY_BASE) { poolCCY = base; } else if (perp.collateralCurrencyType == COLLATERAL_CURRENCY_QUOTE) { poolCCY = quote; } else { poolCCY = base3; } } } let effectivePoolCCY = poolCCY; let currentSymbol3 = perp.S2Symbol + "-" + poolCCY; let perpInfo = this.symbolToPerpStaticInfo.get(currentSymbol3); let count = 0; while (perpInfo) { count++; // rename pool symbol effectivePoolCCY = `${poolCCY}${count}`; currentSymbol3 = perp.S2Symbol + "-" + effectivePoolCCY; perpInfo = this.symbolToPerpStaticInfo.get(currentSymbol3); } // set pool currency this.poolStaticInfos[perp.poolId - 1].poolMarginSymbol = effectivePoolCCY; // push pool margin token address into map this.symbolToTokenAddrMap.set(effectivePoolCCY, this.poolStaticInfos[perp.poolId - 1].poolMarginTokenAddr); this.symbolToPerpStaticInfo.set(currentSymbol3, perpStaticInfos[j]); } // handle settlement token. this.initSettlementToken(perpStaticInfos); // pre-calculate all triangulation paths so we can easily get from // the prices of price-feeds to the index price required, e.g. // BTC-USDC : BTC-USD / USDC-USD this.priceFeedGetter.initializeTriangulations(requiredPairs); // ensure all feed prices can be fetched this.setRequiredSymbols(); await this.priceFeedGetter.fetchFeedPrices(this.requiredSymbols); // fill this.perpetualIdToSymbol for (let [key, info] of this.symbolToPerpStaticInfo) { this.perpetualIdToSymbol.set(info.id, key); } } // setRequiredSymbols determines which symbols (of the form BTC-USD) // need to be available in the price sources setRequiredSymbols() { const triang = this.priceFeedGetter.getTriangulations(); const pairs = new Set(); for (const [key, [stringArray, _]] of triang) { for (const s of stringArray) { pairs.add(s); } } this.requiredSymbols = Array.from(pairs); } /** * Initializes settlement currency for all pools by * completing this.poolStaticInfos with settlement currency info * @param perpStaticInfos PerpetualStaticInfo array from contract call */ initSettlementToken(perpStaticInfos) { let currPoolId = -1; for (let j = 0; j < perpStaticInfos.length; j++) { const poolId = perpStaticInfos[j].poolId; if (poolId == currPoolId) { continue; } currPoolId = poolId; // We only assume the flag to be correct // in the first perpetual of the pool const flag = perpStaticInfos[j].perpFlags == undefined ? 0n : BigInt(perpStaticInfos[j].perpFlags.toString()); // find settlement setting for this flag let s = undefined; for (let j = 0; j < this.settlementConfig.length; j++) { const masked = flag & BigInt(this.settlementConfig[j].perpFlags.toString()); if (masked != 0n) { s = this.settlementConfig[j]; break; } } if (s == undefined) { // no setting for given flag, settlement token = margin token this.poolStaticInfos[poolId - 1].poolSettleSymbol = this.poolStaticInfos[poolId - 1].poolMarginSymbol; this.poolStaticInfos[poolId - 1].poolSettleTokenAddr = this.poolStaticInfos[poolId - 1].poolMarginTokenAddr; this.poolStaticInfos[poolId - 1].poolSettleTokenDecimals = this.poolStaticInfos[poolId - 1].poolMarginTokenDecimals; this.poolStaticInfos[poolId - 1].MgnToSettleTriangulation = ["*", "1"]; } else { this.poolStaticInfos[poolId - 1].poolSettleSymbol = s.settleCCY; this.poolStaticInfos[poolId - 1].poolSettleTokenAddr = s.settleCCYAddr; this.poolStaticInfos[poolId - 1].poolSettleTokenDecimals = s.settleTokenDecimals; this.poolStaticInfos[poolId - 1].MgnToSettleTriangulation = s.triangulation; } } } /** * Utility function to export mapping and re-use in other objects. * @ignore */ getAllMappings() { return { nestedPerpetualIDs: this.nestedPerpetualIDs, poolStaticInfos: this.poolStaticInfos, symbolToTokenAddrMap: this.symbolToTokenAddrMap, symbolToPerpStaticInfo: this.symbolToPerpStaticInfo, perpetualIdToSymbol: this.perpetualIdToSymbol, }; } /** * Get pool symbol given a pool Id. * @param {number} poolId Pool Id. * @returns {symbol} Pool symbol, e.g. "USDC". */ getSymbolFromPoolId(poolId) { return PerpetualDataHandler._getSymbolFromPoolId(poolId, this.poolStaticInfos); } /** * Get pool Id given a pool symbol. Pool IDs start at 1. * @param {string} symbol Pool symbol. * @returns {number} Pool Id. */ getPoolIdFromSymbol(symbol) { return PerpetualDataHandler._getPoolIdFromSymbol(symbol, this.poolStaticInfos); } /** * Get perpetual Id given a perpetual symbol. * @param {string} symbol Perpetual symbol, e.g. "BTC-USD-MATIC". * @returns {number} Perpetual Id. */ getPerpIdFromSymbol(symbol) { return PerpetualDataHandler.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); } /** * Get the symbol in long format of the perpetual id * @param {number} perpId perpetual id * @returns {string} Symbol */ getSymbolFromPerpId(perpId) { return this.perpetualIdToSymbol.get(perpId); } /** * * @param {string} sym Short symbol * @returns {string} Long symbol */ symbol4BToLongSymbol(sym) { return symbol4BToLongSymbol(sym, this.symbolList); } /** * Get PriceFeedSubmission data required for blockchain queries that involve price data, and the corresponding * triangulated prices for the indices S2 and S3 * @param symbol pool symbol of the form "ETH-USD-MATIC" * @returns PriceFeedSubmission and prices for S2 and S3. [S2price, 0] if S3 not defined. */ async fetchPriceSubmissionInfoForPerpetual(symbol) { // fetch prices from required price-feeds (REST) return await this.priceFeedGetter.fetchFeedPriceInfoAndIndicesForPerpetual(symbol); } /** * Get the symbols required as indices for the given perpetual * @param symbol of the form ETH-USD-MATIC, specifying the perpetual * @returns name of underlying index prices, e.g. ["MATIC-USD", ""] */ getIndexSymbols(symbol) { // get index let staticInfo = this.symbolToPerpStaticInfo.get(symbol); if (staticInfo == undefined) { throw new Error(`No static info for perpetual with symbol ${symbol}`); } return [staticInfo.S2Symbol, staticInfo.S3Symbol]; } /** * Get the latest prices for a given perpetual from the offchain oracle * networks * @param symbol perpetual symbol of the form BTC-USD-MATIC * @returns array of price feed updates that can be submitted to the smart contract * and corresponding price information */ async fetchLatestFeedPriceInfo(symbol) { return await this.priceFeedGetter.fetchLatestFeedPriceInfoForPerpetual(symbol); } /** * fetchCollateralToSettlementConversion returns the price which converts the collateral * currency into settlement currency. For example if BTC-USD-STUSD has settlement currency * USDC, we get * let px = fetchCollateralToSettlementConversion("BTC-USD-STUSD") * valueInUSDC = collateralInSTUSD * px * @param symbol either perpetual symbol of the form BTC-USD-MATIC or just collateral token */ async fetchCollateralToSettlementConversion(symbol) { let j = this.getPoolStaticInfoIndexFromSymbol(symbol); if (this.poolStaticInfos[j].poolMarginSymbol == this.poolStaticInfos[j].poolSettleSymbol) { // settlement currency = collateral currency return 1; } const triang = this.poolStaticInfos[j].MgnToSettleTriangulation; let v = 1; for (let k = 0; k < triang.length; k = k + 2) { const sym = triang[k + 1]; const pxMap = await this.priceFeedGetter.fetchFeedPrices([sym]); const vpxinfo = pxMap.get(sym); if (vpxinfo == undefined) { throw Error(`price ${sym} not available`); } v = triang[k] == "*" ? v * vpxinfo[0] : v / vpxinfo[0]; } return v; } /** * Get list of required pyth price source IDs for given perpetual * @param symbol perpetual symbol, e.g., BTC-USD-MATIC * @returns list of required pyth price sources for this perpetual */ getPriceIds(symbol) { let perpInfo = this.symbolToPerpStaticInfo.get(symbol); if (perpInfo == undefined) { throw Error(`Perpetual with symbol ${symbol} not found. Check symbol or use createProxyInstance().`); } return perpInfo.priceIds; } static _getSymbolFromPoolId(poolId, staticInfos) { let idx = poolId - 1; return staticInfos[idx].poolMarginSymbol; } static _getPoolIdFromSymbol(symbol, staticInfos) { let symbols = symbol.split("-"); //in case user provided ETH-USD-MATIC instead of MATIC; or similar if (symbols.length == 3) { symbol = symbols[2]; } let j = 0; while (j < staticInfos.length && staticInfos[j].poolMarginSymbol != symbol) { j++; } if (j == staticInfos.length) { throw new Error(`no pool found for symbol ${symbol}`); } return j + 1; } /** * Get ('normal'-state) perpetual symbols for a given pool * @param poolSymbol pool symbol such as "MATIC" * @returns array of perpetual symbols in this pool */ getPerpetualSymbolsInPool(poolSymbol) { const j = PerpetualDataHandler._getPoolIdFromSymbol(poolSymbol, this.poolStaticInfos); const perpIds = this.nestedPerpetualIDs[j - 1]; let perpSymbols = new Array(); for (let k = 0; k < perpIds.length; k++) { let s = this.getSymbolFromPerpId(perpIds[k]); if (s == undefined) { continue; } const staticInfo = this.symbolToPerpStaticInfo.get(s); if (staticInfo == undefined || staticInfo.state != "NORMAL") { continue; } perpSymbols.push(s); } return perpSymbols; } getNestedPerpetualIds() { return this.nestedPerpetualIDs; } /** * Collect all perpetuals static info * @param {ethers.Contract} _proxyContract perpetuals contract with getter * @param {Array<Array<number>>} nestedPerpetualIDs perpetual id-array for each pool * @param {Map<string, string>} symbolList mapping of symbols to convert long-format <-> blockchain-format * @returns array with PerpetualStaticInfo for each perpetual */ static async getPerpetualStaticInfo(_proxyContract, nestedPerpetualIDs, symbolList, overrides) { // flatten perpetual ids into chunks const chunkSize = 10; let ids = PerpetualDataHandler.nestedIDsToChunks(chunkSize, nestedPerpetualIDs); // query blockchain in chunks const infoArr = new Array(); for (let k = 0; k < ids.length; k++) { let perpInfos = (await _proxyContract.getPerpetualStaticInfo(ids[k], overrides || {})); for (let j = 0; j < perpInfos.length; j++) { let base = contractSymbolToSymbol(perpInfos[j].S2BaseCCY, symbolList); let quote = contractSymbolToSymbol(perpInfos[j].S2QuoteCCY, symbolList); let base3 = contractSymbolToSymbol(perpInfos[j].S3BaseCCY, symbolList); let quote3 = contractSymbolToSymbol(perpInfos[j].S3QuoteCCY, symbolList); let sym2 = base + "-" + quote; let sym3 = base3 == "" ? "" : base3 + "-" + quote3; let info = { id: Number(perpInfos[j].id), state: PERP_STATE_STR[Number(perpInfos[j].perpetualState.toString())], poolId: Math.floor(Number(perpInfos[j].id) / 100000), limitOrderBookAddr: perpInfos[j].limitOrderBookAddr, initialMarginRate: ABDK29ToFloat(perpInfos[j].fInitialMarginRate), maintenanceMarginRate: ABDK29ToFloat(perpInfos[j].fMaintenanceMarginRate), collateralCurrencyType: Number(perpInfos[j].collCurrencyType), S2Symbol: sym2, S3Symbol: sym3, lotSizeBC: ABK64x64ToFloat(perpInfos[j].fLotSizeBC), referralRebate: ABK64x64ToFloat(perpInfos[j].fReferralRebateCC), priceIds: perpInfos[j].priceIds, isPyth: perpInfos[j].isPyth, perpFlags: BigInt(perpInfos[j].perpFlags?.toString() ?? 0), fAMMTargetDD: perpInfos[j].fAMMTargetDD }; infoArr.push(info); } } return infoArr; } /** * Breaks up an array of nested arrays into chunks of a specified size. * @param {number} chunkSize The size of each chunk. * @param {number[][]} nestedIDs The array of nested arrays to chunk. * @returns {number[][]} An array of subarrays, each containing `chunkSize` or fewer elements from `nestedIDs`. */ static nestedIDsToChunks(chunkSize, nestedIDs) { const chunkIDs = []; let currentChunk = []; for (let k = 0; k < nestedIDs.length; k++) { const currentPoolIds = nestedIDs[k]; for (let j = 0; j < currentPoolIds.length; j++) { currentChunk.push(currentPoolIds[j]); if (currentChunk.length === chunkSize) { chunkIDs.push(currentChunk); currentChunk = []; } } } if (currentChunk.length > 0) { chunkIDs.push(currentChunk); } return chunkIDs; } /** * Query perpetuals * @param ids perpetual ids * @param _proxyContract proxy contract instance * @param _symbolList symbol mappings to convert the bytes encoded symbol name to string * @param overrides optional * @returns array of PerpetualData converted into decimals */ static async _getLiquidityPools(fromIdx, toIdx, _proxyContract, _symbolList, overrides) { if (fromIdx < 1) { throw Error("_getLiquidityPools: indices start at 1"); } const rawPools = await _proxyContract.getLiquidityPools(fromIdx, toIdx, overrides || {}); let p = new Array(); for (let k = 0; k < rawPools.length; k++) { let orig = rawPools[k]; let v = { isRunning: orig.isRunning, iPerpetualCount: Number(orig.iPerpetualCount), id: Number(orig.id), fCeilPnLShare: ABK64x64ToFloat(BigInt(orig.fCeilPnLShare)), marginTokenDecimals: Number(orig.marginTokenDecimals), iTargetPoolSizeUpdateTime: Number(orig.iTargetPoolSizeUpdateTime), marginTokenAddress: orig.marginTokenAddress, prevAnchor: Number(orig.prevAnchor), fRedemptionRate: ABK64x64ToFloat(BigInt(orig.fRedemptionRate)), shareTokenAddress: orig.shareTokenAddress, fPnLparticipantsCashCC: ABK64x64ToFloat(BigInt(orig.fPnLparticipantsCashCC)), fTargetAMMFundSize: ABK64x64ToFloat(BigInt(orig.fTargetAMMFundSize)), fDefaultFundCashCC: ABK64x64ToFloat(BigInt(orig.fDefaultFundCashCC)), fTargetDFSize: ABK64x64ToFloat(BigInt(orig.fTargetDFSize)), fBrokerCollateralLotSize: ABK64x64ToFloat(BigInt(orig.fBrokerCollateralLotSize)), prevTokenAmount: dec18ToFloat(BigInt(orig.prevTokenAmount)), nextTokenAmount: dec18ToFloat(BigInt(orig.nextTokenAmount)), totalSupplyShareToken: dec18ToFloat(BigInt(orig.totalSupplyShareToken)), fBrokerFundCashCC: ABK64x64ToFloat(BigInt(orig.fBrokerFundCashCC)), // state: amount of cash in broker fund }; p.push(v); } return p; } /** * Query perpetuals * @param ids perpetual ids * @param _proxyContract proxy contract instance * @param _symbolList symbol mappings to convert the bytes encoded symbol name to string * @param overrides optional * @returns array of PerpetualData converted into decimals */ static async _getPerpetuals(ids, _proxyContract, _symbolList, overrides) { // TODO: can't be type safe here because proxyContract's abi is not static across chains (zkevm is the exception) const rawPerps = await _proxyContract.getPerpetuals(ids, overrides || {}); let p = new Array(); for (let k = 0; k < rawPerps.length; k++) { let orig = rawPerps[k]; let v = { poolId: Number(orig.poolId), id: Number(orig.id), fInitialMarginRate: ABDK29ToFloat(Number(orig.fInitialMarginRate)), fSigma2: ABDK29ToFloat(Number(orig.fSigma2)), iLastFundingTime: Number(orig.iLastFundingTime), fDFCoverNRate: ABDK29ToFloat(Number(orig.fDFCoverNRate)), fMaintenanceMarginRate: ABDK29ToFloat(Number(orig.fMaintenanceMarginRate)), perpetualState: PERP_STATE_STR[Number(orig.state)], eCollateralCurrency: Number(orig.eCollateralCurrency), S2BaseCCY: contractSymbolToSymbol(fromBytes4(Buffer.from(orig.S2BaseCCY.toString())), _symbolList), S2QuoteCCY: contractSymbolToSymbol(fromBytes4(Buffer.from(orig.S2QuoteCCY.toString())), _symbolList), incentiveSpreadBps: Number(orig.incentiveSpreadTbps) / 10, minimalSpreadBps: Number(orig.minimalSpreadBps), S3BaseCCY: contractSymbolToSymbol(fromBytes4(Buffer.from(orig.S3BaseCCY.toString())), _symbolList), S3QuoteCCY: contractSymbolToSymbol(fromBytes4(Buffer.from(orig.S3QuoteCCY.toString())), _symbolList), fSigma3: ABDK29ToFloat(Number(orig.fSigma3)), fRho23: ABDK29ToFloat(Number(orig.fRho23)), liquidationPenaltyRateBps: Number(orig.liquidationPenaltyRateTbps) / 10, currentMarkPremiumRatePrice: ABK64x64ToFloat(BigInt(orig.currentMarkPremiumRate.fPrice)), currentMarkPremiumRateTime: Number(orig.currentMarkPremiumRate.time), premiumRatesEMA: ABK64x64ToFloat(BigInt(orig.premiumRatesEMA)), fUnitAccumulatedFunding: ABK64x64ToFloat(BigInt(orig.fUnitAccumulatedFunding)), fOpenInterest: ABK64x64ToFloat(BigInt(orig.fOpenInterest)), fTargetAMMFundSize: ABK64x64ToFloat(BigInt(orig.fTargetAMMFundSize)), fCurrentTraderExposureEMA: ABK64x64ToFloat(BigInt(orig.fCurrentTraderExposureEMA)), fCurrentFundingRate: ABK64x64ToFloat(BigInt(orig.fCurrentFundingRate)), fLotSizeBC: ABK64x64ToFloat(BigInt(orig.fLotSizeBC)), fReferralRebateCC: ABK64x64ToFloat(BigInt(orig.fReferralRebateCC)), fTargetDFSize: ABK64x64ToFloat(BigInt(orig.fTargetDFSize)), fkStar: ABK64x64ToFloat(BigInt(orig.fkStar)), fAMMTargetDD: ABK64x64ToFloat(BigInt(orig.fAMMTargetDD)), perpFlags: BigInt(orig.perpFlags?.toString() ?? 0), fMinimalTraderExposureEMA: ABK64x64ToFloat(BigInt(orig.fMinimalTraderExposureEMA)), fMinimalAMMExposureEMA: ABK64x64ToFloat(BigInt(orig.fMinimalAMMExposureEMA)), fSettlementS3PriceData: ABK64x64ToFloat(BigInt(orig.fSettlementS3PriceData)), fSettlementS2PriceData: ABK64x64ToFloat(BigInt(orig.fSettlementS2PriceData)), fTotalMarginBalance: ABK64x64ToFloat(BigInt(orig.fParams)), fMarkPriceEMALambda: ABK64x64ToFloat(Number(orig.fMarkPriceEMALambda)), fFundingRateClamp: ABK64x64ToFloat(Number(orig.fFundingRateClamp)), fMaximalTradeSizeBumpUp: ABK64x64ToFloat(Number(orig.fMaximalTradeSizeBumpUp)), iLastTargetPoolSizeTime: Number(orig.iLastTargetPoolSizeTime), fStressReturnS3: [ ABK64x64ToFloat(BigInt(orig.fStressReturnS3[0])), ABK64x64ToFloat(BigInt(orig.fStressReturnS3[1])), ], fDFLambda: [ABK64x64ToFloat(BigInt(orig.fDFLambda[0])), ABK64x64ToFloat(BigInt(orig.fDFLambda[1]))], fCurrentAMMExposureEMA: [ ABK64x64ToFloat(BigInt(orig.fCurrentAMMExposureEMA[0])), ABK64x64ToFloat(BigInt(orig.fCurrentAMMExposureEMA[1])), ], fStressReturnS2: [ ABK64x64ToFloat(BigInt(orig.fStressReturnS2[0])), ABK64x64ToFloat(BigInt(orig.fStressReturnS2[1])), ], // parameter: negative and positive stress returns for base-quote asset }; if (isNaN(v.minimalSpreadBps)) { // proxies have a different name for the variable v.minimalSpreadBps = Number(orig.minimalSpreadTbps); } p.push(v); } return p; } static async getPoolStaticInfo(_proxyContract, overrides) { let idxFrom = 1; const len = 10; let lenReceived = 10; let nestedPerpetualIDs = []; let poolShareTokenAddr = []; let poolMarginTokenAddr = []; let oracleFactory = ""; while (lenReceived == len) { const res = (await _proxyContract.getPoolStaticInfo(idxFrom, idxFrom + len - 1, overrides || {})); lenReceived = res.length; const nestedIds = res[0].map((ids) => ids.map((id) => Number(id))); nestedPerpetualIDs = nestedPerpetualIDs.concat(nestedIds); // TODO: this looks like a bug if num pools > 10 --- concat? poolShareTokenAddr = res[1]; poolMarginTokenAddr = res[2]; oracleFactory = res[3]; idxFrom = idxFrom + len; } return { nestedPerpetualIDs: nestedPerpetualIDs, poolShareTokenAddr: poolShareTokenAddr, poolMarginTokenAddr: poolMarginTokenAddr, oracleFactory: oracleFactory, }; } static buildMarginAccountFromState(symbol, traderState, symbolToPerpStaticInfo, pxInfo, isPredMkt) { const idx_cash = 3; const idx_notional = 4; const idx_locked_in = 5; const idx_mark_price = 8; const idx_lvg = 7; const idx_s3 = 9; let isEmpty = traderState[idx_notional] == 0n; let cash = ABK64x64ToFloat(traderState[idx_cash]); let S2Liq = 0, S3Liq = 0, tau = Infinity, pnl = 0, unpaidFundingCC = 0, fLockedIn = BigInt(0), side = CLOSED_SIDE, entryPrice = 0; if (!isEmpty) { [S2Liq, S3Liq, tau, pnl, unpaidFundingCC] = PerpetualDataHandler._calculateLiquidationPrice(symbol, traderState, pxInfo.s2, symbolToPerpStaticInfo, isPredMkt); fLockedIn = traderState[idx_locked_in]; side = traderState[idx_notional] > 0n ? BUY_SIDE : SELL_SIDE; entryPrice = Math.abs(ABK64x64ToFloat(div64x64(fLockedIn, traderState[idx_notional]))); } let mgn = { symbol: symbol, positionNotionalBaseCCY: isEmpty ? 0 : Math.abs(ABK64x64ToFloat(traderState[idx_notional])), side: isEmpty ? CLOSED_SIDE : side, entryPrice: isEmpty ? 0 : entryPrice, leverage: isEmpty ? 0 : ABK64x64ToFloat(traderState[idx_lvg]), markPrice: Math.abs(ABK64x64ToFloat(traderState[idx_mark_price])), unrealizedPnlQuoteCCY: isEmpty ? 0 : pnl, unrealizedFundingCollateralCCY: isEmpty ? 0 : unpaidFundingCC, collateralCC: cash, liquidationLvg: isEmpty ? 0 : 1 / tau, liquidationPrice: isEmpty ? [0, 0] : [S2Liq, S3Liq], collToQuoteConversion: ABK64x64ToFloat(traderState[idx_s3]), }; return mgn; } async getMarginAccount(traderAddr, symbol, idxPriceInfo, overrides) { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } const isPred = this.isPredictionMarket(symbol); return PerpetualDataHandler.getMarginAccount(traderAddr, symbol, this.symbolToPerpStaticInfo, new Contract(this.proxyAddr, this.config.proxyABI, this.provider), idxPriceInfo, isPred, overrides); } /** * Get trader state from the blockchain and parse into a human-readable margin account * @param traderAddr Trader address * @param symbol Perpetual symbol * @param symbolToPerpStaticInfo Symbol to perp static info mapping * @param _proxyContract Proxy contract instance * @param _pxInfo index price info * @param overrides Optional overrides for eth_call * @returns A Margin account */ static async getMarginAccount(traderAddr, symbol, symbolToPerpStaticInfo, _proxyContract, _pxInfo, isPredMkt, overrides) { let perpId = Number(symbol); if (isNaN(perpId)) { perpId = PerpetualDataHandler.symbolToPerpetualId(symbol, symbolToPerpStaticInfo); } let traderState = await _proxyContract.getTraderState(perpId, traderAddr, [_pxInfo.ema, _pxInfo.s3 ?? 0].map((x) => floatToABK64x64(x)), overrides || {}); return PerpetualDataHandler.buildMarginAccountFromState(symbol, traderState, symbolToPerpStaticInfo, _pxInfo, isPredMkt); } /** * All the orders in the order book for a given symbol that are currently open. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @example * import { OrderExecutorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(OrderExecutorTool); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const pk: string = <string>process.env.PK; * let orderTool = new OrderExecutorTool(config, pk); * await orderTool.createProxyInstance(); * // get all open orders * let openOrders = await orderTool.getAllOpenOrders("ETH-USD-MATIC"); * console.log(openOrders); * } * main(); * * @returns Array with all open orders and their IDs. */ async getAllOpenOrders(symbol, overrides) { const MAX_ORDERS_POLLED = 500; let totalOrders = await this.numberOfOpenOrders(symbol, overrides); let orderBundles = [[], [], []]; let moreOrders = orderBundles[1].length < totalOrders; let startAfter = 0; while (orderBundles[1].length < totalOrders && moreOrders) { let res = await this.pollLimitOrders(symbol, MAX_ORDERS_POLLED, startAfter, overrides); if (res[1].length < 1) { break; } const curIds = new Set(orderBundles[1]); for (let k = 0; k < res[0].length && res[2][k] !== ZERO_ADDRESS; k++) { if (!curIds.has(res[1][k])) { orderBundles[0].push(res[0][k]); orderBundles[1].push(res[1][k]); orderBundles[2].push(res[2][k]); } } startAfter = orderBundles[0].length; moreOrders = orderBundles[1].length < totalOrders; } return orderBundles; } /** * Total number of limit orders for this symbol, excluding those that have been cancelled/removed. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @example * import { OrderExecutorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(OrderExecutorTool); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const pk: string = <string>process.env.PK; * let orderTool = new OrderExecutorTool(config, pk); * await orderTool.createProxyInstance(); * // get all open orders * let numberOfOrders = await orderTool.numberOfOpenOrders("ETH-USD-MATIC"); * console.log(numberOfOrders); * } * main(); * * @returns {number} Number of open orders. */ async numberOfOpenOrders(symbol, overrides) { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, {}); const orderBookSC = this.getOrderBookContract(symbol).connect(provider); let numOrders = await orderBookSC.orderCount(overrides || {}); return Number(numOrders); } /** * Get a list of active conditional orders in the order book. * This a read-only action and does not incur in gas costs. * @param {string} symbol Symbol of the form ETH-USD-MATIC. * @param {number} numElements Maximum number of orders to poll. * @param {string=} startAfter Optional order ID from where to start polling. Defaults to the first order. * @example * import { OrderExecutorTool, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(OrderExecutorTool); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const pk: string = <string>process.env.PK; * let orderTool = new OrderExecutorTool(config, pk); * await orderTool.createProxyInstance(); * // get all open orders * let activeOrders = await orderTool.pollLimitOrders("ETH-USD-MATIC", 2); * console.log(activeOrders); * } * main(); * * @returns Array of orders and corresponding order IDs */ async pollLimitOrders(symbol, numElements, startAfter, overrides) { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); const orderBookSC = this.getOrderBookContract(symbol).connect(provider); const multicall = Multicall3__factory.connect(this.config.multicall ?? MULTICALL_ADDRESS, provider); if (startAfter == undefined) { startAfter = ZERO_ORDER_ID; } else if (typeof startAfter === "string") { startAfter = 0; // TODO: fix } // first get client orders (incl. dependency info) let [orders, orderIds] = await orderBookSC.pollRange(startAfter, numElements, overrides || {}); let userFriendlyOrders = new Array(); let traderAddr = []; let orderIdsOut = []; let k = 0; while (k < numElements && k < orders.length && orders[k].traderAddr !== ZERO_ADDRESS) { userFriendlyOrders.push(PerpetualDataHandler.fromClientOrder(orders[k], this.symbolToPerpStaticInfo)); orderIdsOut.push(orderIds[k]); traderAddr.push(orders[k].traderAddr); k++; } // then get perp orders (incl. submitted ts info) const multicalls = orderIdsOut.map((id) => ({ target: orderBookSC.target, allowFailure: true, callData: orderBookSC.interface.encodeFunctionData("orderOfDigest", [id]), })); const encodedResults = await multicall.aggregate3.staticCall(multicalls, overrides || {}); // order status encodedResults.map((res, k) => { if (res.success) { const order = orderBookSC.interface.decodeFunctionResult("orderOfDigest", res.returnData); userFriendlyOrders[k].submittedTimestamp = Number(order.submittedTimestamp); } }); return [userFriendlyOrders, orderIdsOut, traderAddr]; } /** * Get trader states from the blockchain and parse into a list of human-readable margin accounts * @param traderAddrs List of trader addresses * @param symbols List of symbols * @param symbolToPerpStaticInfo Symbol to perp static info mapping * @param _multicall Multicall3 contract instance * @param _proxyContract Proxy contract instance * @param _pxInfo List of price info * @param overrides Optional eth_call overrides * @returns List of margin accounts */ static async getMarginAccounts(traderAddrs, symbols, symbolToPerpStaticInfo, _multicall, _proxyContract, _pxInfo, isPredMkt, overrides) { if (traderAddrs.length != symbols.length || traderAddrs.length != _pxInfo.length || symbols.length != _pxInfo.length) { throw new Error("traderAddr, symbol and _pxInfo should all have the same length"); } const proxyCalls = traderAddrs.map((_addr, i) => ({ target: _proxyContract.target, allowFailure: true, callData: _proxyContract.interface.encodeFunctionData("getTraderState", [ PerpetualDataHandler.symbolToPerpetualId(symbols[i], symbolToPerpStaticInfo), _addr, [_pxInfo[i].ema, _pxInfo[i].s3 ?? 0].map((x) => floatToABK64x64(x)), ]), })); const encodedResults = await _multicall.aggregate3.staticCall(proxyCalls, overrides || {}); const traderStates = encodedResults.map(({ success, returnData }, i) => { if (!success) throw new Error(`Failed to get perp info for ${symbols[i]}`); return _proxyContract.interface.decodeFunctionResult("getTraderState", returnData)[0]; }); return traderStates.map((traderState, i) => PerpetualDataHandler.buildMarginAccountFromState(symbols[i], traderState, symbolToPerpStaticInfo, _pxInfo[i], isPredMkt[i])); } static async _queryPerpetualPrice(symbol, tradeAmount, symbolToPerpStaticInfo, _proxyContract, indexPrices, conf, params, overrides) { let perpId = PerpetualDataHandler.symbolToPerpetualId(symbol, symbolToPerpStaticInfo); let fIndexPrices = indexPrices.map((x) => floatToABK64x64(x == undefined || Number.isNaN(x) ? 0 : x)); let fPrice = await _proxyContract.queryPerpetualPrice(perpId, floatToABK64x64(tradeAmount), fIndexPrices, conf * 10n, params, overrides || {}); return ABK64x64ToFloat(fPrice); } /** * * @param symbol perpetual symbol of the form BTC-USDC-USDC * @param symbolToPerpStaticInfo mapping * @param _proxyContract contract instance * @param indexPrices IdxPriceInfo * @param isPredMkt true if prediction market perpetual * @param overrides * @returns mark price */ static async _queryPerpetualMarkPrice(symbol, symbolToPerpStaticInfo, _proxyContract, indexPrices, isPredMkt, overrides) { let perpId = PerpetualDataHandler.symbolToPerpetualId(symbol, symbolToPerpStaticInfo); let [S2, S3] = [indexPrices.s2, indexPrices.s3].map((x) => floatToABK64x64(x == undefined || Number.isNaN(x) ? 0 : x)); let ammState = await _proxyContract.getAMMState(perpId, [S2, S3], overrides || {}); // ammState[6] == S2 == indexPrices[0] up to rounding errors (indexPrices is most accurate) if (isPredMkt) { return indexPrices.ema + ABK64x64ToFloat(ammState[8]); } return indexPrices.s2 * (1 + ABK64x64ToFloat(ammState[8])); } static async _queryPerpetualState(symbol, symbolToPerpStaticInfo, _proxyContract, _multicall, indexPrices, overrides) { let perpId = PerpetualDataHandler.symbolToPerpetualId(symbol, symbolToPerpStaticInfo); let staticInfo = symbolToPerpStaticInfo.get(symbol); if (staticInfo.collateralCurrencyType == CollaterlCCY.BASE) { indexPrices.s3 = indexPrices.s2; } else if (staticInfo.collateralCurrencyType == CollaterlCCY.QUOTE) { indexPrices.s3 = 1; } // multicall const proxyCalls = [ { target: _proxyContract.target, allowFailure: false, callData: _proxyContract.interface.encodeFunctionData("getAMMState", [ perpId, [indexPrices.s2, indexPrices.s3 ?? 0].map(floatToABK64x64), ]), }, { target: _proxyContract.target, allowFailure: false, callData: _proxyContract.interface.encodeFunctionData("getMarginAccounts", [[perpId], ZERO_ADDRESS]), }, ]; // multicall const encodedResults = await _multicall.aggregate3.staticCall(proxyCalls, overrides || {}); let ammState = _proxyContract.interface.decodeFunctionResult("getAMMState", encodedResults[0].returnData)[0]; const margin = _proxyContract.interface.decodeFunctionResult("getMarginAccounts", encodedResults[1].returnData)[0]; let longShort = PerpetualDataHandler._oiAndAmmPosToLongShort(ammState[11], margin[0].fPositionBC); return PerpetualDataHandler._parseAMMState(symbol, ammState, longShort, indexPrices, symbolToPerpStaticInfo); } /** * Calculate long and short exposures from open interest and long/short