@d8x/perpetuals-sdk
Version:
Node TypeScript SDK for D8X Perpetual Futures
1,007 lines • 96.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const ethers_1 = require("ethers");
const constants_1 = require("./constants");
const contracts_1 = require("./contracts");
const d8XMath_1 = require("./d8XMath");
const priceFeeds_1 = __importDefault(require("./priceFeeds"));
const utils_1 = require("./utils");
/**
* Parent class for MarketData and WriteAccessHandler that handles
* common data and chain operations.
*/
class PerpetualDataHandler {
/**
* Constructor
* @param {NodeSDKConfig} config Configuration object, see
* PerpetualDataHandler.readSDKConfig.
*/
constructor(config) {
this.PRICE_UPDATE_FEE_GWEI = 1;
this.indexSymbol = new Map(); //mapping perpetual symbol to index symbol (NFL_TOR_...-USD-PUSD => NFL0-USD:84532)
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.refreshLocked = false;
this.refreshTs = 0;
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 ethers_1.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 = constants_1.SYMBOL_LIST;
this.priceFeedGetter = new priceFeeds_1.default(this, config.priceFeedConfigNetwork);
}
async initContractsAndData(signerOrProvider, overrides) {
let count = 0;
while (this.refreshLocked) {
await (0, utils_1.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;
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 = contracts_1.IPerpetualManager__factory.connect(this.proxyAddr, signerOrProvider);
this.multicall = contracts_1.Multicall3__factory.connect(this.config.multicall ?? constants_1.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
*/
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(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 = contracts_1.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");
}
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
*/
async getLiquidityPools(fromIdx, toIdx, overrides) {
if (this.proxyContract == null) {
throw new Error("proxy not defined");
}
await this.refreshSymbols();
return await PerpetualDataHandler._getLiquidityPools(fromIdx, toIdx, this.proxyContract, this.symbolList, overrides);
}
oracleProviders() {
return { pyth: this.pythAddr, ondemand: this.ondemandAddr };
}
/**
* 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);
const IERC20 = contracts_1.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 || {});
// pre-allocate all pool static infos with id and margin token info
const poolStaticInfos = [];
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] != ethers_1.ZeroAddress,
MgnToSettleTriangulation: ["*", "1"], // correct later
};
poolStaticInfos.push(info);
}
//pyth
const oracle = contracts_1.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];
this.lobFactoryContract = contracts_1.LimitOrderBookFactory__factory.connect(this.lobFactoryAddr, this.signerOrProvider);
// oracle factory
this.oraclefactoryAddr = this.proxyContract.interface.decodeFunctionResult("getOracleFactory", encodedResults[encodedResults.length - 1].returnData)[0];
// 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();
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 == constants_1.COLLATERAL_CURRENCY_BASE) {
poolCCY = base;
}
else if (perp.collateralCurrencyType == constants_1.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
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 (0, utils_1.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)
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", ""]
*/
async getIndexSymbols(symbol) {
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
*/
async fetchLatestFeedPriceInfo(symbol) {
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
*/
async fetchCollateralToSettlementConversion(symbol) {
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
*/
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;
}
/**
* 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
*/
static async fetchSlotsInfo(chainId) {
let result = [];
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
*/
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 = (0, utils_1.contractSymbolToSymbol)(perpInfos[j].S2BaseCCY, symbolList);
let quote = (0, utils_1.contractSymbolToSymbol)(perpInfos[j].S2QuoteCCY, symbolList);
let base3 = (0, utils_1.contractSymbolToSymbol)(perpInfos[j].S3BaseCCY, symbolList);
let quote3 = (0, utils_1.contractSymbolToSymbol)(perpInfos[j].S3QuoteCCY, symbolList);
let sym2 = base + "-" + quote;
let sym3 = base3 == "" ? "" : base3 + "-" + quote3;
let info = {
id: Number(perpInfos[j].id),
state: constants_1.PERP_STATE_STR[Number(perpInfos[j].perpetualState.toString())],
poolId: Math.floor(Number(perpInfos[j].id) / 100000),
limitOrderBookAddr: perpInfos[j].limitOrderBookAddr,
initialMarginRate: (0, d8XMath_1.ABDK29ToFloat)(perpInfos[j].fInitialMarginRate),
maintenanceMarginRate: (0, d8XMath_1.ABDK29ToFloat)(perpInfos[j].fMaintenanceMarginRate),
collateralCurrencyType: Number(perpInfos[j].collCurrencyType),
S2Symbol: sym2,
S3Symbol: sym3,
lotSizeBC: (0, d8XMath_1.ABK64x64ToFloat)(perpInfos[j].fLotSizeBC),
referralRebate: (0, d8XMath_1.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: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fCeilPnLShare)),
marginTokenDecimals: Number(orig.marginTokenDecimals),
iTargetPoolSizeUpdateTime: Number(orig.iTargetPoolSizeUpdateTime),
marginTokenAddress: orig.marginTokenAddress,
prevAnchor: Number(orig.prevAnchor),
fRedemptionRate: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fRedemptionRate)),
shareTokenAddress: orig.shareTokenAddress,
fPnLparticipantsCashCC: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fPnLparticipantsCashCC)),
fTargetAMMFundSize: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fTargetAMMFundSize)),
fDefaultFundCashCC: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fDefaultFundCashCC)),
fTargetDFSize: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fTargetDFSize)),
fBrokerCollateralLotSize: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fBrokerCollateralLotSize)),
prevTokenAmount: (0, d8XMath_1.dec18ToFloat)(BigInt(orig.prevTokenAmount)),
nextTokenAmount: (0, d8XMath_1.dec18ToFloat)(BigInt(orig.nextTokenAmount)),
totalSupplyShareToken: (0, d8XMath_1.dec18ToFloat)(BigInt(orig.totalSupplyShareToken)),
fBrokerFundCashCC: (0, d8XMath_1.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: (0, d8XMath_1.ABDK29ToFloat)(Number(orig.fInitialMarginRate)),
fSigma2: (0, d8XMath_1.ABDK29ToFloat)(Number(orig.fSigma2)),
iLastFundingTime: Number(orig.iLastFundingTime),
fDFCoverNRate: (0, d8XMath_1.ABDK29ToFloat)(Number(orig.fDFCoverNRate)),
fMaintenanceMarginRate: (0, d8XMath_1.ABDK29ToFloat)(Number(orig.fMaintenanceMarginRate)),
perpetualState: constants_1.PERP_STATE_STR[Number(orig.state)],
eCollateralCurrency: Number(orig.eCollateralCurrency),
S2BaseCCY: (0, utils_1.contractSymbolToSymbol)((0, utils_1.fromBytes4)(Buffer.from(orig.S2BaseCCY.toString())), _symbolList),
S2QuoteCCY: (0, utils_1.contractSymbolToSymbol)((0, utils_1.fromBytes4)(Buffer.from(orig.S2QuoteCCY.toString())), _symbolList),
incentiveSpreadBps: Number(orig.incentiveSpreadTbps) / 10,
minimalSpreadBps: Number(orig.minimalSpreadBps),
S3BaseCCY: (0, utils_1.contractSymbolToSymbol)((0, utils_1.fromBytes4)(Buffer.from(orig.S3BaseCCY.toString())), _symbolList),
S3QuoteCCY: (0, utils_1.contractSymbolToSymbol)((0, utils_1.fromBytes4)(Buffer.from(orig.S3QuoteCCY.toString())), _symbolList),
fSigma3: (0, d8XMath_1.ABDK29ToFloat)(Number(orig.fSigma3)),
fRho23: (0, d8XMath_1.ABDK29ToFloat)(Number(orig.fRho23)),
liquidationPenaltyRateBps: Number(orig.liquidationPenaltyRateTbps) / 10,
currentMarkPremiumRatePrice: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.currentMarkPremiumRate.fPrice)),
currentMarkPremiumRateTime: Number(orig.currentMarkPremiumRate.time),
premiumRatesEMA: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.premiumRatesEMA)),
fUnitAccumulatedFunding: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fUnitAccumulatedFunding)),
fOpenInterest: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fOpenInterest)),
fTargetAMMFundSize: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fTargetAMMFundSize)),
fCurrentTraderExposureEMA: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fCurrentTraderExposureEMA)),
fCurrentFundingRate: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fCurrentFundingRate)),
fLotSizeBC: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fLotSizeBC)),
fReferralRebateCC: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fReferralRebateCC)),
fTargetDFSize: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fTargetDFSize)),
fkStar: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fkStar)),
fAMMTargetDD: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fAMMTargetDD)),
perpFlags: BigInt(orig.perpFlags?.toString() ?? 0),
fMinimalTraderExposureEMA: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fMinimalTraderExposureEMA)),
fMinimalAMMExposureEMA: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fMinimalAMMExposureEMA)),
fSettlementS3PriceData: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fSettlementS3PriceData)),
fSettlementS2PriceData: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fSettlementS2PriceData)),
fTotalMarginBalance: (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fParams)),
fMarkPriceEMALambda: (0, d8XMath_1.ABK64x64ToFloat)(Number(orig.fMarkPriceEMALambda)),
fFundingRateClamp: (0, d8XMath_1.ABK64x64ToFloat)(Number(orig.fFundingRateClamp)),
fMaximalTradeSizeBumpUp: (0, d8XMath_1.ABK64x64ToFloat)(Number(orig.fMaximalTradeSizeBumpUp)),
iLastTargetPoolSizeTime: Number(orig.iLastTargetPoolSizeTime),
fStressReturnS3: [
(0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fStressReturnS3[0])),
(0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fStressReturnS3[1])),
],
fDFLambda: [(0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fDFLambda[0])), (0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fDFLambda[1]))],
fCurrentAMMExposureEMA: [
(0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fCurrentAMMExposureEMA[0])),
(0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fCurrentAMMExposureEMA[1])),
],
fStressReturnS2: [
(0, d8XMath_1.ABK64x64ToFloat)(BigInt(orig.fStressReturnS2[0])),
(0, d8XMath_1.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 = (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_cash]);
let S2Liq = 0, S3Liq = 0, tau = Infinity, pnl = 0, unpaidFundingCC = 0, fLockedIn = BigInt(0), side = constants_1.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 ? constants_1.BUY_SIDE : constants_1.SELL_SIDE;
entryPrice = Math.abs((0, d8XMath_1.ABK64x64ToFloat)((0, d8XMath_1.div64x64)(fLockedIn, traderState[idx_notional])));
}
let mgn = {
symbol: symbol,
positionNotionalBaseCCY: isEmpty ? 0 : Math.abs((0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_notional])),
side: isEmpty ? constants_1.CLOSED_SIDE : side,
entryPrice: isEmpty ? 0 : entryPrice,
leverage: isEmpty ? 0 : (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_lvg]),
markPrice: Math.abs((0, d8XMath_1.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: (0, d8XMath_1.ABK64x64ToFloat)(traderState[idx_s3]),
};
return mgn;
}
async getMarginAccount(traderAddr, symbol, idxPriceInfo, overrides) {
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 ethers_1.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) => (0, d8XMath_1.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;
await this.refreshSymbols();
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] !== constants_1.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);
}
await this.refreshSymbols();
const provider = new ethers_1.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);
}
await this.refreshSymbols();
const provider = new ethers_1.JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true });
const orderBookSC = this.getOrderBookContract(symbol).connect(provider);
const multicall = contracts_1.Multicall3__factory.connect(this.config.multicall ?? constants_1.MULTICALL_ADDRESS, provider);
if (startAfter == undefined) {
startAfter = constants_1.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 !== constants_1.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) => (0, d8XMath_1.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((traderStat