@strkfarm/sdk 1.2.0 → 2.0.0-dev-strategy2.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.
Files changed (60) hide show
  1. package/dist/index.browser.global.js +76556 -66640
  2. package/dist/index.browser.mjs +34235 -24392
  3. package/dist/index.d.ts +2372 -793
  4. package/dist/index.js +31967 -22084
  5. package/dist/index.mjs +25545 -15719
  6. package/package.json +86 -76
  7. package/readme.md +56 -1
  8. package/src/data/extended-deposit.abi.json +3613 -0
  9. package/src/data/universal-vault.abi.json +135 -20
  10. package/src/dataTypes/_bignumber.ts +11 -0
  11. package/src/dataTypes/address.ts +7 -0
  12. package/src/global.ts +240 -193
  13. package/src/interfaces/common.tsx +26 -2
  14. package/src/modules/ExtendedWrapperSDk/index.ts +62 -0
  15. package/src/modules/ExtendedWrapperSDk/types.ts +311 -0
  16. package/src/modules/ExtendedWrapperSDk/wrapper.ts +448 -0
  17. package/src/modules/avnu.ts +17 -4
  18. package/src/modules/ekubo-quoter.ts +89 -10
  19. package/src/modules/erc20.ts +67 -21
  20. package/src/modules/harvests.ts +29 -43
  21. package/src/modules/index.ts +5 -1
  22. package/src/modules/lst-apr.ts +36 -0
  23. package/src/modules/midas.ts +159 -0
  24. package/src/modules/pricer-from-api.ts +2 -2
  25. package/src/modules/pricer-lst.ts +1 -1
  26. package/src/modules/pricer.ts +3 -38
  27. package/src/modules/token-market-data.ts +202 -0
  28. package/src/node/deployer.ts +1 -36
  29. package/src/strategies/autoCompounderStrk.ts +1 -1
  30. package/src/strategies/base-strategy.ts +20 -3
  31. package/src/strategies/btc-vesu-extended-strategy/core-strategy.tsx +1486 -0
  32. package/src/strategies/btc-vesu-extended-strategy/services/operationService.ts +32 -0
  33. package/src/strategies/btc-vesu-extended-strategy/utils/constants.ts +3 -0
  34. package/src/strategies/btc-vesu-extended-strategy/utils/helper.ts +396 -0
  35. package/src/strategies/btc-vesu-extended-strategy/utils/types.ts +5 -0
  36. package/src/strategies/ekubo-cl-vault.tsx +123 -306
  37. package/src/strategies/index.ts +7 -1
  38. package/src/strategies/svk-strategy.ts +247 -0
  39. package/src/strategies/universal-adapters/adapter-optimizer.ts +65 -0
  40. package/src/strategies/universal-adapters/adapter-utils.ts +5 -1
  41. package/src/strategies/universal-adapters/avnu-adapter.ts +432 -0
  42. package/src/strategies/universal-adapters/baseAdapter.ts +181 -153
  43. package/src/strategies/universal-adapters/common-adapter.ts +98 -77
  44. package/src/strategies/universal-adapters/extended-adapter.ts +976 -0
  45. package/src/strategies/universal-adapters/index.ts +7 -1
  46. package/src/strategies/universal-adapters/unused-balance-adapter.ts +109 -0
  47. package/src/strategies/universal-adapters/vesu-adapter.ts +230 -230
  48. package/src/strategies/universal-adapters/vesu-borrow-adapter.ts +1247 -0
  49. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1306 -0
  50. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +58 -51
  51. package/src/strategies/universal-lst-muliplier-strategy.tsx +716 -844
  52. package/src/strategies/universal-strategy.tsx +1103 -1181
  53. package/src/strategies/vesu-extended-strategy/services/operationService.ts +34 -0
  54. package/src/strategies/vesu-extended-strategy/types/transaction-metadata.ts +25 -0
  55. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +77 -0
  56. package/src/strategies/vesu-extended-strategy/utils/constants.ts +50 -0
  57. package/src/strategies/vesu-extended-strategy/utils/helper.ts +367 -0
  58. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +1420 -0
  59. package/src/strategies/vesu-rebalance.tsx +16 -20
  60. package/src/utils/health-factor-math.ts +11 -5
