@strkfarm/sdk 2.0.0-dev.29 → 2.0.0-dev.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strkfarm/sdk",
3
- "version": "2.0.0-dev.29",
3
+ "version": "2.0.0-dev.30",
4
4
  "description": "STRKFarm TS SDK (Meant for our internal use, but feel free to use it)",
5
5
  "typings": "dist/index.d.ts",
6
6
  "types": "dist/index.d.ts",
@@ -256,6 +256,7 @@ export interface CreateOrderRequest {
256
256
  price: string;
257
257
  side: OrderSide;
258
258
  post_only?: boolean;
259
+ reduce_only?: boolean;
259
260
  previous_order_id?: number;
260
261
  external_id?: string;
261
262
  time_in_force?: TimeInForce;
@@ -3,6 +3,7 @@
3
3
  * Provides a clean interface to interact with the Extended Exchange trading API
4
4
  */
5
5
 
6
+ import BigNumber from "bignumber.js";
6
7
  import {
7
8
  CreateOrderRequest,
8
9
  WithdrawRequest,
@@ -26,12 +27,88 @@ import {
26
27
  UpdateLeverageRequest,
27
28
  } from "./types";
28
29
 
30
+ type ExtendedTradingRules = {
31
+ minOrderSize: BigNumber;
32
+ qtyStep: BigNumber;
33
+ priceStep: BigNumber;
34
+ };
35
+
36
+ function asRecord(v: unknown): Record<string, unknown> | null {
37
+ return v !== null && typeof v === "object" && !Array.isArray(v)
38
+ ? (v as Record<string, unknown>)
39
+ : null;
40
+ }
41
+
42
+ function pickTradingConfig(row: Record<string, unknown>): Record<string, unknown> | null {
43
+ const tc = row.trading_config ?? row.tradingConfig;
44
+ return asRecord(tc);
45
+ }
46
+
47
+ function readTcString(
48
+ tc: Record<string, unknown>,
49
+ snake: string,
50
+ camel: string,
51
+ ): string | undefined {
52
+ const v = tc[snake] ?? tc[camel];
53
+ if (v === undefined || v === null) return undefined;
54
+ return String(v).trim();
55
+ }
56
+
57
+ function tradingRulesFromMarketRow(
58
+ marketName: string,
59
+ row: unknown,
60
+ ): ExtendedTradingRules {
61
+ const r = asRecord(row);
62
+ if (!r) {
63
+ throw new Error(`ExtendedWrapper: invalid market payload for ${marketName}`);
64
+ }
65
+ const tc = pickTradingConfig(r);
66
+ if (!tc) {
67
+ throw new Error(`ExtendedWrapper: missing tradingConfig for market ${marketName}`);
68
+ }
69
+ const minS = readTcString(tc, "min_order_size", "minOrderSize");
70
+ const qtyStep = readTcString(tc, "min_order_size_change", "minOrderSizeChange");
71
+ const pxStep = readTcString(tc, "min_price_change", "minPriceChange");
72
+ if (!minS || !qtyStep || !pxStep) {
73
+ throw new Error(`ExtendedWrapper: incomplete tradingConfig for market ${marketName}`);
74
+ }
75
+ const minOrderSize = new BigNumber(minS);
76
+ const qty = new BigNumber(qtyStep);
77
+ const px = new BigNumber(pxStep);
78
+ if (!minOrderSize.isFinite() || minOrderSize.lte(0)) {
79
+ throw new Error(`ExtendedWrapper: invalid minOrderSize for ${marketName}`);
80
+ }
81
+ if (!qty.isFinite() || qty.lte(0)) {
82
+ throw new Error(`ExtendedWrapper: invalid minOrderSizeChange for ${marketName}`);
83
+ }
84
+ if (!px.isFinite() || px.lte(0)) {
85
+ throw new Error(`ExtendedWrapper: invalid minPriceChange for ${marketName}`);
86
+ }
87
+ return { minOrderSize, qtyStep: qty, priceStep: px };
88
+ }
89
+
90
+ function roundToStepBn(value: BigNumber, step: BigNumber): BigNumber {
91
+ if (step.lte(0)) return value;
92
+ return value.div(step).round(0, BigNumber.ROUND_HALF_UP).times(step);
93
+ }
94
+
95
+ function formatBnForApi(bn: BigNumber, step: BigNumber): string {
96
+ const dp = Math.max(step.decimalPlaces() ?? 0, bn.decimalPlaces() ?? 0, 0);
97
+ return Number(bn.toFixed(Math.min(80, dp))).toString();
98
+ }
99
+
29
100
  export class ExtendedWrapper {
30
101
  private readUrl: string;
31
102
  private writeUrl: string;
32
103
  private apiKey?: string;
33
104
  private timeout: number;
34
105
  private retries: number;
106
+ /** Per-market rules from GET /markets (tradingConfig); retained for process lifetime (no TTL). */
107
+ private marketTradingRulesCache = new Map<string, ExtendedTradingRules>();
108
+ private marketTradingRulesInflight = new Map<
109
+ string,
110
+ Promise<ExtendedTradingRules>
111
+ >();
35
112
 
36
113
  constructor(config: ExtendedWrapperConfig) {
37
114
  this.apiKey = config.apiKey;
@@ -126,15 +203,80 @@ export class ExtendedWrapper {
126
203
  throw lastError || new Error("Request failed after all retries");
127
204
  }
128
205
 
206
+ private async resolveTradingRules(
207
+ marketName: string,
208
+ ): Promise<ExtendedTradingRules> {
209
+ const cached = this.marketTradingRulesCache.get(marketName);
210
+ if (cached) return cached;
211
+ const existing = this.marketTradingRulesInflight.get(marketName);
212
+ if (existing) return existing;
213
+ const inflight = (async () => {
214
+ const res = await this.getMarkets(marketName);
215
+ if (res.status !== "OK") {
216
+ throw new Error(
217
+ `ExtendedWrapper: getMarkets failed for ${marketName}: ${res.message}`,
218
+ );
219
+ }
220
+ const rows = res.data;
221
+ if (!Array.isArray(rows) || rows.length === 0) {
222
+ throw new Error(
223
+ `ExtendedWrapper: empty markets response for ${marketName}`,
224
+ );
225
+ }
226
+ const row = rows.find((m) => asRecord(m)?.name === marketName);
227
+ if (!row) {
228
+ throw new Error(
229
+ `ExtendedWrapper: market ${marketName} not found in markets list`,
230
+ );
231
+ }
232
+ const rules = tradingRulesFromMarketRow(marketName, row);
233
+ this.marketTradingRulesCache.set(marketName, rules);
234
+ return rules;
235
+ })();
236
+ this.marketTradingRulesInflight.set(marketName, inflight);
237
+ void inflight.finally(() => {
238
+ if (this.marketTradingRulesInflight.get(marketName) === inflight) {
239
+ this.marketTradingRulesInflight.delete(marketName);
240
+ }
241
+ });
242
+ return inflight;
243
+ }
244
+
129
245
  /**
130
246
  * Create a new order on Extended Exchange
131
247
  */
132
248
  async createOrder(
133
249
  request: CreateOrderRequest,
134
250
  ): Promise<ExtendedApiResponse<PlacedOrder>> {
251
+ const rules = await this.resolveTradingRules(request.market_name);
252
+ const amountBn = new BigNumber(String(request.amount).trim());
253
+ const priceBn = new BigNumber(String(request.price).trim());
254
+ if (!amountBn.isFinite() || amountBn.lte(0)) {
255
+ throw new Error(`ExtendedWrapper: invalid order amount=${request.amount}`);
256
+ }
257
+ if (!priceBn.isFinite() || priceBn.lte(0)) {
258
+ throw new Error(`ExtendedWrapper: invalid order price=${request.price}`);
259
+ }
260
+ if (amountBn.lt(rules.minOrderSize)) {
261
+ throw new Error(
262
+ `ExtendedWrapper: order amount ${request.amount} is below minOrderSize ${rules.minOrderSize.toFixed()} for ${request.market_name}`,
263
+ );
264
+ }
265
+ const adjAmount = roundToStepBn(amountBn, rules.qtyStep);
266
+ const adjPrice = roundToStepBn(priceBn, rules.priceStep);
267
+ if (adjAmount.lt(rules.minOrderSize)) {
268
+ throw new Error(
269
+ `ExtendedWrapper: amount after tick rounding ${formatBnForApi(adjAmount, rules.qtyStep)} is below minOrderSize ${rules.minOrderSize.toFixed()} (${request.market_name})`,
270
+ );
271
+ }
272
+ const payload: CreateOrderRequest = {
273
+ ...request,
274
+ amount: formatBnForApi(adjAmount, rules.qtyStep),
275
+ price: formatBnForApi(adjPrice, rules.priceStep),
276
+ };
135
277
  return this.makeRequest<PlacedOrder>("/api/v1/orders", false, {
136
278
  method: "POST",
137
- body: JSON.stringify(request),
279
+ body: JSON.stringify(payload),
138
280
  });
139
281
  }
140
282
 
@@ -323,11 +465,11 @@ export class ExtendedWrapper {
323
465
  amount: string,
324
466
  price: string,
325
467
  options: {
326
- postOnly?: boolean;
327
- reduceOnly?: boolean;
328
- previousOrderId?: number;
329
- externalId?: string;
330
- timeInForce?: TimeInForce;
468
+ post_only?: boolean;
469
+ reduce_only?: boolean;
470
+ previous_order_id?: number;
471
+ external_id?: string;
472
+ time_in_force?: TimeInForce;
331
473
  } = {},
332
474
  ): Promise<ExtendedApiResponse<PlacedOrder>> {
333
475
  return this.createOrder({
@@ -356,11 +498,11 @@ export class ExtendedWrapper {
356
498
  amount: string,
357
499
  price: string,
358
500
  options: {
359
- postOnly?: boolean;
360
- reduceOnly?: boolean;
361
- previousOrderId?: number;
362
- externalId?: string;
363
- timeInForce?: TimeInForce;
501
+ post_only?: boolean;
502
+ reduce_only?: boolean;
503
+ previous_order_id?: number;
504
+ external_id?: string;
505
+ time_in_force?: TimeInForce;
364
506
  } = {},
365
507
  ): Promise<ExtendedApiResponse<PlacedOrder>> {
366
508
  return this.createOrder({
@@ -46,6 +46,8 @@ export class EkuboQuoter {
46
46
  const url = this.ENDPOINT.replace("{{AMOUNT}}", amount.toWei()).replace("{{TOKEN_FROM_ADDRESS}}", fromToken).replace("{{TOKEN_TO_ADDRESS}}", toToken);
47
47
  logger.verbose(`EkuboQuoter::_callQuoterApi url: ${url}`);
48
48
  const quote = await axios.get(url);
49
+ // console.log('quote', quote.data);
50
+ // console.log('')
49
51
  return quote.data as EkuboQuote;
50
52
  } catch (error: any) {
51
53
  logger.error(`EkuboQuoter::_callQuoterApi error: ${error.message}`);
@@ -519,14 +519,15 @@ export class ExtendedAdapter extends BaseAdapter<
519
519
  logger.error("error initializing client");
520
520
  return null;
521
521
  }
522
- const setLeverage = await this.setLeverage(
523
- leverage,
524
- this.config.extendedMarketName,
525
- );
526
- if (!setLeverage) {
527
- logger.error("error depositing or setting leverage");
528
- return null;
529
- }
522
+ // todo, instead get levgerage and log errors if not ideal lever already
523
+ // const setLeverage = await this.setLeverage(
524
+ // leverage,
525
+ // this.config.extendedMarketName,
526
+ // );
527
+ // if (!setLeverage) {
528
+ // logger.error("error depositing or setting leverage");
529
+ // return null;
530
+ // }
530
531
  const { ask, bid } = await this.fetchOrderBookFromExtended();
531
532
  if (
532
533
  !ask ||
@@ -705,14 +706,14 @@ export class ExtendedAdapter extends BaseAdapter<
705
706
  const result =
706
707
  side === OrderSide.SELL
707
708
  ? await client.createSellOrder(marketName, amount, price, {
708
- postOnly: false,
709
- reduceOnly: false,
710
- timeInForce: TimeInForce.IOC,
709
+ post_only: false,
710
+ reduce_only: false,
711
+ time_in_force: TimeInForce.IOC,
711
712
  })
712
713
  : await client.createBuyOrder(marketName, amount, price, {
713
- postOnly: false,
714
- reduceOnly: true,
715
- timeInForce: TimeInForce.IOC,
714
+ post_only: false,
715
+ reduce_only: true,
716
+ time_in_force: TimeInForce.IOC,
716
717
  });
717
718
  if (result.data.id) {
718
719
  const position_id = result.data.id.toString();
@@ -648,7 +648,7 @@ export class VesuAdapter extends CacheClass {
648
648
  throw new Error('LTV is 0');
649
649
  }
650
650
  this.setCache(CACHE_KEY, ltv, 300000); // ttl: 5min
651
- return this.getCache<number>(CACHE_KEY) as number;
651
+ return ltv;
652
652
  }
653
653
 
654
654
  async getPositions(config: IConfig, blockNumber: BlockIdentifier = 'latest'): Promise<VaultPosition[]> {
@@ -752,6 +752,9 @@ export class VesuAdapter extends CacheClass {
752
752
  debtPrice = (await pricer.getPrice(this.config.debt.priceProxySymbol || this.config.debt.symbol)).price;
753
753
  }
754
754
  logger.verbose(`VesuAdapter::getAssetPrices collateralPrice: ${collateralPrice}, debtPrice: ${debtPrice}`);
755
+ if (isNaN(collateralPrice) || isNaN(debtPrice) || collateralPrice == 0 || debtPrice == 0) {
756
+ throw new Error(`VesuAdapter::getAssetPrices collateralPrice: ${collateralPrice}, debtPrice: ${debtPrice}`);
757
+ }
755
758
  return {
756
759
  collateralTokenAmount,
757
760
  collateralUSDAmount,
@@ -196,25 +196,34 @@ export class VesuMultiplyAdapter extends BaseAdapter<
196
196
  }
197
197
 
198
198
  private _buildZeroAmountSwapsWithWeights(
199
- quote: { splits: any[] },
199
+ quote: EkuboQuote,
200
200
  token: TokenInfo,
201
201
  reverseRoutes = false,
202
202
  ): { swaps: Swap[]; weights: Web3Number[] } {
203
203
  const swaps: Swap[] = quote.splits.map((split) => {
204
- const routeNodes = split.route.map((_route: any) => ({
205
- pool_key: {
206
- token0: ContractAddr.from(_route.pool_key.token0),
207
- token1: ContractAddr.from(_route.pool_key.token1),
208
- fee: _route.pool_key.fee,
209
- tick_spacing: _route.pool_key.tick_spacing.toString(),
210
- extension: _route.pool_key.extension,
211
- },
212
- sqrt_ratio_limit: Web3Number.fromWei(_route.sqrt_ratio_limit, 18),
213
- skip_ahead: Web3Number.fromWei(_route.skip_ahead, 0),
214
- }));
215
- if (reverseRoutes) {
216
- routeNodes.reverse();
217
- }
204
+ let sellToken = token.address;
205
+ const routeNodes = split.route.map((_route: any) => {
206
+ // switch to other for next route.
207
+ let isSellToken0 = sellToken.eqString(_route.pool_key.token0);
208
+ isSellToken0 = reverseRoutes ? !isSellToken0 : isSellToken0;
209
+ sellToken = isSellToken0 ? ContractAddr.from(_route.pool_key.token1) : ContractAddr.from(_route.pool_key.token0);
210
+
211
+ return {
212
+ pool_key: {
213
+ token0: ContractAddr.from(_route.pool_key.token0),
214
+ token1: ContractAddr.from(_route.pool_key.token1),
215
+ fee: _route.pool_key.fee,
216
+ tick_spacing: _route.pool_key.tick_spacing.toString(),
217
+ extension: _route.pool_key.extension,
218
+ },
219
+ sqrt_ratio_limit: isSellToken0 ? Web3Number.fromWei(MIN_SQRT_RATIO_LIMIT.toString(), 18) : Web3Number.fromWei(MAX_SQRT_RATIO_LIMIT.toString(), 18),
220
+ skip_ahead: Web3Number.fromWei(_route.skip_ahead, 0),
221
+ }
222
+ });
223
+ // incorrect logic, hence commented. Ekubo already returns routes in correct order.
224
+ // if (reverseRoutes) {
225
+ // routeNodes.reverse();
226
+ // }
218
227
  return {
219
228
  route: routeNodes,
220
229
  token_amount: {
@@ -684,7 +693,7 @@ export class VesuMultiplyAdapter extends BaseAdapter<
684
693
  dexPrice
685
694
  );
686
695
 
687
- logger.verbose(
696
+ logger.info(
688
697
  `${VesuMultiplyAdapter.name}::_getIncreaseCalldata debtAmount: ${debtAmount}, addedCollateral: ${addedCollateral}`
689
698
  );
690
699
  let leverSwap: Swap[] = [];
@@ -738,7 +747,7 @@ export class VesuMultiplyAdapter extends BaseAdapter<
738
747
  await new Promise((resolve) => setTimeout(resolve, 10000));
739
748
  } else {
740
749
  throw new Error(
741
- `VesuMultiplyAdapter: Price impact too high (${swapQuote.price_impact}), skipping swap`
750
+ `VesuMultiplyAdapter: Price impact too high (${swapQuote.price_impact}), skipping swap, debtAmount=${debtAmount.toNumber()}, collateralPrice=${collateralPrice}, debtPrice=${debtPrice}`
742
751
  );
743
752
  }
744
753
  } catch (error) {
@@ -1082,14 +1082,14 @@ export class ExecutionService {
1082
1082
  }
1083
1083
 
1084
1084
  // set extended leverage
1085
- const setLevResult = await this._config.extendedAdapter.setLeverage(calculateExtendedLevergae().toString(), this._config.extendedAdapter.config.extendedMarketName);
1086
- if (!setLevResult) {
1087
- logger.error(
1088
- `${this._tag}::_executeCoordinatedCase failed to set leverage`,
1089
- );
1090
- results.push(this._failureResult(extendedRoute));
1091
- return results;
1092
- }
1085
+ // const setLevResult = await this._config.extendedAdapter.setLeverage(calculateExtendedLevergae().toString(), this._config.extendedAdapter.config.extendedMarketName);
1086
+ // if (!setLevResult) {
1087
+ // logger.error(
1088
+ // `${this._tag}::_executeCoordinatedCase failed to set leverage`,
1089
+ // );
1090
+ // results.push(this._failureResult(extendedRoute));
1091
+ // return results;
1092
+ // }
1093
1093
 
1094
1094
  const isIncrease = ExecutionService.INCREASE_EXPOSURE_ROUTES.has(
1095
1095
  extendedRoute.type,
@@ -1116,8 +1116,9 @@ export class ExecutionService {
1116
1116
  onChainCalls = callArrays.flat();
1117
1117
  } catch (err) {
1118
1118
  logger.error(
1119
- `${this._tag}::_executeCoordinatedCase on-chain call construction failed: ${err}`,
1119
+ `${this._tag}::_executeCoordinatedCase on-chain call construction failed:`,
1120
1120
  );
1121
+ console.error(err);
1121
1122
  await this._emitEvent(ExecutionEventType.FAILURE, {
1122
1123
  routeSummary: `coordinated on-chain build for case "${caseEntry.case.id}"`,
1123
1124
  error: `${err}`,
@@ -1207,7 +1208,7 @@ export class ExecutionService {
1207
1208
  const timeoutMs =
1208
1209
  this._config.extendedFillTimeoutMs ?? DEFAULT_EXTENDED_FILL_TIMEOUT_MS;
1209
1210
  const extResult = await this._executeExtendedLimitOrderWithRecovery(
1210
- btcAmount,
1211
+ Math.abs(btcAmount),
1211
1212
  executionPrice,
1212
1213
  side,
1213
1214
  {
@@ -1873,7 +1874,7 @@ export class ExecutionService {
1873
1874
  return this._failureResult(route);
1874
1875
  }
1875
1876
  const closeResult = await this._executeExtendedLimitOrderWithRecovery(
1876
- positionToClose.toNumber(),
1877
+ Math.abs(positionToClose.toNumber()),
1877
1878
  midPrice,
1878
1879
  OrderSide.BUY,
1879
1880
  );
@@ -1894,7 +1895,7 @@ export class ExecutionService {
1894
1895
  );
1895
1896
  // todo need to ensure this one passes for sure
1896
1897
  const reopenResult = await this._executeExtendedLimitOrderWithRecovery(
1897
- positionToClose.toNumber(),
1898
+ Math.abs(positionToClose.toNumber()),
1898
1899
  midPrice,
1899
1900
  OrderSide.SELL,
1900
1901
  );
@@ -1957,7 +1958,7 @@ export class ExecutionService {
1957
1958
  return this._failureResult(route);
1958
1959
  }
1959
1960
  const result = await this._executeExtendedLimitOrderWithRecovery(
1960
- route.amount.toNumber(),
1961
+ Math.abs(route.amount.toNumber()),
1961
1962
  midPrice,
1962
1963
  OrderSide.SELL,
1963
1964
  );
@@ -2013,7 +2014,7 @@ export class ExecutionService {
2013
2014
  return this._failureResult(route);
2014
2015
  }
2015
2016
  const result = await this._executeExtendedLimitOrderWithRecovery(
2016
- route.amount.toNumber(),
2017
+ Math.abs(route.amount.toNumber()),
2017
2018
  midPrice,
2018
2019
  OrderSide.BUY,
2019
2020
  );