UNPKG

@d8x/perpetuals-sdk

Version:

Node TypeScript SDK for D8X Perpetual Futures

1,208 lines (1,147 loc) 98.3 kB
import { BaseContract, Contract, Interface, JsonRpcProvider, Network, Overrides, Provider, Signer, 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, LimitOrderBook, LimitOrderBookFactory, LimitOrderBookFactory__factory, LimitOrderBook__factory, Multicall3, Multicall3__factory, OracleFactory__factory, } from "./contracts"; import { IPerpetualInfo, IPerpetualManager } from "./contracts/IPerpetualManager"; import { IClientOrder, IPerpetualOrder } from "./contracts/LimitOrderBook"; import { ABDK29ToFloat, ABK64x64ToFloat, calculateLiquidationPriceCollateralBase, calculateLiquidationPriceCollateralQuanto, calculateLiquidationPriceCollateralQuote, dec18ToFloat, div64x64, floatToABK64x64, pmFindLiquidationPrice, pmMaintenanceMarginRate, priceToProb, probToPrice, } from "./d8XMath"; import { IdxPriceInfo, LiquidityPoolData, SettlementCcyItem, SettlementConfig, SlotInfo, TokenOverride, TypeSafeOrder, type ClientOrder, type MarginAccount, type NodeSDKConfig, type Order, type PerpetualData, type PerpetualState, type PerpetualStaticInfo, type PoolStaticInfo, type PriceFeedSubmission, type SmartContractOrder, } from "./nodeSDKTypes"; import PriceFeeds from "./priceFeeds"; import { combineFlags, containsFlag, contractSymbolToSymbol, fromBytes4, loadConfigAbis, sleepForSec, symbol4BToLongSymbol, to4Chars, } from "./utils"; /** * Parent class for MarketData and WriteAccessHandler that handles * common data and chain operations. */ export default class PerpetualDataHandler { PRICE_UPDATE_FEE_GWEI = 1; //map symbol of the form ETH-USD-MATIC into perpetual ID and other static info //this is initialized in the createProxyInstance function protected symbolToPerpStaticInfo: Map<string, PerpetualStaticInfo>; // maps symbol of the form BTC-USD-MATIC to static info protected perpetualIdToSymbol: Map<number, string>; // maps unique perpetual id to symbol of the form BTC-USD-MATIC protected poolStaticInfos: Array<PoolStaticInfo>; protected symbolList: Map<string, string>; //mapping 4-digit symbol <-> long format protected indexSymbol: Map<string, { indexSymbol: string; expiry: number }> = new Map(); //mapping perpetual symbol to index symbol (NFL_TOR_...-USD-PUSD => NFL0-USD:84532) protected settlementConfig: SettlementConfig; public requiredSymbols: string[] = []; // array of symbols in the current perpetual deployment // config public config: NodeSDKConfig; //map margin token of the form MATIC or ETH or USDC into //the address of the margin token protected symbolToTokenAddrMap: Map<string, string>; public chainId: bigint; public network: Network; protected proxyContract: IPerpetualManager | null = null; protected proxyABI: Interface; protected proxyAddr: string; // oracle protected oraclefactoryAddr: string | undefined; // limit order book protected lobFactoryContract: LimitOrderBookFactory | null = null; protected lobFactoryABI: Interface; protected lobFactoryAddr: string | undefined; protected lobABI: Interface; // share token protected shareTokenABI: Interface; // multicall protected multicall: Multicall3 | null = null; // provider protected nodeURL: string; protected provider: Provider | null = null; // pyth protected pythAddr: string | undefined; protected ondemandAddr: string | undefined; protected signerOrProvider: Signer | Provider | null = null; protected priceFeedGetter: PriceFeeds; // pools are numbered consecutively starting at 1 // nestedPerpetualIDs contains an array for each pool // each pool-array contains perpetual ids protected nestedPerpetualIDs: number[][]; private refreshLocked: boolean = false; private refreshTs: number = 0; /** * Constructor * @param {NodeSDKConfig} config Configuration object, see * PerpetualDataHandler.readSDKConfig. */ public constructor(config: NodeSDKConfig) { this.settlementConfig = require("./config/settlement.json") as SettlementConfig; this.config = config; this.symbolToPerpStaticInfo = new Map<string, PerpetualStaticInfo>(); this.poolStaticInfos = new Array<PoolStaticInfo>(); this.symbolToTokenAddrMap = new Map<string, string>(); this.perpetualIdToSymbol = new Map<number, string>(); this.nestedPerpetualIDs = new Array<Array<number>>(); 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); } protected async initContractsAndData(signerOrProvider: Signer | Provider, overrides?: Overrides) { let count = 0; while (this.refreshLocked) { await sleepForSec(1); if (count == 25) { throw new Error("could not obtain lock for init"); } count += 1; } this.refreshLocked = true; try { this.signerOrProvider = signerOrProvider; // check network let network: Network; try { if (signerOrProvider.provider) { network = await signerOrProvider.provider.getNetwork(); } else { throw new Error("Signer has no provider"); // TODO: check } } catch (error: any) { 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.fetchSymbolList(); // static from sync-hub await this._fillSymbolMaps(); } finally { this.refreshLocked = false; } } /** * sets the symbollist if a remote config url is specified */ private 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(); // console.log("fetched symbol list from source", this.config.configSource + "/symbolList.json"); this.symbolList = new Map<string, string>(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 */ public getOrderBookContract(symbol: string, signerOrProvider?: Signer | Provider): LimitOrderBook { 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 */ public getOrderBookAddress(symbol: string): string | undefined { 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 */ public async getPerpetuals(ids: number[], overrides?: Overrides): Promise<PerpetualData[]> { if (this.proxyContract == null) { throw new Error("proxy not defined"); } await this.refreshSymbols(); 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 */ public async getLiquidityPools(fromIdx: number, toIdx: number, overrides?: Overrides): Promise<LiquidityPoolData[]> { if (this.proxyContract == null) { throw new Error("proxy not defined"); } await this.refreshSymbols(); return await PerpetualDataHandler._getLiquidityPools( fromIdx, toIdx, this.proxyContract, this.symbolList, overrides ); } public oracleProviders() { return { pyth: this.pythAddr, ondemand: this.ondemandAddr }; } /** * Called when initializing. This function fills this.symbolToTokenAddrMap, * and this.nestedPerpetualIDs and this.symbolToPerpStaticInfo * */ protected async _fillSymbolMaps(overrides?: 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") as TokenOverride[]; let poolInfo = await PerpetualDataHandler.getPoolStaticInfo(this.proxyContract); const IERC20 = ERC20__factory.createInterface(); const proxyCalls: Multicall3.Call3Struct[] = 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 || {}); // pre-allocate all pool static infos with id and margin token info const poolStaticInfos: PoolStaticInfo[] = []; for (let j = 0; j < poolInfo.nestedPerpetualIDs.length; j++) { const decimals = IERC20.decodeFunctionResult("decimals", encodedResults[j].returnData)[0] as bigint; let info: PoolStaticInfo = { poolId: j + 1, poolMarginSymbol: "", //fill later poolMarginTokenAddr: poolInfo.poolMarginTokenAddr[j], poolMarginTokenDecimals: Number(decimals), poolSettleSymbol: "", //fill later poolSettleTokenAddr: poolInfo.poolMarginTokenAddr[j], //correct later poolSettleTokenDecimals: Number(decimals), //correct later shareTokenAddr: poolInfo.poolShareTokenAddr[j], oracleFactoryAddr: poolInfo.oracleFactory, isRunning: poolInfo.poolShareTokenAddr[j] != ZeroAddress, MgnToSettleTriangulation: ["*", "1"], // correct later }; poolStaticInfos.push(info); } //pyth const oracle = OracleFactory__factory.connect(poolInfo.oracleFactory, this.signerOrProvider); this.pythAddr = await oracle.pyth(); this.ondemandAddr = await oracle.onDemandFeed(); // order book factory this.lobFactoryAddr = this.proxyContract.interface.decodeFunctionResult( "getOrderBookFactoryAddress", encodedResults[encodedResults.length - 2].returnData )[0] as string; this.lobFactoryContract = LimitOrderBookFactory__factory.connect(this.lobFactoryAddr, this.signerOrProvider); // oracle factory this.oraclefactoryAddr = this.proxyContract.interface.decodeFunctionResult( "getOracleFactory", encodedResults[encodedResults.length - 1].returnData )[0] as string; // perp static infos, symbols let perpStaticInfos = await PerpetualDataHandler.getPerpetualStaticInfo( this.proxyContract, poolInfo.nestedPerpetualIDs, this.symbolList, overrides ); const slotsInfo = await PerpetualDataHandler.fetchSlotsInfo(this.chainId); // replace symbols, if any for (const slot of slotsInfo) { const slotSymbol = slot.slot + "-USD"; const perpIdx = perpStaticInfos.findIndex( (p) => p.S2Symbol == slotSymbol && Number(this.chainId) == slot.chain_id ); if (perpIdx < 0) { // not a known perp - ignore continue; } // set symbol for this slot this.symbolList.set(slot.slot, slot.contract_id); perpStaticInfos[perpIdx].S2Symbol = slot.contract_id + "-USD"; // set expiry for this symbol this.indexSymbol.set(slot.slot, { indexSymbol: slot.slot + "-USD:" + slot.chain_id, expiry: Number(slot.expiry), }); } let requiredPairs = new Set<string>(); this.symbolToPerpStaticInfo = new Map(); this.nestedPerpetualIDs = poolInfo.nestedPerpetualIDs; this.poolStaticInfos = poolStaticInfos; // 1) determine pool currency based on its perpetuals // 2) determine which triangulations we need // 3) fill mapping this.symbolToPerpStaticInf for (const perp of perpStaticInfos) { const poolIdx = this.poolStaticInfos.findIndex((p) => p.poolId == perp.poolId); // need the index for later if (poolIdx < 0) { // no pool for this perp, skip continue; } const pool = this.poolStaticInfos[poolIdx]; if (perp.state != "INVALID" && perp.state != "INITIALIZING") { // we only require price feeds to be available if the perpetual // is in normal state requiredPairs.add(PerpetualDataHandler.getIndexSymbol(this.symbolList, perp)); if (perp.S3Symbol != "") { requiredPairs.add(perp.S3Symbol); } } let poolCCY = pool.poolMarginSymbol; if (poolCCY == "") { // check if token address has an override for its symbol const tokenOverride = tokenOverrides.find( ({ tokenAddress }) => tokenAddress.toLowerCase() === pool.poolMarginTokenAddr.toLowerCase() ); 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[poolIdx].poolMarginSymbol = effectivePoolCCY; // push pool margin token address into map this.symbolToTokenAddrMap.set(effectivePoolCCY, pool.poolMarginTokenAddr); this.symbolToPerpStaticInfo.set(currentSymbol3, perp); } // handle settlement token. this.initSettlementToken(perpStaticInfos); // fill this.perpetualIdToSymbol for (let [key, info] of this.symbolToPerpStaticInfo) { this.perpetualIdToSymbol.set(info.id, key); } // 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); this.setRequiredSymbols(); } // setRequiredSymbols determines which symbols (of the form BTC-USD) // need to be available in the price sources private setRequiredSymbols() { const triang = this.priceFeedGetter.getTriangulations(); const pairs = new Set<string>(); 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 */ private initSettlementToken(perpStaticInfos: PerpetualStaticInfo[]) { 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: SettlementCcyItem | undefined = 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 */ public 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". */ public getSymbolFromPoolId(poolId: number): string { 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. */ public getPoolIdFromSymbol(symbol: string): number { 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. */ public getPerpIdFromSymbol(symbol: string): number { return PerpetualDataHandler.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); } /** * Get the symbol in long format of the perpetual id * @param {number} perpId perpetual id * @returns {string} Symbol */ public getSymbolFromPerpId(perpId: number): string | undefined { return this.perpetualIdToSymbol.get(perpId); } /** * * @param {string} sym Short symbol * @returns {string} Long symbol */ public symbol4BToLongSymbol(sym: string): string { 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. */ public async fetchPriceSubmissionInfoForPerpetual( symbol: string ): Promise<{ submission: PriceFeedSubmission; pxS2S3: [number, number] }> { // fetch prices from required price-feeds (REST) await this.refreshSymbols(); 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", ""] */ public async getIndexSymbols(symbol: string): Promise<[string, string]> { await this.refreshSymbols(); // get index let staticInfo = this.symbolToPerpStaticInfo.get(symbol); if (staticInfo == undefined) { throw new Error(`No static info for perpetual with symbol ${symbol}`); } return this.isPredictionMarket(symbol) ? [PerpetualDataHandler.getIndexSymbol(this.symbolList, staticInfo), staticInfo.S3Symbol] : [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 */ public async fetchLatestFeedPriceInfo(symbol: string): Promise<PriceFeedSubmission> { await this.refreshSymbols(); 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 */ public async fetchCollateralToSettlementConversion(symbol: string): Promise<number> { await this.refreshSymbols(); 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 */ public getPriceIds(symbol: string): string[] { 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; } protected static _getSymbolFromPoolId(poolId: number, staticInfos: PoolStaticInfo[]): string { let idx = poolId - 1; return staticInfos[idx].poolMarginSymbol; } protected static _getPoolIdFromSymbol(symbol: string, staticInfos: PoolStaticInfo[]): number { 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 */ public getPerpetualSymbolsInPool(poolSymbol: string): string[] { const j = PerpetualDataHandler._getPoolIdFromSymbol(poolSymbol, this.poolStaticInfos); const perpIds = this.nestedPerpetualIDs[j - 1]; let perpSymbols = new Array<string>(); 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; } public getNestedPerpetualIds(): number[][] { return this.nestedPerpetualIDs; } /** * Fetch list of {short,long} symbols from API. * Symbol is left unchanged if unknown. * @param chainId Chain id * @param short List of short perp symbols (<=4 chars) * @returns List of long perp symbols */ protected static async fetchSlotsInfo(chainId: bigint | number | string) { let result: SlotInfo[] = []; const query = `https://sports-v2.quantena.org/slots-info/${Number(chainId)}`; const res = await fetch(query); if (res.status == 200 && res.ok) { result = await res.json(); } return result; } /** * 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 */ public static async getPerpetualStaticInfo( _proxyContract: IPerpetualManager, nestedPerpetualIDs: Array<Array<number>>, symbolList: Map<string, string>, overrides?: Overrides ): Promise<Array<PerpetualStaticInfo>> { // flatten perpetual ids into chunks const chunkSize = 10; let ids = PerpetualDataHandler.nestedIDsToChunks(chunkSize, nestedPerpetualIDs); // query blockchain in chunks const infoArr = new Array<PerpetualStaticInfo>(); for (let k = 0; k < ids.length; k++) { let perpInfos = (await _proxyContract.getPerpetualStaticInfo( ids[k], overrides || {} )) as IPerpetualInfo.PerpetualStaticInfoStructOutput[]; 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: PerpetualStaticInfo = { id: Number(perpInfos[j].id), state: PERP_STATE_STR[Number(perpInfos[j].perpetualState.toString())], poolId: Math.floor(Number(perpInfos[j].id) / 100_000), //uint24(_iPoolId) * 100_000 + iPerpetualIndex; 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`. */ public static nestedIDsToChunks( chunkSize: number, nestedIDs: ReadonlyArray<ReadonlyArray<number>> ): Array<Array<number>> { const chunkIDs: number[][] = []; let currentChunk: number[] = []; 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 */ public static async _getLiquidityPools( fromIdx: number, toIdx: number, _proxyContract: IPerpetualManager, _symbolList: Map<string, string>, overrides?: Overrides ): Promise<LiquidityPoolData[]> { if (fromIdx < 1) { throw Error("_getLiquidityPools: indices start at 1"); } const rawPools = await _proxyContract.getLiquidityPools(fromIdx, toIdx, overrides || {}); let p = new Array<LiquidityPoolData>(); for (let k = 0; k < rawPools.length; k++) { let orig = rawPools[k]; let v: LiquidityPoolData = { isRunning: orig.isRunning, // state iPerpetualCount: Number(orig.iPerpetualCount), // state id: Number(orig.id), // parameter: index, starts from 1 fCeilPnLShare: ABK64x64ToFloat(BigInt(orig.fCeilPnLShare)), // parameter: cap on the share of PnL allocated to liquidity providers marginTokenDecimals: Number(orig.marginTokenDecimals), // parameter: decimals of margin token, inferred from token contract iTargetPoolSizeUpdateTime: Number(orig.iTargetPoolSizeUpdateTime), //parameter: timestamp in seconds. How often we update the pool's target size marginTokenAddress: orig.marginTokenAddress, //parameter: address of the margin token prevAnchor: Number(orig.prevAnchor), // state: keep track of timestamp since last withdrawal was initiated fRedemptionRate: ABK64x64ToFloat(BigInt(orig.fRedemptionRate)), // state: used for settlement in case of AMM default shareTokenAddress: orig.shareTokenAddress, // parameter fPnLparticipantsCashCC: ABK64x64ToFloat(BigInt(orig.fPnLparticipantsCashCC)), // state: addLiquidity/withdrawLiquidity + profit/loss - rebalance fTargetAMMFundSize: ABK64x64ToFloat(BigInt(orig.fTargetAMMFundSize)), // state: target liquidity for all perpetuals in pool (sum) fDefaultFundCashCC: ABK64x64ToFloat(BigInt(orig.fDefaultFundCashCC)), // state: profit/loss fTargetDFSize: ABK64x64ToFloat(BigInt(orig.fTargetDFSize)), // state: target default fund size for all perpetuals in pool fBrokerCollateralLotSize: ABK64x64ToFloat(BigInt(orig.fBrokerCollateralLotSize)), // param:how much collateral do brokers deposit when providing "1 lot" (not trading lot) prevTokenAmount: dec18ToFloat(BigInt(orig.prevTokenAmount)), // state nextTokenAmount: dec18ToFloat(BigInt(orig.nextTokenAmount)), // state totalSupplyShareToken: dec18ToFloat(BigInt(orig.totalSupplyShareToken)), // state 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 */ public static async _getPerpetuals( ids: number[], _proxyContract: IPerpetualManager, _symbolList: Map<string, string>, overrides?: Overrides ): Promise<PerpetualData[]> { // 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<PerpetualData>(); for (let k = 0; k < rawPerps.length; k++) { let orig = rawPerps[k]; let v: PerpetualData = { poolId: Number(orig.poolId), id: Number(orig.id), fInitialMarginRate: ABDK29ToFloat(Number(orig.fInitialMarginRate)), //parameter: initial margin fSigma2: ABDK29ToFloat(Number(orig.fSigma2)), // parameter: volatility of base-quote pair iLastFundingTime: Number(orig.iLastFundingTime), //timestamp since last funding rate payment fDFCoverNRate: ABDK29ToFloat(Number(orig.fDFCoverNRate)), // parameter: cover-n rule for default fund. E.g., fDFCoverNRate=0.05 -> we try to cover 5% of active accounts with default fund fMaintenanceMarginRate: ABDK29ToFloat(Number(orig.fMaintenanceMarginRate)), // parameter: maintenance margin perpetualState: PERP_STATE_STR[Number(orig.state)], // Perpetual AMM state eCollateralCurrency: Number(orig.eCollateralCurrency), //parameter: in what currency is the collateral held? S2BaseCCY: contractSymbolToSymbol(fromBytes4(Buffer.from(orig.S2BaseCCY.toString())), _symbolList), //base currency of S2 S2QuoteCCY: contractSymbolToSymbol(fromBytes4(Buffer.from(orig.S2QuoteCCY.toString())), _symbolList), //quote currency of S2 incentiveSpreadBps: Number(orig.incentiveSpreadTbps) / 10, //parameter: maximum spread added to the PD minimalSpreadBps: Number(orig.minimalSpreadBps), //parameter: minimal half-spread between long and short perpetual price S3BaseCCY: contractSymbolToSymbol(fromBytes4(Buffer.from(orig.S3BaseCCY.toString())), _symbolList), //base currency of S3 S3QuoteCCY: contractSymbolToSymbol(fromBytes4(Buffer.from(orig.S3QuoteCCY.toString())), _symbolList), //quote currency of S3 fSigma3: ABDK29ToFloat(Number(orig.fSigma3)), // parameter: volatility of quanto-quote pair fRho23: ABDK29ToFloat(Number(orig.fRho23)), // parameter: correlation of quanto/base returns liquidationPenaltyRateBps: Number(orig.liquidationPenaltyRateTbps) / 10, //parameter: penalty if AMM closes the position and not the trader currentMarkPremiumRatePrice: ABK64x64ToFloat(BigInt(orig.currentMarkPremiumRate.fPrice)), //relative diff to index price EMA, used for markprice. currentMarkPremiumRateTime: Number(orig.currentMarkPremiumRate.time), //relative diff to index price EMA, used for markprice. premiumRatesEMA: ABK64x64ToFloat(BigInt(orig.premiumRatesEMA)), // EMA of premium rate fUnitAccumulatedFunding: ABK64x64ToFloat(BigInt(orig.fUnitAccumulatedFunding)), //accumulated funding in collateral currency fOpenInterest: ABK64x64ToFloat(BigInt(orig.fOpenInterest)), //open interest is the larger of the amount of long and short positions in base currency fTargetAMMFundSize: ABK64x64ToFloat(BigInt(orig.fTargetAMMFundSize)), //target liquidity pool funds to allocate to the AMM fCurrentTraderExposureEMA: ABK64x64ToFloat(BigInt(orig.fCurrentTraderExposureEMA)), // trade amounts (storing absolute value) fCurrentFundingRate: ABK64x64ToFloat(BigInt(orig.fCurrentFundingRate)), // current instantaneous funding rate fLotSizeBC: ABK64x64ToFloat(BigInt(orig.fLotSizeBC)), //parameter: minimal trade unit (in base currency) to avoid dust positions fReferralRebateCC: ABK64x64ToFloat(BigInt(orig.fReferralRebateCC)), //parameter: referall rebate in collateral currency fTargetDFSize: ABK64x64ToFloat(BigInt(orig.fTargetDFSize)), // target default fund size fkStar: ABK64x64ToFloat(BigInt(orig.fkStar)), // signed trade size that minimizes the AMM risk fAMMTargetDD: ABK64x64ToFloat(BigInt(orig.fAMMTargetDD)), // parameter: target distance to default (=inverse of default probability) perpFlags: BigInt(orig.perpFlags?.toString() ?? 0), // flags for perpetual fMinimalTraderExposureEMA: ABK64x64ToFloat(BigInt(orig.fMinimalTraderExposureEMA)), // parameter: minimal value for fCurrentTraderExposureEMA that we don't want to undershoot fMinimalAMMExposureEMA: ABK64x64ToFloat(BigInt(orig.fMinimalAMMExposureEMA)), // parameter: minimal abs value for fCurrentAMMExposureEMA that we don't want to undershoot fSettlementS3PriceData: ABK64x64ToFloat(BigInt(orig.fSettlementS3PriceData)), //quanto index fSettlementS2PriceData: ABK64x64ToFloat(BigInt(orig.fSettlementS2PriceData)), //base-quote pair. Used as last price in normal state. fTotalMarginBalance: ABK64x64ToFloat(BigInt(orig.fParams)), //calculated for settlement, in collateral currency fMarkPriceEMALambda: ABK64x64ToFloat(Number(orig.fMarkPriceEMALambda)), // parameter: Lambda parameter for EMA used in mark-price for funding rates fFundingRateClamp: ABK64x64ToFloat(Number(orig.fFundingRateClamp)), // parameter: funding rate clamp between which we charge 1bps fMaximalTradeSizeBumpUp: ABK64x64ToFloat(Number(orig.fMaximalTradeSizeBumpUp)), // parameter: >1, users can create a maximal position of size fMaximalTradeSizeBumpUp*fCurrentAMMExposureEMA iLastTargetPoolSizeTime: Number(orig.iLastTargetPoolSizeTime), //timestamp (seconds) since last update of fTargetDFSize and fTargetAMMFundSize fStressReturnS3: [ ABK64x64ToFloat(BigInt(orig.fStressReturnS3[0])), ABK64x64ToFloat(BigInt(orig.fStressReturnS3[1])), ], // parameter: negative and positive stress returns for quanto-quote asset fDFLambda: [ABK64x64ToFloat(BigInt(orig.fDFLambda[0])), ABK64x64ToFloat(BigInt(orig.fDFLambda[1]))], // parameter: EMA lambda for AMM and trader exposure K,k: EMA*lambda + (1-lambda)*K. 0 regular lambda, 1 if current value exceeds past fCurrentAMMExposureEMA: [ ABK64x64ToFloat(BigInt(orig.fCurrentAMMExposureEMA[0])), ABK64x64ToFloat(BigInt(orig.fCurrentAMMExposureEMA[1])), ], // 0: negative aggregated exposure (storing negative value), 1: positive 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 as any).minimalSpreadTbps); } p.push(v); } return p; } public static async getPoolStaticInfo( _proxyContract: IPerpetualManager, overrides?: Overrides ): Promise<{ nestedPerpetualIDs: Array<Array<number>>; poolShareTokenAddr: Array<string>; poolMarginTokenAddr: Array<string>; oracleFactory: string; }> { let idxFrom = 1; const len = 10; let lenReceived = 10; let nestedPerpetualIDs: Array<Array<number>> = []; let poolShareTokenAddr: Array<string> = []; let poolMarginTokenAddr: Array<string> = []; let oracleFactory: string = ""; while (lenReceived == len) { const res = (await _proxyContract.getPoolStaticInfo(idxFrom, idxFrom + len - 1, overrides || {})) as [ bigint[][], string[], string[], string ] & { _oracleFactoryAddress: string; }; 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, }; } public static buildMarginAccountFromState( symbol: string, traderState: bigint[], symbolToPerpStaticInfo: Map<string, PerpetualStaticInfo>, pxInfo: IdxPriceInfo, isPredMkt: boolean ): MarginAccount { 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: MarginAccount = { 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; } public async getMarginAccount( traderAddr: string, symbol: string, idxPriceInfo: IdxPriceInfo, overrides?: Overrides ): Promise<MarginAccount> { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } await this.refreshSymbols(); 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 */ public static async getMarginAccount( traderAddr: string, symbol: string, symbolToPerpStaticInfo: Map<string, PerpetualStaticInfo>, _proxyContract: Contract, _pxInfo: IdxPriceInfo, isPredMkt: boolean, overrides?: Overrides ): Promise<MarginAccount> { 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)) as [bigint, bigint], 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. */ public async getAllOpenOrders(symbol: string, overrides?: Overrides): Promise<[Order[], string[], string[]]> { const MAX_ORDERS_POLLED = 500; await this.refreshSymbols(); let totalOrders = await this.numberOfOpenOrders(symbol, overrides); let orderBundles: [Order[], string[], string[]] = [[], [], []]; 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. */ public async numberOfOpenOrders(symbol: string, overrides?: Overrides & { rpcURL?: string }): Promise<number> { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } let rpcURL: string | undefined; if (overrides) { ({ rpcURL, ...overrides } = overrides); } await this.refreshSymbols(); 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); * } * ma