@@ -0,0 +1,976 @@
1
+ import {
2
+ BaseAdapter,
3
+ DepositParams,
4
+ WithdrawParams,
5
+ BaseAdapterConfig,
6
+ } from "./baseAdapter";
7
+ import { Protocols } from "@/interfaces";
8
+ import { SupportedPosition } from "./baseAdapter";
9
+ import { PositionAPY, APYType, PositionAmount } from "./baseAdapter";
10
+ import { Web3Number } from "@/dataTypes";
11
+ import { PositionInfo } from "./baseAdapter";
12
+ import { ManageCall } from "./baseAdapter";
13
+ import { ContractAddr } from "@/dataTypes";
14
+ import { AVNU_EXCHANGE_FOR_LEGACY_USDC } from "./adapter-utils";
15
+ import { StandardMerkleTree } from "@/utils";
16
+ import { hash, uint256 } from "starknet";
17
+ import { Global } from "@/global";
18
+ import { AdapterLeafType, GenerateCallFn } from "./baseAdapter";
19
+ import {
20
+ AVNU_LEGACY_SANITIZER,
21
+ EXTENDED_SANITIZER,
22
+ SIMPLE_SANITIZER,
23
+ toBigInt,
24
+ } from "./adapter-utils";
25
+ import ExtendedWrapper, {
26
+ AssetOperationStatus,
27
+ AssetOperationType,
28
+ FundingRate,
29
+ OrderSide,
30
+ Position,
31
+ TimeInForce,
32
+ } from "@/modules/ExtendedWrapperSDk";
33
+ import { Balance, OpenOrder, OrderStatus } from "@/modules/ExtendedWrapperSDk";
34
+ import axios from "axios";
35
+ import { AvnuAdapter } from "./avnu-adapter";
36
+ import { logger } from "@/utils";
37
+ export interface ExtendedAdapterConfig extends BaseAdapterConfig {
38
+ vaultIdExtended: number;
39
+ extendedContract: ContractAddr; //0x04270219d365d6b017231b52e92b3fb5d7c8378b05e9abc97724537a80e93b0f
40
+ extendedBackendUrl: string;
41
+ extendedApiKey: string;
42
+ extendedTimeout: number;
43
+ extendedRetries: number;
44
+ extendedBaseUrl: string;
45
+ extendedMarketName: string;
46
+ extendedPrecision: number;
47
+ avnuAdapter: AvnuAdapter;
48
+ retryDelayForOrderStatus: number;
49
+ minimumExtendedMovementAmount: number;
50
+ }
51
+
52
+ export class ExtendedAdapter extends BaseAdapter<
53
+ DepositParams,
54
+ WithdrawParams
55
+ > {
56
+ readonly config: ExtendedAdapterConfig;
57
+ readonly client: ExtendedWrapper;
58
+ readonly retryDelayForOrderStatus: number;
59
+ readonly minimumExtendedMovementAmount: number;
60
+
61
+ constructor(config: ExtendedAdapterConfig) {
62
+ super(config, ExtendedAdapter.name, Protocols.EXTENDED);
63
+ this.config = config as ExtendedAdapterConfig;
64
+ const client = new ExtendedWrapper({
65
+ baseUrl: this.config.extendedBackendUrl,
66
+ apiKey: this.config.extendedApiKey,
67
+ timeout: this.config.extendedTimeout,
68
+ retries: this.config.extendedRetries,
69
+ });
70
+ this.minimumExtendedMovementAmount =
71
+ this.config.minimumExtendedMovementAmount ?? 5; //5 usdc
72
+ this.client = client;
73
+ this.retryDelayForOrderStatus =
74
+ this.config.retryDelayForOrderStatus ?? 3000;
75
+ }
76
+ //abstract means the method has no implementation in this class; instead, child classes must implement it.
77
+ protected async getAPY(
78
+ supportedPosition: SupportedPosition
79
+ ): Promise<PositionAPY> {
80
+ /** Considering supportedPosiiton.isDebt as side parameter to get the funding rates */
81
+ const side = supportedPosition.isDebt ? "LONG" : "SHORT";
82
+ const fundingRates = await this.client.getFundingRates(
83
+ this.config.extendedMarketName,
84
+ side
85
+ );
86
+ if (fundingRates.status !== "OK") {
87
+ logger.error("error getting funding rates", fundingRates);
88
+ return { apy: 0, type: APYType.BASE };
89
+ }
90
+ const fundingRate: FundingRate = fundingRates.data[0];
91
+ const apy = Number(fundingRate.f) * 365 * 24;
92
+ return { apy: apy, type: APYType.BASE };
93
+ }
94
+
95
+ protected async getPosition(
96
+ supportedPosition: SupportedPosition
97
+ ): Promise<PositionAmount> {
98
+ if (!this.client) {
99
+ throw new Error("Client not initialized");
100
+ }
101
+ const holdings = await this.getExtendedDepositAmount();
102
+ if (!holdings) {
103
+ throw new Error("No position found");
104
+ }
105
+ // equity is the total amount on extended
106
+ const amount = holdings.equity;
107
+ return Promise.resolve({
108
+ amount: new Web3Number(amount, 0),
109
+ remarks: `extendedAmount ${holdings.equity}`,
110
+ });
111
+ }
112
+
113
+ async maxDeposit(amount?: Web3Number): Promise<PositionInfo> {
114
+ return Promise.resolve({
115
+ tokenInfo: this.config.baseToken,
116
+ amount: new Web3Number(0, 0),
117
+ usdValue: 0,
118
+ apy: { apy: 0, type: APYType.BASE },
119
+ protocol: Protocols.EXTENDED,
120
+ remarks: "",
121
+ });
122
+ }
123
+
124
+ async maxWithdraw(): Promise<PositionInfo> {
125
+ return Promise.resolve({
126
+ tokenInfo: this.config.baseToken,
127
+ amount: new Web3Number(0, 0),
128
+ usdValue: 0,
129
+ apy: { apy: 0, type: APYType.BASE },
130
+ protocol: Protocols.EXTENDED,
131
+ remarks: "",
132
+ });
133
+ }
134
+
135
+ protected _getDepositLeaf(): {
136
+ target: ContractAddr;
137
+ method: string;
138
+ packedArguments: bigint[];
139
+ sanitizer: ContractAddr;
140
+ id: string;
141
+ }[] {
142
+ const usdceToken = Global.getDefaultTokens().find(
143
+ (token) => token.symbol === "USDCe"
144
+ )
145
+
146
+ return [
147
+ {
148
+ target: this.config.supportedPositions[0].asset.address,
149
+ method: "approve",
150
+ packedArguments: [AVNU_EXCHANGE_FOR_LEGACY_USDC.toBigInt()],
151
+ id: `extended_approve_${this.config.supportedPositions[0].asset.symbol}`,
152
+ sanitizer: AVNU_LEGACY_SANITIZER,
153
+ },
154
+ {
155
+ target: AVNU_EXCHANGE_FOR_LEGACY_USDC,
156
+ method: "swap_to_legacy",
157
+ packedArguments: [],
158
+ id: `extended_swap_to_legacy_${this.config.supportedPositions[0].asset.symbol}`,
159
+ sanitizer: AVNU_LEGACY_SANITIZER,
160
+ },
161
+ {
162
+ target: usdceToken!.address,
163
+ method: "approve",
164
+ packedArguments: [this.config.extendedContract.toBigInt()],
165
+ id: `extended_approve_${usdceToken!.symbol}`,
166
+ sanitizer: SIMPLE_SANITIZER,
167
+ },
168
+ {
169
+ target: this.config.extendedContract,
170
+ method: "deposit",
171
+ packedArguments: [BigInt(this.config.vaultIdExtended)],
172
+ sanitizer: EXTENDED_SANITIZER,
173
+ id: `extended_deposit_${usdceToken!.symbol}`,
174
+ },
175
+ ];
176
+ }
177
+
178
+ getSwapFromLegacyLeaf(): AdapterLeafType<DepositParams> {
179
+ const leafConfigs = this._getSwapFromLegacyLeaf();
180
+ const leaves = leafConfigs.map((config) => {
181
+ const { target, method, packedArguments, sanitizer, id } = config;
182
+ const leaf = this.constructSimpleLeafData(
183
+ {
184
+ id: id,
185
+ target,
186
+ method,
187
+ packedArguments,
188
+ },
189
+ sanitizer
190
+ );
191
+ return leaf;
192
+ });
193
+ return {
194
+ leaves,
195
+ callConstructor: this.getSwapFromLegacyCall.bind(
196
+ this
197
+ ) as unknown as GenerateCallFn<DepositParams>,
198
+ };
199
+ }
200
+
201
+ protected _getSwapFromLegacyLeaf(): {
202
+ target: ContractAddr;
203
+ method: string;
204
+ packedArguments: bigint[];
205
+ sanitizer: ContractAddr;
206
+ id: string;
207
+ }[] {
208
+ const usdceToken = Global.getDefaultTokens().find(
209
+ (token) => token.symbol === "USDCe"
210
+ );
211
+ return [
212
+ {
213
+ target: usdceToken!.address,
214
+ method: "approve",
215
+ packedArguments: [AVNU_EXCHANGE_FOR_LEGACY_USDC.toBigInt()],
216
+ id: `extendedswaplegacyapprove_${usdceToken!.symbol}`,
217
+ sanitizer: AVNU_LEGACY_SANITIZER,
218
+ },
219
+ {
220
+ target: AVNU_EXCHANGE_FOR_LEGACY_USDC,
221
+ method: "swap_to_new",
222
+ packedArguments: [],
223
+ id: `extended_swap_to_new_${usdceToken!.symbol}`,
224
+ sanitizer: AVNU_LEGACY_SANITIZER,
225
+ },
226
+ ];
227
+ }
228
+
229
+ protected _getWithdrawLeaf(): {
230
+ target: ContractAddr;
231
+ method: string;
232
+ packedArguments: bigint[];
233
+ sanitizer: ContractAddr;
234
+ id: string;
235
+ }[] {
236
+ /**
237
+ * Withdraw is done via extended wrapper
238
+ */
239
+ return [];
240
+ }
241
+
242
+ async getDepositCall(params: DepositParams): Promise<ManageCall[]> {
243
+ try {
244
+ const usdcToken = this.config.supportedPositions[0].asset;
245
+ const usdceToken = Global.getDefaultTokens().find(
246
+ (token) => token.symbol === "USDCe"
247
+ );
248
+ const salt = Math.floor(Math.random() * 10 ** usdcToken.decimals);
249
+ // Give approval for more amount than the required amount
250
+ const amount = uint256.bnToUint256(
251
+ params.amount.multipliedBy(10).multipliedBy(10 ** usdceToken!.decimals).toFixed(0)
252
+ );
253
+
254
+
255
+ console.log(params, params.amount.multipliedBy(10).multipliedBy(10 ** usdceToken!.decimals).toFixed(0))
256
+ const quotes = await this.config.avnuAdapter.getQuotesAvnu(
257
+ usdcToken.address.toString(),
258
+ usdceToken!.address.toString(),
259
+ params.amount.toNumber(),
260
+ this.config.avnuAdapter.config.vaultAllocator.address.toString(),
261
+ usdceToken!.decimals,
262
+ false
263
+ );
264
+
265
+ if (!quotes) {
266
+ logger.error("error getting quotes from avnu");
267
+ return [];
268
+ }
269
+ const getCalldata = await this.config.avnuAdapter.getSwapCallData(
270
+ quotes!
271
+ );
272
+ const swapCallData = getCalldata[0];
273
+ //change extended sanitizer here
274
+ return [
275
+ {
276
+ sanitizer: AVNU_LEGACY_SANITIZER,
277
+ call: {
278
+ contractAddress: usdcToken.address,
279
+ selector: hash.getSelectorFromName("approve"),
280
+ calldata: [
281
+ AVNU_EXCHANGE_FOR_LEGACY_USDC.toBigInt(),
282
+ toBigInt(amount.low.toString()), // amount low
283
+ toBigInt(amount.high.toString()), // amount high
284
+ ],
285
+ },
286
+ },
287
+ {
288
+ sanitizer: AVNU_LEGACY_SANITIZER,
289
+ call: {
290
+ contractAddress: AVNU_EXCHANGE_FOR_LEGACY_USDC,
291
+ selector: hash.getSelectorFromName("swap_to_legacy"),
292
+ calldata: swapCallData,
293
+ },
294
+ },
295
+ {
296
+ sanitizer: SIMPLE_SANITIZER,
297
+ call: {
298
+ contractAddress: usdceToken!.address,
299
+ selector: hash.getSelectorFromName("approve"),
300
+ calldata: [
301
+ this.config.extendedContract.toBigInt(),
302
+ toBigInt(amount.low.toString()), // amount low
303
+ toBigInt(amount.high.toString()), // amount high
304
+ ],
305
+ },
306
+ },
307
+ {
308
+ sanitizer: EXTENDED_SANITIZER,
309
+ call: {
310
+ contractAddress: this.config.extendedContract,
311
+ selector: hash.getSelectorFromName("deposit"),
312
+ calldata: [
313
+ BigInt(this.config.vaultIdExtended),
314
+ BigInt(params.amount.multipliedBy(10 ** usdceToken!.decimals).toFixed(0)),
315
+ BigInt(salt),
316
+ ],
317
+ },
318
+ },
319
+ ];
320
+ } catch (error) {
321
+ logger.error(`Error creating Deposit Call: ${error}`);
322
+ return [];
323
+ }
324
+ }
325
+
326
+ getProofsForFromLegacySwap<T>(tree: StandardMerkleTree): {
327
+ proofs: string[][];
328
+ callConstructor:
329
+ | GenerateCallFn<DepositParams>
330
+ | GenerateCallFn<WithdrawParams>;
331
+ } {
332
+ let proofGroups: string[][] = [];
333
+
334
+ const ids = this.getSwapFromLegacyLeaf().leaves.map((l) => l.readableId);
335
+ // console.log(`${this.name}::getProofs ids: ${ids}`);
336
+ for (const [i, v] of tree.entries()) {
337
+ // console.log(`${this.name}::getProofs v: ${v.readableId}`);
338
+ if (ids.includes(v.readableId)) {
339
+ //console.log(`${this.name}::getProofs found id: ${v.readableId}`);
340
+ proofGroups.push(tree.getProof(i));
341
+ }
342
+ }
343
+ if (proofGroups.length != ids.length) {
344
+ throw new Error(`Not all proofs found for IDs: ${ids.join(", ")}`);
345
+ }
346
+
347
+ // find leaf adapter
348
+ return {
349
+ proofs: proofGroups,
350
+ callConstructor: this.getSwapFromLegacyCall.bind(this),
351
+ };
352
+ }
353
+
354
+ async getSwapFromLegacyCall(params: DepositParams): Promise<ManageCall[]> {
355
+ try {
356
+ const usdcToken = this.config.supportedPositions[0].asset;
357
+ const usdceToken = Global.getDefaultTokens().find(
358
+ (token) => token.symbol === "USDCe"
359
+ );
360
+ // Give approval for more amount than the required amount
361
+ const amount = uint256.bnToUint256(
362
+ params.amount.multipliedBy(10).toWei()
363
+ );
364
+ const quotes = await this.config.avnuAdapter.getQuotesAvnu(
365
+ usdceToken!.address.toString(),
366
+ usdcToken!.address.toString(),
367
+ params.amount.toNumber(),
368
+ this.config.avnuAdapter.config.vaultAllocator.address.toString(),
369
+ usdcToken!.decimals,
370
+ false
371
+ );
372
+
373
+ if (!quotes) {
374
+ logger.error("error getting quotes from avnu");
375
+ return [];
376
+ }
377
+ const getCalldata = await this.config.avnuAdapter.getSwapCallData(
378
+ quotes!
379
+ );
380
+ const swapCallData = getCalldata[0];
381
+ //change extended sanitizer here
382
+ return [
383
+ {
384
+ sanitizer: AVNU_LEGACY_SANITIZER,
385
+ call: {
386
+ contractAddress: usdceToken!.address,
387
+ selector: hash.getSelectorFromName("approve"),
388
+ calldata: [
389
+ AVNU_EXCHANGE_FOR_LEGACY_USDC.toBigInt(),
390
+ toBigInt(amount.low.toString()), // amount low
391
+ toBigInt(amount.high.toString()), // amount high
392
+ ],
393
+ },
394
+ },
395
+ {
396
+ sanitizer: AVNU_LEGACY_SANITIZER,
397
+ call: {
398
+ contractAddress: AVNU_EXCHANGE_FOR_LEGACY_USDC,
399
+ selector: hash.getSelectorFromName("swap_to_new"),
400
+ calldata: swapCallData,
401
+ },
402
+ },
403
+ ];
404
+ } catch (error) {
405
+ logger.error(`Error creating Deposit Call: ${error}`);
406
+ return [];
407
+ }
408
+ }
409
+ //Swap wbtc to usdc
410
+ async getWithdrawCall(params: WithdrawParams): Promise<ManageCall[]> {
411
+ try {
412
+ if (!this.client) {
413
+ throw new Error("Client not initialized");
414
+ }
415
+ return [];
416
+ } catch (error) {
417
+ logger.error(`Error creating Withdraw Call: ${error}`);
418
+ return [];
419
+ }
420
+ }
421
+
422
+ async withdrawFromExtended(amount: Web3Number): Promise<boolean> {
423
+ try {
424
+ if (!this.client) {
425
+ logger.error("Client not initialized");
426
+ return false;
427
+ }
428
+ if (amount.lessThanOrEqualTo(0)) {
429
+ logger.error(
430
+ `Invalid withdrawal amount: ${amount.toNumber()}. Amount must be positive.`
431
+ );
432
+ return false;
433
+ }
434
+ if (amount.lessThanOrEqualTo(this.minimumExtendedMovementAmount)) {
435
+ logger.warn(
436
+ `Withdrawal amount ${amount.toNumber()} is below minimum Extended movement amount ${this.minimumExtendedMovementAmount}. Skipping withdrawal.`
437
+ );
438
+ return false;
439
+ }
440
+ const holdings = await this.getExtendedDepositAmount();
441
+ if (!holdings) {
442
+ logger.error(
443
+ "Cannot get holdings - unable to validate withdrawal amount"
444
+ );
445
+ return false;
446
+ }
447
+
448
+ const availableForWithdrawal = parseFloat(
449
+ holdings.availableForWithdrawal
450
+ );
451
+ if (
452
+ !Number.isFinite(availableForWithdrawal) ||
453
+ availableForWithdrawal < 0
454
+ ) {
455
+ logger.error(
456
+ `Invalid availableForWithdrawal: ${holdings.availableForWithdrawal}. Expected a finite, non-negative number.`
457
+ );
458
+ return false;
459
+ }
460
+
461
+ const withdrawalAmount = amount.toNumber();
462
+ if (withdrawalAmount > availableForWithdrawal) {
463
+ logger.error(
464
+ `Withdrawal amount ${withdrawalAmount} exceeds available balance ${availableForWithdrawal}`
465
+ );
466
+ return false;
467
+ }
468
+
469
+ logger.info(
470
+ `Withdrawing ${withdrawalAmount} from Extended. Available balance: ${availableForWithdrawal}`
471
+ );
472
+
473
+ const withdrawalRequest = await this.client.withdrawUSDC(
474
+ amount.toFixed(2)
475
+ );
476
+
477
+ if (withdrawalRequest.status === "OK") {
478
+ const withdrawalStatus = await this.getDepositOrWithdrawalStatus(
479
+ withdrawalRequest.data,
480
+ AssetOperationType.WITHDRAWAL
481
+ );
482
+ return withdrawalStatus;
483
+ }
484
+
485
+ logger.error(
486
+ `Withdrawal request failed with status: ${withdrawalRequest.status}`
487
+ );
488
+ return false;
489
+ } catch (error) {
490
+ logger.error(`Error creating Withdraw Call: ${error}`);
491
+ return false;
492
+ }
493
+ }
494
+
495
+ async getHealthFactor(): Promise<number> {
496
+ return Promise.resolve(1);
497
+ }
498
+
499
+ async getExtendedDepositAmount(): Promise<Balance | undefined> {
500
+ try {
501
+ if (this.client === null) {
502
+ logger.error("error initializing client - client is null");
503
+ return undefined; // Error: client not initialized
504
+ }
505
+
506
+ const result = await this.client.getHoldings();
507
+ if (!result) {
508
+ logger.error("error getting holdings - API returned null/undefined");
509
+ return undefined;
510
+ }
511
+
512
+ if (result.status && result.status !== "OK") {
513
+ logger.error(
514
+ `error getting holdings - API returned status: ${result.status}`
515
+ );
516
+ return undefined;
517
+ }
518
+
519
+ const holdings = result.data;
520
+ if (!holdings) {
521
+ logger.warn(
522
+ "holdings data is null/undefined - treating as zero balance"
523
+ );
524
+ return {
525
+ collateral_name: "",
526
+ balance: "0",
527
+ equity: "0",
528
+ availableForTrade: "0",
529
+ availableForWithdrawal: "0",
530
+ unrealisedPnl: "0",
531
+ initialMargin: "0",
532
+ marginRatio: "0",
533
+ updatedTime: Date.now(),
534
+ };
535
+ }
536
+ return holdings;
537
+ } catch (error) {
538
+ logger.error(`error getting holdings - exception: ${error}`);
539
+ return undefined;
540
+ }
541
+ }
542
+
543
+ async setLeverage(leverage: string, marketName: string): Promise<boolean> {
544
+ if (this.client === null) {
545
+ logger.error("error initializing client");
546
+ return false;
547
+ }
548
+ const res = await this.client.updateLeverage({
549
+ leverage: leverage,
550
+ market: marketName,
551
+ });
552
+ if (res.status === "OK") {
553
+ return true;
554
+ }
555
+ return false;
556
+ }
557
+
558
+ async getAllOpenPositions(): Promise<Position[] | null> {
559
+ if (this.client === null) {
560
+ logger.error("error initializing client");
561
+ return null;
562
+ }
563
+ const response = await this.client.getPositionsForMarket(
564
+ this.config.extendedMarketName
565
+ );
566
+ if (response.status === "OK") {
567
+ if (response.data.length === 0) {
568
+ return [];
569
+ } else {
570
+ return response.data;
571
+ }
572
+ }
573
+ return null;
574
+ }
575
+
576
+ async getOrderHistory(marketName: string): Promise<OpenOrder[] | null> {
577
+ if (this.client === null) {
578
+ logger.error("error initializing client");
579
+ return null;
580
+ }
581
+ const result = await this.client.getOrderHistory(marketName);
582
+ return result.data;
583
+ }
584
+
585
+ async getOrderStatus(
586
+ orderId: string,
587
+ marketName: string
588
+ ): Promise<OpenOrder | null> {
589
+ try {
590
+ if (this.client === null) {
591
+ logger.error("error initializing client");
592
+ return null;
593
+ }
594
+ const orderhistory = await this.getOrderHistory(marketName);
595
+
596
+ if (!orderhistory || orderhistory.length === 0) {
597
+ return null;
598
+ }
599
+ const order = orderhistory
600
+ .slice(0, 20)
601
+ .find((order) => order.id.toString() === orderId);
602
+
603
+ if (order) {
604
+ return order;
605
+ }
606
+
607
+ return null; // Order not found
608
+ } catch (error) {
609
+ logger.error(`error getting order status: ${error}`);
610
+ return null;
611
+ }
612
+ }
613
+
614
+ async fetchOrderBookBTCUSDC(): Promise<{
615
+ status: boolean;
616
+ bid: Web3Number;
617
+ ask: Web3Number;
618
+ }> {
619
+ try {
620
+ const res = await axios.get(
621
+ `${this.config.extendedBaseUrl}/api/v1/info/markets/${this.config.extendedMarketName}/orderbook`
622
+ );
623
+ let ask_price = new Web3Number(0, 0);
624
+ let bid_price = new Web3Number(0, 0);
625
+ if (res.data.status !== "OK") {
626
+ return {
627
+ status: false,
628
+ bid: bid_price,
629
+ ask: ask_price,
630
+ };
631
+ }
632
+ const data = res.data.data;
633
+ const bid = data.bid[0];
634
+ const ask = data.ask[0];
635
+ ask_price = new Web3Number(ask.price, 0);
636
+ bid_price = new Web3Number(bid.price, 0);
637
+
638
+ return {
639
+ status: false,
640
+ bid: bid_price, // check for quantity as well, not at the moment though
641
+ ask: ask_price, // check for quantity as well, not at the moment though
642
+ };
643
+ } catch (err) {
644
+ logger.error(`the err is: ${err}`);
645
+ return {
646
+ status: false,
647
+ bid: new Web3Number(0, 0),
648
+ ask: new Web3Number(0, 0),
649
+ };
650
+ }
651
+ }
652
+
653
+ async createOrder(
654
+ leverage: string,
655
+ btcAmount: number,
656
+ side: OrderSide,
657
+ attempt = 1,
658
+ maxAttempts = 5
659
+ ): Promise<{ position_id: string; btc_exposure: string } | null> {
660
+ try {
661
+ if (this.client === null) {
662
+ logger.error("error initializing client");
663
+ return null;
664
+ }
665
+ const setLeverage = await this.setLeverage(
666
+ leverage,
667
+ this.config.extendedMarketName
668
+ );
669
+ if (!setLeverage) {
670
+ logger.error("error depositing or setting leverage");
671
+ return null;
672
+ }
673
+ const { ask, bid } = await this.fetchOrderBookBTCUSDC();
674
+ if (
675
+ !ask ||
676
+ !bid ||
677
+ ask.lessThanOrEqualTo(0) ||
678
+ bid.lessThanOrEqualTo(0)
679
+ ) {
680
+ logger.error(
681
+ `Invalid orderbook prices: ask=${ask?.toNumber()}, bid=${bid?.toNumber()}`
682
+ );
683
+ return null;
684
+ }
685
+
686
+ const spread = ask.minus(bid);
687
+ const midPrice = ask.plus(bid).div(2);
688
+ /** Maximum deviation: 50% of spread (to prevent extremely unfavorable prices) */
689
+ const MAX_PRICE_DEVIATION_MULTIPLIER = 0.5;
690
+ const priceAdjustmentMultiplier = Math.min(
691
+ 0.2 * attempt,
692
+ MAX_PRICE_DEVIATION_MULTIPLIER
693
+ );
694
+ const priceAdjustment = spread.times(priceAdjustmentMultiplier);
695
+
696
+ let price = midPrice;
697
+ if (side === OrderSide.SELL) {
698
+ price = midPrice.minus(priceAdjustment);
699
+ } else {
700
+ price = midPrice.plus(priceAdjustment);
701
+ }
702
+
703
+ /** Validate price is still reasonable (within 50% of mid price) */
704
+ const maxDeviation = midPrice.times(0.5);
705
+ if (price.minus(midPrice).abs().greaterThan(maxDeviation)) {
706
+ logger.error(
707
+ `Price deviation too large on attempt ${attempt}: price=${price.toNumber()}, midPrice=${midPrice.toNumber()}, deviation=${price
708
+ .minus(midPrice)
709
+ .abs()
710
+ .toNumber()}`
711
+ );
712
+ if (attempt >= maxAttempts) {
713
+ return null;
714
+ }
715
+ price =
716
+ side === OrderSide.SELL
717
+ ? midPrice.minus(maxDeviation)
718
+ : midPrice.plus(maxDeviation);
719
+ }
720
+
721
+ logger.info(
722
+ `createOrder attempt ${attempt}/${maxAttempts}: side=${side}, midPrice=${midPrice.toNumber()}, adjustedPrice=${price.toNumber()}, adjustment=${
723
+ priceAdjustmentMultiplier * 100
724
+ }%`
725
+ );
726
+
727
+ const amount_in_token = (btcAmount * parseInt(leverage)).toFixed(
728
+ this.config.extendedPrecision
729
+ ); // gives the amount of wbtc
730
+
731
+ const result = await this.createExtendedPositon(
732
+ this.client,
733
+ this.config.extendedMarketName,
734
+ amount_in_token,
735
+ price.toFixed(0),
736
+ side
737
+ );
738
+ if (!result || !result.position_id) {
739
+ logger.error("Failed to create order - no position_id returned");
740
+ return null;
741
+ }
742
+
743
+ const positionId = result.position_id;
744
+ logger.info(
745
+ `Order created with position_id: ${positionId}. Waiting for API to update...`
746
+ );
747
+
748
+ let openOrder = await this.getOrderStatus(
749
+ positionId,
750
+ this.config.extendedMarketName
751
+ );
752
+ const maxStatusRetries = 3;
753
+ const statusRetryDelay = 5000;
754
+
755
+ if (!openOrder) {
756
+ logger.warn(
757
+ `Order ${positionId} not found in API yet. Retrying status fetch (max ${maxStatusRetries} times)...`
758
+ );
759
+ for (
760
+ let statusRetry = 1;
761
+ statusRetry <= maxStatusRetries;
762
+ statusRetry++
763
+ ) {
764
+ await new Promise((resolve) => setTimeout(resolve, statusRetryDelay));
765
+ openOrder = await this.getOrderStatus(
766
+ positionId,
767
+ this.config.extendedMarketName
768
+ );
769
+ if (openOrder) {
770
+ logger.info(
771
+ `Order ${positionId} found after ${statusRetry} status retry(ies)`
772
+ );
773
+ break;
774
+ }
775
+ logger.warn(
776
+ `Order ${positionId} still not found after ${statusRetry}/${maxStatusRetries} status retries`
777
+ );
778
+ }
779
+ }
780
+
781
+ if (openOrder && openOrder.status === OrderStatus.FILLED) {
782
+ logger.info(
783
+ `Order ${positionId} successfully filled with quantity ${openOrder.qty}`
784
+ );
785
+ return {
786
+ position_id: positionId,
787
+ btc_exposure: openOrder.qty,
788
+ };
789
+ } else if (openOrder && openOrder.status !== OrderStatus.FILLED) {
790
+ // Order found but NOT FILLED - retry (recreate)
791
+ logger.warn(
792
+ `Order ${positionId} found but status is ${openOrder.status}, not FILLED. Retrying order creation...`
793
+ );
794
+ if (attempt >= maxAttempts) {
795
+ logger.error(
796
+ `Max retries reached — order ${positionId} status is ${openOrder.status}, not FILLED`
797
+ );
798
+ return null;
799
+ } else {
800
+ const backoff = 2000 * attempt;
801
+ await new Promise((resolve) => setTimeout(resolve, backoff));
802
+ return this.createOrder(
803
+ leverage,
804
+ btcAmount,
805
+ side,
806
+ attempt + 1,
807
+ maxAttempts
808
+ );
809
+ }
810
+ } else {
811
+ logger.warn(
812
+ `Order ${positionId} not found in API after ${maxStatusRetries} status retries (API update delayed ~30s). We got position_id from creation, so order exists. Returning position_id - status will be checked in next loop iteration.`
813
+ );
814
+ return {
815
+ position_id: positionId,
816
+ btc_exposure: amount_in_token,
817
+ };
818
+ }
819
+ } catch (err: any) {
820
+ logger.error(
821
+ `createShortOrder failed on attempt ${attempt}: ${err.message}`
822
+ );
823
+
824
+ if (attempt < maxAttempts) {
825
+ const backoff = 1200 * attempt;
826
+ logger.info(`Retrying after ${backoff}ms...`);
827
+ await new Promise((resolve) => setTimeout(resolve, backoff));
828
+ return this.createOrder(
829
+ leverage,
830
+ btcAmount,
831
+ side,
832
+ attempt + 1,
833
+ maxAttempts
834
+ );
835
+ }
836
+ logger.error("Max retry attempts reached — aborting createShortOrder");
837
+ return null;
838
+ }
839
+ }
840
+
841
+ async createExtendedPositon(
842
+ client: ExtendedWrapper,
843
+ marketName: string,
844
+ amount: string,
845
+ price: string,
846
+ side: OrderSide
847
+ ) {
848
+ try {
849
+ const result =
850
+ side === OrderSide.SELL
851
+ ? await client.createSellOrder(marketName, amount, price, {
852
+ postOnly: false,
853
+ timeInForce: TimeInForce.IOC,
854
+ })
855
+ : await client.createBuyOrder(marketName, amount, price, {
856
+ postOnly: false,
857
+ timeInForce: TimeInForce.IOC,
858
+ });
859
+ if (result.data.id) {
860
+ const position_id = result.data.id.toString();
861
+ return {
862
+ position_id,
863
+ };
864
+ }
865
+ return null;
866
+ } catch (err) {
867
+ logger.error(`Error opening short extended position, ${err}`);
868
+ return null;
869
+ }
870
+ }
871
+
872
+ async getDepositOrWithdrawalStatus(
873
+ orderId: number | string, // for deposits, send txn hash as string
874
+ operationsType: AssetOperationType
875
+ ): Promise<boolean> {
876
+ const maxAttempts = 15;
877
+ const retryDelayMs = 30000; // 60 seconds
878
+
879
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
880
+ try {
881
+ let transferHistory = await this.client.getAssetOperations({
882
+ operationsType: [operationsType],
883
+ operationsStatus: [AssetOperationStatus.COMPLETED],
884
+ });
885
+ if (operationsType === AssetOperationType.DEPOSIT) {
886
+ const myTransferStatus = transferHistory.data.find(
887
+ (operation) => operation.transactionHash?.toLowerCase() === orderId.toString().toLowerCase()
888
+ );
889
+ if (!myTransferStatus) {
890
+ if (attempt < maxAttempts) {
891
+ logger.info(
892
+ `Deposit operation not found for transactionHash ${orderId}, retrying (attempt ${attempt}/${maxAttempts})...`
893
+ );
894
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
895
+ continue;
896
+ }
897
+ logger.warn(
898
+ `Deposit operation not found for transactionHash ${orderId} after ${maxAttempts} attempts`
899
+ );
900
+ return false;
901
+ }
902
+ // Check if status is COMPLETED
903
+ if (myTransferStatus.status === AssetOperationStatus.COMPLETED) {
904
+ logger.info(
905
+ `Deposit operation ${orderId} completed successfully`
906
+ );
907
+ return true;
908
+ } else {
909
+ if (attempt < maxAttempts) {
910
+ logger.info(
911
+ `Deposit operation ${orderId} found but status is ${myTransferStatus.status}, not COMPLETED. Retrying (attempt ${attempt}/${maxAttempts})...`
912
+ );
913
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
914
+ continue;
915
+ }
916
+ logger.warn(
917
+ `Deposit operation ${orderId} status is ${myTransferStatus.status} after ${maxAttempts} attempts, expected COMPLETED`
918
+ );
919
+ return false;
920
+ }
921
+ } else {
922
+ const myTransferStatus = transferHistory.data.find(
923
+ (operation) => operation.id === orderId.toString()
924
+ );
925
+ if (!myTransferStatus) {
926
+ if (attempt < maxAttempts) {
927
+ logger.info(
928
+ `Withdrawal status not found for orderId ${orderId} in completed operations, retrying (attempt ${attempt}/${maxAttempts})...`
929
+ );
930
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
931
+ continue;
932
+ }
933
+ logger.warn(
934
+ `Withdrawal operation not found for orderId ${orderId} after ${maxAttempts} attempts`
935
+ );
936
+ return false;
937
+ }
938
+ // Check if status is COMPLETED
939
+ if (myTransferStatus.status === AssetOperationStatus.COMPLETED) {
940
+ logger.info(
941
+ `Withdrawal operation ${orderId} completed successfully`
942
+ );
943
+ return true;
944
+ } else {
945
+ if (attempt < maxAttempts) {
946
+ logger.info(
947
+ `Withdrawal operation ${orderId} found but status is ${myTransferStatus.status}, not COMPLETED. Retrying (attempt ${attempt}/${maxAttempts})...`
948
+ );
949
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
950
+ continue;
951
+ }
952
+ logger.warn(
953
+ `Withdrawal operation ${orderId} status is ${myTransferStatus.status} after ${maxAttempts} attempts, expected COMPLETED`
954
+ );
955
+ return false;
956
+ }
957
+ }
958
+ } catch (err) {
959
+ logger.error(
960
+ `error getting deposit or withdrawal status (attempt ${attempt}/${maxAttempts}): ${err}`
961
+ );
962
+ if (attempt < maxAttempts) {
963
+ logger.info(`Retrying after ${retryDelayMs}ms...`);
964
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
965
+ continue;
966
+ }
967
+ logger.error(
968
+ `Max retry attempts reached for getDepositOrWithdrawalStatus`
969
+ );
970
+ return false;
971
+ }
972
+ }
973
+
974
+ return false;
975
+ }
976
+ }