@d8x/perpetuals-sdk
Version:
Node TypeScript SDK for D8X Perpetual Futures
1,187 lines (1,135 loc) • 90.7 kB
text/typescript
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,
div64x64,
floatToABK64x64,
dec18ToFloat,
priceToProb,
probToPrice,
pmFindLiquidationPrice,
pmMaintenanceMarginRate,
} from "./d8XMath";
import {
TokenOverride,
TypeSafeOrder,
type ClientOrder,
type MarginAccount,
type NodeSDKConfig,
type Order,
type PerpetualState,
type PerpetualStaticInfo,
type PoolStaticInfo,
type PriceFeedSubmission,
type SmartContractOrder,
type PerpetualData,
LiquidityPoolData,
SettlementConfig,
SettlementCcyItem,
IdxPriceInfo,
} from "./nodeSDKTypes";
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 {
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 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 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[][];
/**
* 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) {
await this.fetchSymbolList();
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._fillSymbolMaps(overrides);
}
/**
* 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();
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");
}
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");
}
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
*
*/
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, overrides);
this.nestedPerpetualIDs = poolInfo.nestedPerpetualIDs;
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 || {});
// decimals
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
};
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] 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;
let perpStaticInfos = await PerpetualDataHandler.getPerpetualStaticInfo(
this.proxyContract,
this.nestedPerpetualIDs,
this.symbolList,
overrides
);
let requiredPairs = new Set<string>();
// 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
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)
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 getIndexSymbols(symbol: string): [string, string] {
// 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
*/
public async fetchLatestFeedPriceInfo(symbol: string): Promise<PriceFeedSubmission> {
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> {
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;
}
/**
* 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: Array<Array<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().");
}
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;
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);
}
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
*/
public async pollLimitOrders(
symbol: string,
numElements: number,
startAfter?: string | number,
overrides?: Overrides & { rpcURL?: string }
): Promise<[Order[], string[], string[]]> {
if (this.proxyContract == null) {
throw Error("no proxy contract initialized. Use createProxyInstance().");
}
let rpcURL: string | undefined;
if (overrides) {
({ rpcURL, ...overrides } = overrides);
}
const provider = new JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true });
const orderBookSC = this.getOrderBookContract(symbol).connect(provider) as LimitOrderBook;
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: Order[] = new Array<Order>();
let traderAddr: string[] = [];
let orderIdsOut: string[] = [];
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: Multicall3.Call3Struct[] = 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
*/
public static async getMarginAccounts(
traderAddrs: string[],
symbols: str