@d8x/perpetuals-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/abi/ERC20.json +288 -0
- package/abi/IPerpetualManager.json +4674 -0
- package/abi/LimitOrderBook.json +865 -0
- package/abi/LimitOrderBookFactory.json +166 -0
- package/config/defaultConfig.json +9 -0
- package/config/oldConfig.json +9 -0
- package/dist/accountTrade.d.ts +54 -0
- package/dist/accountTrade.js +164 -0
- package/dist/brokerTool.d.ts +41 -0
- package/dist/brokerTool.js +129 -0
- package/dist/d8XMath.d.ts +71 -0
- package/dist/d8XMath.js +162 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +49 -0
- package/dist/liquiditatorTool.d.ts +14 -0
- package/dist/liquiditatorTool.js +21 -0
- package/dist/liquidityProviderTool.d.ts +39 -0
- package/dist/liquidityProviderTool.js +100 -0
- package/dist/marketData.d.ts +39 -0
- package/dist/marketData.js +160 -0
- package/dist/nodeSDKTypes.d.ts +130 -0
- package/dist/nodeSDKTypes.js +52 -0
- package/dist/orderReferrerTool.d.ts +14 -0
- package/dist/orderReferrerTool.js +21 -0
- package/dist/perpetualDataHandler.d.ts +85 -0
- package/dist/perpetualDataHandler.js +474 -0
- package/dist/utils.d.ts +37 -0
- package/dist/utils.js +84 -0
- package/dist/writeAccessHandler.d.ts +36 -0
- package/dist/writeAccessHandler.js +95 -0
- package/module.d.ts +1 -0
- package/package.json +63 -0
- package/src/accountTrade.ts +217 -0
- package/src/brokerTool.ts +155 -0
- package/src/d8XMath.ts +176 -0
- package/src/index.ts +32 -0
- package/src/liquiditatorTool.ts +21 -0
- package/src/liquidityProviderTool.ts +100 -0
- package/src/marketData.ts +149 -0
- package/src/nodeSDKTypes.ts +158 -0
- package/src/orderReferrerTool.ts +17 -0
- package/src/perpetualDataHandler.ts +549 -0
- package/src/utils.ts +83 -0
- package/src/writeAccessHandler.ts +83 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { ethers, BigNumber } from "ethers";
|
|
2
|
+
import {
|
|
3
|
+
NodeSDKConfig,
|
|
4
|
+
MAX_64x64,
|
|
5
|
+
Order,
|
|
6
|
+
SmartContractOrder,
|
|
7
|
+
CollaterlCCY,
|
|
8
|
+
PerpetualStaticInfo,
|
|
9
|
+
COLLATERAL_CURRENCY_BASE,
|
|
10
|
+
COLLATERAL_CURRENCY_QUOTE,
|
|
11
|
+
BUY_SIDE,
|
|
12
|
+
SELL_SIDE,
|
|
13
|
+
CLOSED_SIDE,
|
|
14
|
+
ORDER_MAX_DURATION_SEC,
|
|
15
|
+
ZERO_ADDRESS,
|
|
16
|
+
ORDER_TYPE_LIMIT,
|
|
17
|
+
ORDER_TYPE_MARKET,
|
|
18
|
+
ORDER_TYPE_STOP_MARKET,
|
|
19
|
+
ORDER_TYPE_STOP_LIMIT,
|
|
20
|
+
MASK_LIMIT_ORDER,
|
|
21
|
+
MASK_CLOSE_ONLY,
|
|
22
|
+
MASK_KEEP_POS_LEVERAGE,
|
|
23
|
+
MASK_MARKET_ORDER,
|
|
24
|
+
MASK_STOP_ORDER,
|
|
25
|
+
MarginAccount,
|
|
26
|
+
PoolStaticInfo,
|
|
27
|
+
DEFAULT_CONFIG_MAINNET_NAME,
|
|
28
|
+
DEFAULT_CONFIG_MAINNET,
|
|
29
|
+
DEFAULT_CONFIG_TESTNET_NAME,
|
|
30
|
+
DEFAULT_CONFIG_TESTNET,
|
|
31
|
+
} from "./nodeSDKTypes";
|
|
32
|
+
import { fromBytes4HexString, to4Chars, combineFlags, containsFlag } from "./utils";
|
|
33
|
+
import {
|
|
34
|
+
ABK64x64ToFloat,
|
|
35
|
+
floatToABK64x64,
|
|
36
|
+
div64x64,
|
|
37
|
+
calculateLiquidationPriceCollateralQuanto,
|
|
38
|
+
calculateLiquidationPriceCollateralBase,
|
|
39
|
+
calculateLiquidationPriceCollateralQuote,
|
|
40
|
+
} from "./d8XMath";
|
|
41
|
+
import { Config } from "jest";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parent class for AccountTrade and MarketData that handles
|
|
45
|
+
* common data and chain operations
|
|
46
|
+
*/
|
|
47
|
+
export default class PerpetualDataHandler {
|
|
48
|
+
//map symbol of the form ETH-USD-MATIC into perpetual ID and other static info
|
|
49
|
+
//this is initialized in the createProxyInstance function
|
|
50
|
+
protected symbolToPerpStaticInfo: Map<string, PerpetualStaticInfo>;
|
|
51
|
+
protected poolStaticInfos: Array<PoolStaticInfo>;
|
|
52
|
+
//map margin token of the form MATIC or ETH or USDC into
|
|
53
|
+
//the address of the margin token
|
|
54
|
+
protected symbolToTokenAddrMap: Map<string, string>;
|
|
55
|
+
|
|
56
|
+
protected proxyContract: ethers.Contract | null = null;
|
|
57
|
+
protected proxyABI: ethers.ContractInterface;
|
|
58
|
+
protected proxyAddr: string;
|
|
59
|
+
// limit order book
|
|
60
|
+
protected lobFactoryContract: ethers.Contract | null = null;
|
|
61
|
+
protected lobFactoryABI: ethers.ContractInterface;
|
|
62
|
+
protected lobFactoryAddr: string;
|
|
63
|
+
protected lobABI: ethers.ContractInterface;
|
|
64
|
+
protected nodeURL: string;
|
|
65
|
+
protected provider: ethers.providers.JsonRpcProvider | null = null;
|
|
66
|
+
|
|
67
|
+
private signerOrProvider: ethers.Signer | ethers.providers.Provider | null = null;
|
|
68
|
+
|
|
69
|
+
// pools are numbered consecutively starting at 1
|
|
70
|
+
// nestedPerpetualIDs contains an array for each pool
|
|
71
|
+
// each pool-array contains perpetual ids
|
|
72
|
+
protected nestedPerpetualIDs: number[][];
|
|
73
|
+
|
|
74
|
+
public constructor(config: NodeSDKConfig) {
|
|
75
|
+
this.symbolToPerpStaticInfo = new Map<string, PerpetualStaticInfo>();
|
|
76
|
+
this.poolStaticInfos = new Array<PoolStaticInfo>();
|
|
77
|
+
this.symbolToTokenAddrMap = new Map<string, string>();
|
|
78
|
+
this.nestedPerpetualIDs = new Array<Array<number>>();
|
|
79
|
+
this.proxyAddr = config.proxyAddr;
|
|
80
|
+
this.lobFactoryAddr = config.limitOrderBookFactoryAddr;
|
|
81
|
+
this.nodeURL = config.nodeURL;
|
|
82
|
+
this.proxyABI = require(config.proxyABILocation);
|
|
83
|
+
this.lobFactoryABI = require(config.limitOrderBookFactoryABILocation);
|
|
84
|
+
this.lobABI = require(config.limitOrderBookABILocation);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected async initContractsAndData(signerOrProvider: ethers.Signer | ethers.providers.Provider) {
|
|
88
|
+
this.signerOrProvider = signerOrProvider;
|
|
89
|
+
this.proxyContract = new ethers.Contract(this.proxyAddr, this.proxyABI, signerOrProvider);
|
|
90
|
+
this.lobFactoryContract = new ethers.Contract(this.lobFactoryAddr, this.lobFactoryABI, signerOrProvider);
|
|
91
|
+
await this._fillSymbolMaps(this.proxyContract);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns the order-book contract for the symbol if found or fails
|
|
96
|
+
* @param symbol symbol of the form ETH-USD-MATIC
|
|
97
|
+
* @returns order book contract for the perpetual
|
|
98
|
+
*/
|
|
99
|
+
public getOrderBookContract(symbol: string): ethers.Contract {
|
|
100
|
+
let cleanSymbol = PerpetualDataHandler.symbolToBytes4Symbol(symbol);
|
|
101
|
+
let orderBookAddr = this.symbolToPerpStaticInfo.get(cleanSymbol)?.limitOrderBookAddr;
|
|
102
|
+
if (orderBookAddr == "" || orderBookAddr == undefined || this.signerOrProvider == null) {
|
|
103
|
+
throw Error(`no limit order book found for ${symbol} or no signer`);
|
|
104
|
+
}
|
|
105
|
+
let lobContract = new ethers.Contract(orderBookAddr, this.lobABI, this.signerOrProvider);
|
|
106
|
+
return lobContract;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Called when initializing. This function fills this.symbolToTokenAddrMap,
|
|
111
|
+
* and this.nestedPerpetualIDs and this.symbolToPerpStaticInfo
|
|
112
|
+
*
|
|
113
|
+
*/
|
|
114
|
+
protected async _fillSymbolMaps(proxyContract: ethers.Contract) {
|
|
115
|
+
if (proxyContract == null || this.lobFactoryContract == null) {
|
|
116
|
+
throw Error("proxy or limit order book not defined");
|
|
117
|
+
}
|
|
118
|
+
this.nestedPerpetualIDs = await PerpetualDataHandler.getNestedPerpetualIds(proxyContract);
|
|
119
|
+
for (let j = 0; j < this.nestedPerpetualIDs.length; j++) {
|
|
120
|
+
let pool = await proxyContract.getLiquidityPool(j + 1);
|
|
121
|
+
let poolMarginTokenAddr = pool.marginTokenAddress;
|
|
122
|
+
let perpetualIDs = this.nestedPerpetualIDs[j];
|
|
123
|
+
let poolCCY: string | undefined = undefined;
|
|
124
|
+
let currentSymbols: string[] = [];
|
|
125
|
+
let currentSymbolsS3: string[] = [];
|
|
126
|
+
let currentLimitOrderBookAddr: string[] = [];
|
|
127
|
+
let ccy: CollaterlCCY[] = [];
|
|
128
|
+
let mgnRate: number[] = [];
|
|
129
|
+
|
|
130
|
+
for (let k = 0; k < perpetualIDs.length; k++) {
|
|
131
|
+
let perp = await proxyContract.getPerpetual(perpetualIDs[k]);
|
|
132
|
+
let base = fromBytes4HexString(perp.S2BaseCCY);
|
|
133
|
+
let quote = fromBytes4HexString(perp.S2QuoteCCY);
|
|
134
|
+
let base3 = fromBytes4HexString(perp.S3BaseCCY);
|
|
135
|
+
let quote3 = fromBytes4HexString(perp.S3QuoteCCY);
|
|
136
|
+
currentSymbols.push(base + "-" + quote);
|
|
137
|
+
currentSymbolsS3.push(base3 + "-" + quote3);
|
|
138
|
+
mgnRate.push(ABK64x64ToFloat(perp.fMaintenanceMarginRate));
|
|
139
|
+
// try to find a limit order book
|
|
140
|
+
let lobAddr = await this.lobFactoryContract.getOrderBookAddress(perpetualIDs[k]);
|
|
141
|
+
currentLimitOrderBookAddr.push(lobAddr);
|
|
142
|
+
// we find out the pool currency by looking at all perpetuals
|
|
143
|
+
// unless for quanto perpetuals, we know the pool currency
|
|
144
|
+
// from the perpetual. This fails if we have a pool with only
|
|
145
|
+
// quanto perpetuals
|
|
146
|
+
if (perp.eCollateralCurrency == COLLATERAL_CURRENCY_BASE) {
|
|
147
|
+
poolCCY = base;
|
|
148
|
+
ccy.push(CollaterlCCY.BASE);
|
|
149
|
+
} else if (perp.eCollateralCurrency == COLLATERAL_CURRENCY_QUOTE) {
|
|
150
|
+
poolCCY = quote;
|
|
151
|
+
ccy.push(CollaterlCCY.QUOTE);
|
|
152
|
+
} else {
|
|
153
|
+
ccy.push(CollaterlCCY.QUANTO);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (perpetualIDs.length == 0) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (poolCCY == undefined) {
|
|
160
|
+
throw Error("Pool only has quanto perps, unable to determine collateral currency");
|
|
161
|
+
}
|
|
162
|
+
let oracleFactoryAddr = await proxyContract.getOracleFactory();
|
|
163
|
+
let info: PoolStaticInfo = {
|
|
164
|
+
poolId: j + 1,
|
|
165
|
+
poolMarginSymbol: poolCCY,
|
|
166
|
+
poolMarginTokenAddr: poolMarginTokenAddr,
|
|
167
|
+
shareTokenAddr: pool.shareTokenAddress,
|
|
168
|
+
oracleFactoryAddr: oracleFactoryAddr,
|
|
169
|
+
};
|
|
170
|
+
this.poolStaticInfos.push(info);
|
|
171
|
+
let currentSymbols3 = currentSymbols.map((x) => x + "-" + poolCCY);
|
|
172
|
+
// push into map
|
|
173
|
+
for (let k = 0; k < perpetualIDs.length; k++) {
|
|
174
|
+
this.symbolToPerpStaticInfo.set(currentSymbols3[k], {
|
|
175
|
+
id: perpetualIDs[k],
|
|
176
|
+
limitOrderBookAddr: currentLimitOrderBookAddr[k],
|
|
177
|
+
maintenanceMarginRate: mgnRate[k],
|
|
178
|
+
collateralCurrencyType: ccy[k],
|
|
179
|
+
S2Symbol: currentSymbols[k],
|
|
180
|
+
S3Symbol: currentSymbolsS3[k],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// push margin token address into map
|
|
184
|
+
this.symbolToTokenAddrMap.set(poolCCY, poolMarginTokenAddr);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public getSymbolFromPoolId(poolId: number): string {
|
|
189
|
+
return PerpetualDataHandler._getSymbolFromPoolId(poolId, this.poolStaticInfos);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public getPoolIdFromSymbol(symbol: string): number {
|
|
193
|
+
return PerpetualDataHandler._getPoolIdFromSymbol(symbol, this.poolStaticInfos);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
protected static _getSymbolFromPoolId(poolId: number, staticInfos: PoolStaticInfo[]): string {
|
|
197
|
+
let idx = poolId - 1;
|
|
198
|
+
return staticInfos[idx].poolMarginSymbol;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
protected static _getPoolIdFromSymbol(symbol: string, staticInfos: PoolStaticInfo[]): number {
|
|
202
|
+
let symbols = symbol.split("-");
|
|
203
|
+
//in case user provided ETH-USD-MATIC instead of MATIC; or similar
|
|
204
|
+
if (symbols.length == 3) {
|
|
205
|
+
symbol = symbols[2];
|
|
206
|
+
}
|
|
207
|
+
let cleanSymbol = to4Chars(symbol);
|
|
208
|
+
cleanSymbol = cleanSymbol.replace(/\0/g, "");
|
|
209
|
+
let j = 0;
|
|
210
|
+
while (j < staticInfos.length && staticInfos[j].poolMarginSymbol != cleanSymbol) {
|
|
211
|
+
j++;
|
|
212
|
+
}
|
|
213
|
+
if (j == staticInfos.length) {
|
|
214
|
+
throw new Error(`no pool found for symbol ${symbol}`);
|
|
215
|
+
}
|
|
216
|
+
return j + 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public static async getNestedPerpetualIds(_proxyContract: ethers.Contract): Promise<number[][]> {
|
|
220
|
+
let poolCount = await _proxyContract.getPoolCount();
|
|
221
|
+
let poolIds: number[][] = new Array(poolCount);
|
|
222
|
+
for (let i = 1; i < poolCount + 1; i++) {
|
|
223
|
+
let perpetualCount = await _proxyContract.getPerpetualCountInPool(i);
|
|
224
|
+
poolIds[i - 1] = new Array(perpetualCount);
|
|
225
|
+
for (let j = 0; j < perpetualCount; j++) {
|
|
226
|
+
let id = await _proxyContract.getPerpetualId(i, j);
|
|
227
|
+
poolIds[i - 1][j] = id;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return poolIds;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public static async getMarginAccount(
|
|
234
|
+
traderAddr: string,
|
|
235
|
+
symbol: string,
|
|
236
|
+
symbolToPerpStaticInfo: Map<string, PerpetualStaticInfo>,
|
|
237
|
+
_proxyContract: ethers.Contract
|
|
238
|
+
): Promise<MarginAccount> {
|
|
239
|
+
let cleanSymbol = PerpetualDataHandler.symbolToBytes4Symbol(symbol);
|
|
240
|
+
let perpId = PerpetualDataHandler.symbolToPerpetualId(cleanSymbol, symbolToPerpStaticInfo);
|
|
241
|
+
const idx_cash = 3;
|
|
242
|
+
const idx_notional = 4;
|
|
243
|
+
const idx_locked_in = 5;
|
|
244
|
+
const idx_mark_price = 8;
|
|
245
|
+
const idx_lvg = 7;
|
|
246
|
+
const idx_s3 = 9;
|
|
247
|
+
let traderState = await _proxyContract.getTraderState(perpId, traderAddr);
|
|
248
|
+
let isEmpty = traderState[idx_notional] == 0;
|
|
249
|
+
let cash = ABK64x64ToFloat(traderState[idx_cash]);
|
|
250
|
+
let S2Liq = 0,
|
|
251
|
+
S3Liq = 0,
|
|
252
|
+
tau = Infinity,
|
|
253
|
+
pnl = 0,
|
|
254
|
+
unpaidFundingCC = 0,
|
|
255
|
+
fLockedIn = BigNumber.from(0),
|
|
256
|
+
side = CLOSED_SIDE,
|
|
257
|
+
entryPrice = 0;
|
|
258
|
+
if (!isEmpty) {
|
|
259
|
+
[S2Liq, S3Liq, tau, pnl, unpaidFundingCC] = PerpetualDataHandler._calculateLiquidationPrice(
|
|
260
|
+
cleanSymbol,
|
|
261
|
+
traderState,
|
|
262
|
+
symbolToPerpStaticInfo
|
|
263
|
+
);
|
|
264
|
+
fLockedIn = traderState[idx_locked_in];
|
|
265
|
+
side = traderState[idx_locked_in] > 0 ? BUY_SIDE : SELL_SIDE;
|
|
266
|
+
entryPrice = ABK64x64ToFloat(div64x64(fLockedIn, traderState[idx_notional]));
|
|
267
|
+
}
|
|
268
|
+
let mgn: MarginAccount = {
|
|
269
|
+
symbol: symbol,
|
|
270
|
+
positionNotionalBaseCCY: isEmpty ? 0 : ABK64x64ToFloat(traderState[idx_notional]),
|
|
271
|
+
side: isEmpty ? CLOSED_SIDE : side,
|
|
272
|
+
entryPrice: isEmpty ? 0 : entryPrice,
|
|
273
|
+
leverage: isEmpty ? 0 : ABK64x64ToFloat(traderState[idx_lvg]),
|
|
274
|
+
markPrice: ABK64x64ToFloat(traderState[idx_mark_price].abs()),
|
|
275
|
+
unrealizedPnlQuoteCCY: isEmpty ? 0 : pnl,
|
|
276
|
+
unrealizedFundingCollateralCCY: isEmpty ? 0 : unpaidFundingCC,
|
|
277
|
+
collateralCC: cash,
|
|
278
|
+
liquidationLvg: isEmpty ? 0 : 1 / tau,
|
|
279
|
+
liquidationPrice: isEmpty ? [0, 0] : [S2Liq, S3Liq],
|
|
280
|
+
collToQuoteConversion: ABK64x64ToFloat(traderState[idx_s3]),
|
|
281
|
+
};
|
|
282
|
+
return mgn;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Liquidation price
|
|
287
|
+
* @param cleanSymbol symbol after calling symbolToBytes4Symbol
|
|
288
|
+
* @param traderState BigInt array according to smart contract
|
|
289
|
+
* @param symbolToPerpStaticInfo mapping symbol->PerpStaticInfo
|
|
290
|
+
* @returns liquidation mark-price, corresponding collateral/quote conversion
|
|
291
|
+
*/
|
|
292
|
+
protected static _calculateLiquidationPrice(
|
|
293
|
+
cleanSymbol: string,
|
|
294
|
+
traderState: BigNumber[],
|
|
295
|
+
symbolToPerpStaticInfo: Map<string, PerpetualStaticInfo>
|
|
296
|
+
): [number, number, number, number, number] {
|
|
297
|
+
const idx_availableCashCC = 2;
|
|
298
|
+
const idx_cash = 3;
|
|
299
|
+
const idx_notional = 4;
|
|
300
|
+
const idx_locked_in = 5;
|
|
301
|
+
const idx_mark_price = 8;
|
|
302
|
+
const idx_s3 = 9;
|
|
303
|
+
const idx_s2 = 10;
|
|
304
|
+
let S2Liq: number;
|
|
305
|
+
let S3Liq: number = ABK64x64ToFloat(traderState[idx_s3]);
|
|
306
|
+
let perpInfo: PerpetualStaticInfo | undefined = symbolToPerpStaticInfo.get(cleanSymbol);
|
|
307
|
+
if (perpInfo == undefined) {
|
|
308
|
+
throw new Error(`no info for perpetual ${cleanSymbol}`);
|
|
309
|
+
}
|
|
310
|
+
let tau = perpInfo.maintenanceMarginRate;
|
|
311
|
+
let lockedInValueQC = ABK64x64ToFloat(traderState[idx_locked_in]);
|
|
312
|
+
let position = ABK64x64ToFloat(traderState[idx_notional]);
|
|
313
|
+
let cashCC = ABK64x64ToFloat(traderState[idx_availableCashCC]);
|
|
314
|
+
let Sm = ABK64x64ToFloat(traderState[idx_mark_price]);
|
|
315
|
+
let unpaidFundingCC = ABK64x64ToFloat(traderState[idx_availableCashCC].sub(traderState[idx_cash]));
|
|
316
|
+
let unpaidFunding = unpaidFundingCC;
|
|
317
|
+
|
|
318
|
+
if (perpInfo.collateralCurrencyType == CollaterlCCY.BASE) {
|
|
319
|
+
S2Liq = calculateLiquidationPriceCollateralBase(lockedInValueQC, position, cashCC, tau);
|
|
320
|
+
S3Liq = S2Liq;
|
|
321
|
+
unpaidFunding = unpaidFunding / ABK64x64ToFloat(traderState[idx_s2]);
|
|
322
|
+
} else if (perpInfo.collateralCurrencyType == CollaterlCCY.QUANTO) {
|
|
323
|
+
let S3 = S3Liq;
|
|
324
|
+
S3Liq = S3;
|
|
325
|
+
S2Liq = calculateLiquidationPriceCollateralQuanto(lockedInValueQC, position, cashCC, tau, S3, Sm);
|
|
326
|
+
unpaidFunding = unpaidFunding / S3;
|
|
327
|
+
} else {
|
|
328
|
+
S2Liq = calculateLiquidationPriceCollateralQuote(lockedInValueQC, position, cashCC, tau);
|
|
329
|
+
}
|
|
330
|
+
let pnl = position * Sm - lockedInValueQC - unpaidFunding;
|
|
331
|
+
return [S2Liq, S3Liq, tau, pnl, unpaidFundingCC];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Finds the perpetual id for a symbol of the form
|
|
336
|
+
* <base>-<quote>-<collateral>. The function first converts the
|
|
337
|
+
* token names into bytes4 representation
|
|
338
|
+
* @param symbol symbol (e.g., BTC-USD-MATIC)
|
|
339
|
+
* @param symbolToPerpStaticInfo map that contains the bytes4-symbol to PerpetualStaticInfo
|
|
340
|
+
* including id mapping
|
|
341
|
+
* @returns perpetual id or it fails
|
|
342
|
+
*/
|
|
343
|
+
protected static symbolToPerpetualId(
|
|
344
|
+
symbol: string,
|
|
345
|
+
symbolToPerpStaticInfo: Map<string, PerpetualStaticInfo>
|
|
346
|
+
): number {
|
|
347
|
+
let cleanSymbol = PerpetualDataHandler.symbolToBytes4Symbol(symbol);
|
|
348
|
+
let id = symbolToPerpStaticInfo.get(cleanSymbol)?.id;
|
|
349
|
+
if (id == undefined) {
|
|
350
|
+
throw Error(`No perpetual found for symbol ${symbol}`);
|
|
351
|
+
}
|
|
352
|
+
return id;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
protected static symbolToBytes4Symbol(symbol: string): string {
|
|
356
|
+
//split by dashes BTC-USD-MATIC
|
|
357
|
+
let symbols: string[] = symbol.split("-");
|
|
358
|
+
if (symbols.length != 3) {
|
|
359
|
+
throw Error(`Symbol ${symbol} not valid. Expecting CCY-CCY-CCY format`);
|
|
360
|
+
}
|
|
361
|
+
//transform into bytes4 currencies (without the space): "BTC", "USD", "MATC"
|
|
362
|
+
symbols = symbols.map((x) => {
|
|
363
|
+
let v = to4Chars(x);
|
|
364
|
+
v = v.replace(/\0/g, "");
|
|
365
|
+
return v;
|
|
366
|
+
});
|
|
367
|
+
// concatenate and find perpetual Id in map
|
|
368
|
+
return symbols[0] + "-" + symbols[1] + "-" + symbols[2];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private static _getByValue(map: any, searchValue: any) {
|
|
372
|
+
for (let [key, value] of map.entries()) {
|
|
373
|
+
if (value === searchValue) {
|
|
374
|
+
return key;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
protected static fromSmartContractOrder(
|
|
380
|
+
order: SmartContractOrder,
|
|
381
|
+
symbolToPerpInfoMap: Map<string, PerpetualStaticInfo>
|
|
382
|
+
): Order {
|
|
383
|
+
// find symbol of perpetual id
|
|
384
|
+
let symbol = PerpetualDataHandler._getByValue(symbolToPerpInfoMap, order.iPerpetualId);
|
|
385
|
+
let side = order.fAmount > 0 ? BUY_SIDE : SELL_SIDE;
|
|
386
|
+
let limitPrice, stopPrice;
|
|
387
|
+
let fLimitPrice: BigNumber | undefined = BigNumber.from(order.fLimitPrice);
|
|
388
|
+
if (fLimitPrice.eq(0) || fLimitPrice.eq(MAX_64x64)) {
|
|
389
|
+
limitPrice = undefined;
|
|
390
|
+
} else {
|
|
391
|
+
limitPrice = ABK64x64ToFloat(fLimitPrice);
|
|
392
|
+
}
|
|
393
|
+
let fStopPrice: BigNumber | undefined = BigNumber.from(order.fTriggerPrice);
|
|
394
|
+
if (fStopPrice.eq(0) || fStopPrice.eq(MAX_64x64)) {
|
|
395
|
+
stopPrice = undefined;
|
|
396
|
+
} else {
|
|
397
|
+
stopPrice = ABK64x64ToFloat(fStopPrice);
|
|
398
|
+
}
|
|
399
|
+
let userOrder: Order = {
|
|
400
|
+
symbol: symbol,
|
|
401
|
+
side: side,
|
|
402
|
+
type: PerpetualDataHandler._flagToOrderType(order),
|
|
403
|
+
quantity: Math.abs(ABK64x64ToFloat(BigNumber.from(order.fAmount))),
|
|
404
|
+
reduceOnly: containsFlag(BigNumber.from(order.flags), MASK_CLOSE_ONLY),
|
|
405
|
+
limitPrice: limitPrice,
|
|
406
|
+
keepPositionLvg: containsFlag(BigNumber.from(order.flags), MASK_KEEP_POS_LEVERAGE),
|
|
407
|
+
brokerFeeTbps: Number(order.brokerFeeTbps),
|
|
408
|
+
brokerAddr: order.brokerAddr,
|
|
409
|
+
brokerSignature: order.brokerSignature,
|
|
410
|
+
stopPrice: stopPrice,
|
|
411
|
+
leverage: ABK64x64ToFloat(BigNumber.from(order.fLeverage)),
|
|
412
|
+
deadline: Number(order.iDeadline),
|
|
413
|
+
timestamp: Number(order.createdTimestamp),
|
|
414
|
+
};
|
|
415
|
+
return userOrder;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Transform the convenient form of the order into a smart-contract accepted type of order
|
|
419
|
+
* @param order order type
|
|
420
|
+
* @param traderAddr address of the trader
|
|
421
|
+
* @param symbolToPerpetualMap mapping of symbol to perpetual Id
|
|
422
|
+
* @returns SmartContractOrder
|
|
423
|
+
*/
|
|
424
|
+
protected static toSmartContractOrder(
|
|
425
|
+
order: Order,
|
|
426
|
+
traderAddr: string,
|
|
427
|
+
perpStaticInfo: Map<string, PerpetualStaticInfo>
|
|
428
|
+
): SmartContractOrder {
|
|
429
|
+
let flags = PerpetualDataHandler._orderTypeToFlag(order);
|
|
430
|
+
|
|
431
|
+
let brokerSig = order.brokerSignature == undefined ? [] : order.brokerSignature;
|
|
432
|
+
let perpetualId = PerpetualDataHandler.symbolToPerpetualId(order.symbol, perpStaticInfo);
|
|
433
|
+
let fAmount: BigNumber;
|
|
434
|
+
if (order.side == BUY_SIDE) {
|
|
435
|
+
fAmount = floatToABK64x64(Math.abs(order.quantity));
|
|
436
|
+
} else if (order.side == SELL_SIDE) {
|
|
437
|
+
fAmount = floatToABK64x64(-Math.abs(order.quantity));
|
|
438
|
+
} else {
|
|
439
|
+
throw Error(`invalid side in order spec, use ${BUY_SIDE} or ${SELL_SIDE}`);
|
|
440
|
+
}
|
|
441
|
+
let fLimitPrice: BigNumber;
|
|
442
|
+
if (order.limitPrice == undefined) {
|
|
443
|
+
// we need to set the limit price to infinity or zero for
|
|
444
|
+
// the trade to go through
|
|
445
|
+
// Also: stop orders always have limits set, so even for this case
|
|
446
|
+
// we set the limit to 0 or infinity
|
|
447
|
+
fLimitPrice = order.side == BUY_SIDE ? MAX_64x64 : BigNumber.from(0);
|
|
448
|
+
} else {
|
|
449
|
+
fLimitPrice = floatToABK64x64(order.limitPrice);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let iDeadline = order.deadline == undefined ? Date.now() + ORDER_MAX_DURATION_SEC : order.deadline;
|
|
453
|
+
let fTriggerPrice = order.stopPrice == undefined ? BigNumber.from(0) : floatToABK64x64(order.stopPrice);
|
|
454
|
+
if (order.reduceOnly != undefined && order.reduceOnly == true) {
|
|
455
|
+
}
|
|
456
|
+
let smOrder: SmartContractOrder = {
|
|
457
|
+
flags: flags,
|
|
458
|
+
iPerpetualId: BigNumber.from(perpetualId),
|
|
459
|
+
brokerFeeTbps: order.brokerFeeTbps == undefined ? BigNumber.from(0) : BigNumber.from(order.brokerFeeTbps),
|
|
460
|
+
traderAddr: traderAddr,
|
|
461
|
+
brokerAddr: order.brokerAddr == undefined ? ZERO_ADDRESS : order.brokerAddr,
|
|
462
|
+
referrerAddr: ZERO_ADDRESS,
|
|
463
|
+
brokerSignature: brokerSig,
|
|
464
|
+
fAmount: fAmount,
|
|
465
|
+
fLimitPrice: fLimitPrice,
|
|
466
|
+
fTriggerPrice: fTriggerPrice,
|
|
467
|
+
fLeverage: order.leverage == undefined ? BigNumber.from(0) : floatToABK64x64(order.leverage),
|
|
468
|
+
iDeadline: BigNumber.from(iDeadline),
|
|
469
|
+
createdTimestamp: BigNumber.from(order.timestamp),
|
|
470
|
+
};
|
|
471
|
+
return smOrder;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private static _flagToOrderType(order: SmartContractOrder): string {
|
|
475
|
+
let hasTrigger = BigNumber.from(order.fTriggerPrice).eq(0);
|
|
476
|
+
let hasLimit = !BigNumber.from(order.fTriggerPrice).eq(0) || !BigNumber.from(order.fTriggerPrice).eq(MAX_64x64);
|
|
477
|
+
if (hasTrigger && hasLimit) {
|
|
478
|
+
return ORDER_TYPE_STOP_LIMIT;
|
|
479
|
+
} else if (hasTrigger && !hasLimit) {
|
|
480
|
+
return ORDER_TYPE_STOP_MARKET;
|
|
481
|
+
} else if (hasLimit && containsFlag(BigNumber.from(order.flags), MASK_LIMIT_ORDER)) {
|
|
482
|
+
return ORDER_TYPE_LIMIT;
|
|
483
|
+
} else {
|
|
484
|
+
return ORDER_TYPE_MARKET;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Determine the correct order flags based on the order-properties.
|
|
490
|
+
* Checks for some misspecifications.
|
|
491
|
+
* @param order order type
|
|
492
|
+
* @returns BigNumber flags
|
|
493
|
+
*/
|
|
494
|
+
private static _orderTypeToFlag(order: Order): BigNumber {
|
|
495
|
+
let flag: BigNumber;
|
|
496
|
+
switch (order.type) {
|
|
497
|
+
case ORDER_TYPE_LIMIT:
|
|
498
|
+
flag = MASK_LIMIT_ORDER;
|
|
499
|
+
break;
|
|
500
|
+
case ORDER_TYPE_MARKET:
|
|
501
|
+
flag = MASK_MARKET_ORDER;
|
|
502
|
+
break;
|
|
503
|
+
case ORDER_TYPE_STOP_MARKET:
|
|
504
|
+
flag = MASK_STOP_ORDER;
|
|
505
|
+
break;
|
|
506
|
+
case ORDER_TYPE_STOP_LIMIT:
|
|
507
|
+
flag = MASK_STOP_ORDER;
|
|
508
|
+
break;
|
|
509
|
+
default: {
|
|
510
|
+
throw Error(`Order type ${order.type} not found.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (order.keepPositionLvg != undefined && order.keepPositionLvg) {
|
|
514
|
+
flag = combineFlags(flag, MASK_KEEP_POS_LEVERAGE);
|
|
515
|
+
}
|
|
516
|
+
if (order.reduceOnly != undefined && order.reduceOnly) {
|
|
517
|
+
flag = combineFlags(flag, MASK_CLOSE_ONLY);
|
|
518
|
+
}
|
|
519
|
+
if ((order.type == ORDER_TYPE_LIMIT || order.type == ORDER_TYPE_STOP_LIMIT) && order.limitPrice == undefined) {
|
|
520
|
+
throw Error(`Order type ${order.type} requires limit price.`);
|
|
521
|
+
}
|
|
522
|
+
if ((order.type == ORDER_TYPE_STOP_MARKET || order.type == ORDER_TYPE_STOP_LIMIT) && order.stopPrice == undefined) {
|
|
523
|
+
throw Error(`Order type ${order.type} requires trigger price.`);
|
|
524
|
+
}
|
|
525
|
+
if ((order.type == ORDER_TYPE_MARKET || order.type == ORDER_TYPE_LIMIT) && order.stopPrice != undefined) {
|
|
526
|
+
throw Error(`Order type ${order.type} has no trigger price.`);
|
|
527
|
+
}
|
|
528
|
+
if (order.type != ORDER_TYPE_MARKET && order.stopPrice != undefined) {
|
|
529
|
+
throw Error(`Order type ${order.type} has no trigger price.`);
|
|
530
|
+
}
|
|
531
|
+
return flag;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Read config file into NodeSDKConfig interface
|
|
536
|
+
* @param fileLocation json-file with required variables for config
|
|
537
|
+
* @returns NodeSDKConfig
|
|
538
|
+
*/
|
|
539
|
+
public static readSDKConfig(fileLocation: string): NodeSDKConfig {
|
|
540
|
+
if (fileLocation == DEFAULT_CONFIG_MAINNET_NAME) {
|
|
541
|
+
fileLocation = DEFAULT_CONFIG_MAINNET;
|
|
542
|
+
} else if (fileLocation == DEFAULT_CONFIG_TESTNET_NAME) {
|
|
543
|
+
fileLocation = DEFAULT_CONFIG_TESTNET;
|
|
544
|
+
}
|
|
545
|
+
let configFile = require(fileLocation);
|
|
546
|
+
let config: NodeSDKConfig = <NodeSDKConfig>configFile;
|
|
547
|
+
return config;
|
|
548
|
+
}
|
|
549
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { BigNumber } from "ethers";
|
|
2
|
+
|
|
3
|
+
const ethers = require("ethers");
|
|
4
|
+
|
|
5
|
+
function _isVocal(char: string) {
|
|
6
|
+
char = char.toLowerCase();
|
|
7
|
+
return char == "a" || char == "e" || char == "i" || char == "o" || char == "u";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
*
|
|
12
|
+
* @param s string to shorten/extend to 4 characters
|
|
13
|
+
* @returns string with 4 characters (or characters + null chars)
|
|
14
|
+
*/
|
|
15
|
+
export function to4Chars(s: string) {
|
|
16
|
+
while (s.length < 4) {
|
|
17
|
+
s = s + "\0";
|
|
18
|
+
}
|
|
19
|
+
let k = s.length - 1;
|
|
20
|
+
while (s.length > 4 && k >= 0) {
|
|
21
|
+
// chop off vocals from the end of string
|
|
22
|
+
// e.g. MATIC -> MATC
|
|
23
|
+
if (_isVocal(s.charAt(k))) {
|
|
24
|
+
s = s.substring(0, k) + s.substring(k + 1, s.length);
|
|
25
|
+
}
|
|
26
|
+
k--;
|
|
27
|
+
}
|
|
28
|
+
s = s.substring(0, 4);
|
|
29
|
+
return s;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Converts string into 4-character bytes4
|
|
34
|
+
* uses to4Chars to first convert the string into
|
|
35
|
+
* 4 characters.
|
|
36
|
+
* Resulting buffer can be used with smart contract to
|
|
37
|
+
* identify tokens (BTC, USDC, MATIC etc.)
|
|
38
|
+
* @param s string to encode into bytes4
|
|
39
|
+
* @returns buffer
|
|
40
|
+
*/
|
|
41
|
+
export function toBytes4(s: string): Buffer {
|
|
42
|
+
s = to4Chars(s);
|
|
43
|
+
let valBuff: Buffer = Buffer.from(s, "ascii");
|
|
44
|
+
return valBuff;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decodes a buffer encoded with toBytes4 into
|
|
49
|
+
* a string. The string is the result of to4Chars of the
|
|
50
|
+
* originally encoded string stripped from null-chars
|
|
51
|
+
* @param b correctly encoded bytes4 buffer using toBytes4
|
|
52
|
+
* @returns string decoded into to4Chars-type string without null characters
|
|
53
|
+
*/
|
|
54
|
+
export function fromBytes4(b: Buffer): string {
|
|
55
|
+
let val: string = b.toString("ascii");
|
|
56
|
+
val = val.replace(/\0/g, "");
|
|
57
|
+
return val;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Decodes the bytes4 encoded string received from the
|
|
62
|
+
* smart contract as a hex-number in string-format
|
|
63
|
+
* @param s string representing a hex-number ("0x...")
|
|
64
|
+
* @returns x of to4Chars(x) stripped from null-chars,
|
|
65
|
+
* where x was originally encoded and
|
|
66
|
+
* returned by the smart contract as bytes4
|
|
67
|
+
*/
|
|
68
|
+
export function fromBytes4HexString(s: string): string {
|
|
69
|
+
let res = "";
|
|
70
|
+
for (let k = 2; k < s.length; k = k + 2) {
|
|
71
|
+
res = res + String.fromCharCode(parseInt(s.substring(k, k + 2), 16));
|
|
72
|
+
}
|
|
73
|
+
res = res.replace(/\0/g, "");
|
|
74
|
+
return res;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function combineFlags(f1: BigNumber, f2: BigNumber): BigNumber {
|
|
78
|
+
return BigNumber.from(parseInt(f1.toString()) | parseInt(f2.toString()));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function containsFlag(f1: BigNumber, f2: BigNumber): boolean {
|
|
82
|
+
return (parseInt(f1.toString()) & parseInt(f2.toString())) > 0;
|
|
83
|
+
}
|