@d8x/perpetuals-sdk
Version:
Node TypeScript SDK for D8X Perpetual Futures
992 lines (991 loc) • 86.3 kB
JavaScript
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