@strkfarm/sdk 2.0.0-dev.4 → 2.0.0-dev.40

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 (77) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +116018 -90768
  4. package/dist/index.browser.mjs +12769 -10876
  5. package/dist/index.d.ts +2222 -1947
  6. package/dist/index.js +13171 -11077
  7. package/dist/index.mjs +13076 -11004
  8. package/package.json +3 -3
  9. package/src/data/avnu.abi.json +840 -0
  10. package/src/data/ekubo-price-fethcer.abi.json +265 -0
  11. package/src/data/redeem-request-nft.abi.json +752 -0
  12. package/src/data/universal-vault.abi.json +8 -7
  13. package/src/dataTypes/_bignumber.ts +13 -4
  14. package/src/dataTypes/bignumber.browser.ts +10 -1
  15. package/src/dataTypes/bignumber.node.ts +10 -1
  16. package/src/dataTypes/index.ts +3 -2
  17. package/src/dataTypes/mynumber.ts +141 -0
  18. package/src/global.ts +96 -41
  19. package/src/index.browser.ts +2 -1
  20. package/src/interfaces/common.tsx +212 -5
  21. package/src/modules/apollo-client-config.ts +28 -0
  22. package/src/modules/avnu.ts +21 -12
  23. package/src/modules/ekubo-pricer.ts +79 -0
  24. package/src/modules/ekubo-quoter.ts +48 -30
  25. package/src/modules/erc20.ts +17 -0
  26. package/src/modules/harvests.ts +43 -29
  27. package/src/modules/index.ts +1 -1
  28. package/src/modules/pragma.ts +23 -8
  29. package/src/modules/pricer-from-api.ts +156 -15
  30. package/src/modules/pricer-lst.ts +1 -1
  31. package/src/modules/pricer.ts +40 -4
  32. package/src/modules/pricerBase.ts +2 -1
  33. package/src/node/deployer.ts +36 -1
  34. package/src/node/pricer-redis.ts +2 -1
  35. package/src/strategies/base-strategy.ts +168 -16
  36. package/src/strategies/constants.ts +8 -3
  37. package/src/strategies/ekubo-cl-vault.tsx +1044 -351
  38. package/src/strategies/factory.ts +199 -0
  39. package/src/strategies/index.ts +5 -3
  40. package/src/strategies/registry.ts +262 -0
  41. package/src/strategies/sensei.ts +353 -9
  42. package/src/strategies/svk-strategy.ts +125 -30
  43. package/src/strategies/token-boosted-xstrk-carry-strategy.tsx +1225 -0
  44. package/src/strategies/types.ts +4 -0
  45. package/src/strategies/universal-adapters/adapter-utils.ts +4 -1
  46. package/src/strategies/universal-adapters/avnu-adapter.ts +196 -272
  47. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  48. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  49. package/src/strategies/universal-adapters/index.ts +10 -8
  50. package/src/strategies/universal-adapters/svk-troves-adapter.ts +511 -0
  51. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  52. package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
  53. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +525 -0
  54. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1098 -712
  55. package/src/strategies/universal-adapters/vesu-position-common.ts +258 -0
  56. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  57. package/src/strategies/universal-lst-muliplier-strategy.tsx +551 -405
  58. package/src/strategies/universal-strategy.tsx +1487 -1173
  59. package/src/strategies/vesu-rebalance.tsx +252 -152
  60. package/src/strategies/yoloVault.ts +1084 -0
  61. package/src/utils/cacheClass.ts +11 -2
  62. package/src/utils/health-factor-math.ts +33 -1
  63. package/src/utils/index.ts +3 -1
  64. package/src/utils/logger.browser.ts +22 -4
  65. package/src/utils/logger.node.ts +259 -24
  66. package/src/utils/starknet-call-parser.ts +1036 -0
  67. package/src/utils/strategy-utils.ts +61 -0
  68. package/src/modules/ExtendedWrapperSDk/index.ts +0 -62
  69. package/src/modules/ExtendedWrapperSDk/types.ts +0 -311
  70. package/src/modules/ExtendedWrapperSDk/wrapper.ts +0 -395
  71. package/src/strategies/universal-adapters/extended-adapter.ts +0 -661
  72. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
  73. package/src/strategies/vesu-extended-strategy/services/operationService.ts +0 -34
  74. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +0 -77
  75. package/src/strategies/vesu-extended-strategy/utils/constants.ts +0 -49
  76. package/src/strategies/vesu-extended-strategy/utils/helper.ts +0 -372
  77. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +0 -1140
@@ -1,7 +1,13 @@
1
- import { getNoRiskTags, highlightTextWithLinks, IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType, TokenInfo } from "@/interfaces";
2
- import { BaseStrategy, SingleActionAmount, SingleTokenInfo } from "./base-strategy";
1
+ import { getNoRiskTags, highlightTextWithLinks, IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType, StrategyTag, TokenInfo, AuditStatus, SourceCodeType, AccessControlType, InstantWithdrawalVault, StrategyLiveStatus, VaultType, UnwrapLabsCurator } from "@/interfaces";
2
+ import {
3
+ BaseStrategy,
4
+ SingleActionAmount,
5
+ SingleTokenInfo,
6
+ UserPositionCard,
7
+ UserPositionCardsInput,
8
+ } from "./base-strategy";
3
9
  import { ContractAddr, Web3Number } from "@/dataTypes";
4
- import { Call, Contract, uint256 } from "starknet";
10
+ import { Call, Contract, num, uint256, BlockIdentifier } from "starknet";
5
11
  import SenseiABI from "@/data/sensei.abi.json";
6
12
  import { getTrovesEndpoint, logger } from "@/utils";
7
13
  import { Global } from "@/global";
@@ -9,6 +15,10 @@ import { QuoteRequest } from "@avnu/avnu-sdk";
9
15
  import { PricerBase } from "@/modules/pricerBase";
10
16
  import ERC20ABI from "@/data/erc20.abi.json";
11
17
  import { AvnuWrapper } from "@/modules";
18
+ import { gql } from "@apollo/client";
19
+ import apolloClient from "@/modules/apollo-client";
20
+ import { VesuAdapter, VesuPools } from "./universal-adapters/vesu-adapter";
21
+ import { LSTAPRService } from "@/modules/lst-apr";
12
22
 
13
23
  export interface SenseiVaultSettings {
14
24
  mainToken: TokenInfo;
@@ -37,17 +47,27 @@ export class SenseiVault extends BaseStrategy<
37
47
  }
38
48
  }
39
49
 
40
- async getUserTVL(user: ContractAddr): Promise<SingleTokenInfo> {
50
+ async getUserTVL(user: ContractAddr, blockIdentifier: BlockIdentifier = "latest"): Promise<SingleTokenInfo> {
41
51
  const result: any = await this.contract.call(
42
52
  "describe_position",
43
53
  [user.address],
54
+ {
55
+ blockIdentifier,
56
+ }
44
57
  );
45
58
  const amount = Web3Number.fromWei(
46
59
  uint256.uint256ToBN(result[1].estimated_size).toString(),
47
60
  this.metadata.depositTokens[0].decimals,
48
61
  )
62
+
63
+ // Convert blockIdentifier to block number for pricer if it's a number
64
+ const blockNumber = typeof blockIdentifier === 'number' || typeof blockIdentifier === 'bigint'
65
+ ? Number(blockIdentifier)
66
+ : undefined;
67
+
49
68
  const price = await this.pricer.getPrice(
50
69
  this.metadata.depositTokens[0].symbol,
70
+ blockNumber
51
71
  );
52
72
  return {
53
73
  usdValue: Number(amount.toFixed(6)) * price.price,
@@ -80,7 +100,7 @@ export class SenseiVault extends BaseStrategy<
80
100
  tokenInfo: this.metadata.depositTokens[0],
81
101
  };
82
102
  } catch (error) {
83
- console.error('Error fetching TVL:', error);
103
+ console.error(`[SDK] Error fetching TVL for ${this.metadata.id}:`, error);
84
104
  return {
85
105
  usdValue: 0,
86
106
  amount: new Web3Number('0', this.metadata.depositTokens[0].decimals),
@@ -223,7 +243,272 @@ export class SenseiVault extends BaseStrategy<
223
243
  logger.verbose('getSettings', settings);
224
244
  return settings;
225
245
  };
246
+
247
+ /**
248
+ * Calculate lifetime earnings for a user
249
+ * Not yet implemented for Sensei Vault strategy
250
+ */
251
+ getLifetimeEarnings(
252
+ userTVL: SingleTokenInfo,
253
+ investmentFlows: Array<{ amount: string; type: string; timestamp: number; tx_hash: string }>
254
+ ): any {
255
+ throw new Error("getLifetimeEarnings is not implemented yet for this strategy");
256
+ }
257
+
258
+ async netAPY(): Promise<number> {
259
+ try {
260
+ // Fetch Vesu pools and select the Re7 xSTRK pool
261
+ const { pools } = await VesuAdapter.getVesuPools();
262
+ const re7PoolId = VesuPools.Re7xSTRK;
263
+ const pool = pools.find((p: any) =>
264
+ ContractAddr.from(num.getHexString(p.id)).eq(re7PoolId),
265
+ );
266
+
267
+ if (!pool) {
268
+ logger.warn(`${SenseiVault.name}::netAPY - Re7 xSTRK pool not found`);
269
+ return 0;
270
+ }
271
+
272
+ const mainSymbol = this.metadata.additionalInfo.mainToken.symbol.toLowerCase(); // STRK
273
+ const secondarySymbol =
274
+ this.metadata.additionalInfo.secondaryToken.symbol.toLowerCase(); // xSTRK
275
+
276
+ const collateralAssetStats = pool.assets.find(
277
+ (a: any) => String(a.symbol).toLowerCase() === secondarySymbol,
278
+ )?.stats;
279
+ const debtAssetStats = pool.assets.find(
280
+ (a: any) => String(a.symbol).toLowerCase() === mainSymbol,
281
+ )?.stats;
282
+
283
+ if (!collateralAssetStats || !debtAssetStats) {
284
+ logger.warn(
285
+ `${SenseiVault.name}::netAPY - Missing collateral/debt stats on Vesu pool`,
286
+ );
287
+ return 0;
288
+ }
289
+
290
+ // Base xSTRK lending APY from Vesu
291
+ const xstrkSupplyAPY =
292
+ Number(collateralAssetStats.supplyApy?.value || 0) / 1e18;
293
+
294
+ // STRK rewards APR on xSTRK collateral from Vesu
295
+ const strkRewardsAPR = collateralAssetStats.defiSpringSupplyApr
296
+ ? Number(collateralAssetStats.defiSpringSupplyApr.value || 0) / 1e18
297
+ : 0;
298
+
299
+ // STRK borrow APY from Vesu
300
+ const borrowAPY =
301
+ Number(debtAssetStats.borrowApr?.value || 0) / 1e18;
302
+
303
+ // LST APR for xSTRK from Endur (based on underlying STRK asset)
304
+ const lstAPY = await LSTAPRService.getLSTAPR(
305
+ this.metadata.additionalInfo.mainToken.address,
306
+ );
307
+
308
+ // Collateral APY = Vesu xSTRK supply + Endur xSTRK APR + STRK rewards
309
+ const collateralAPY = xstrkSupplyAPY + lstAPY + strkRewardsAPR;
310
+
311
+ const feeFactor = this.metadata.additionalInfo.feeBps / 10000; // convert bps to decimal
312
+ const feeAdjustedColAPY =
313
+ collateralAPY - strkRewardsAPR * feeFactor;
314
+
315
+ // Position info (collateral & debt in USD terms)
316
+ const { collateralUSDValue, debtUSDValue } =
317
+ await this.getPositionInfo();
318
+
319
+ const collateralUSD = Number(collateralUSDValue.toFixed(6));
320
+ const debtUSD = Number(debtUSDValue.toFixed(6));
321
+
322
+ // Compute expected leverage using the same math as app-side strategy
323
+ const targetHf = this.metadata.additionalInfo.targetHfBps / 10000;
324
+ const xSTRKPrice = await this.getSecondaryTokenPriceRelativeToMain();
325
+ const denominator = targetHf * xSTRKPrice - 0.87;
326
+ if (denominator <= 0) {
327
+ logger.warn(
328
+ `${SenseiVault.name}::netAPY - Invalid denominator in leverage calc`,
329
+ );
330
+ return 0;
331
+ }
332
+ const borrowedSTRK = (0.87 * xSTRKPrice) / denominator;
333
+ const expectedLeverage = 1 + borrowedSTRK;
334
+ if (!Number.isFinite(expectedLeverage) || expectedLeverage <= 0) {
335
+ logger.warn(
336
+ `${SenseiVault.name}::netAPY - Non-positive or invalid expectedLeverage`,
337
+ );
338
+ return 0;
339
+ }
340
+
341
+ const payoff =
342
+ collateralUSD * feeAdjustedColAPY - debtUSD * borrowAPY;
343
+ const investment = collateralUSD - debtUSD;
344
+
345
+ if (investment === 0) {
346
+ return 0;
347
+ }
348
+
349
+ const netAPY = payoff / investment;
350
+ return Number.isFinite(netAPY) ? netAPY : 0;
351
+ } catch (error) {
352
+ logger.error(`${SenseiVault.name}::netAPY - Error`, error);
353
+ return 0;
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Calculates user realized APY based on position growth accounting for deposits and withdrawals.
359
+ * Returns the APY as a number.
360
+ * Not implemented for Sensei Strategy yet.
361
+ */
362
+ async getUserRealizedAPY(
363
+ blockIdentifier: BlockIdentifier = "latest",
364
+ sinceBlocks = 600000
365
+ ): Promise<number> {
366
+ throw new Error("getUserRealizedAPY not implemented yet for Sensei strategy");
226
367
 
368
+ /*
369
+ logger.verbose(
370
+ `${SenseiVault.name}: getUserRealizedAPY => starting with userAddress=${userAddress.address}, blockIdentifier=${blockIdentifier}, sinceBlocks=${sinceBlocks}`
371
+ );
372
+
373
+ // Determine current block number
374
+ let blockNow =
375
+ typeof blockIdentifier === "number" || typeof blockIdentifier === "bigint"
376
+ ? Number(blockIdentifier)
377
+ : (await this.config.provider.getBlockLatestAccepted()).block_number;
378
+
379
+ // Look back window, but never before launch block
380
+ const blockBefore = Math.max(
381
+ blockNow - sinceBlocks,
382
+ this.metadata.launchBlock
383
+ );
384
+
385
+ logger.verbose(
386
+ `${SenseiVault.name}: getUserRealizedAPY => blockNow=${blockNow}, blockBefore=${blockBefore}`
387
+ );
388
+
389
+ // Get current estimated size
390
+ const currentResult: any = await this.contract.call(
391
+ "describe_position",
392
+ [userAddress.address],
393
+ {
394
+ blockIdentifier,
395
+ }
396
+ );
397
+ const currentEstimatedSize = Web3Number.fromWei(
398
+ uint256.uint256ToBN(currentResult[1].estimated_size).toString(),
399
+ this.metadata.depositTokens[0].decimals,
400
+ );
401
+
402
+ // Get previous estimated size
403
+ const previousResult: any = await this.contract.call(
404
+ "describe_position",
405
+ [userAddress.address],
406
+ {
407
+ blockIdentifier: blockBefore,
408
+ }
409
+ );
410
+ const previousEstimatedSize = Web3Number.fromWei(
411
+ uint256.uint256ToBN(previousResult[1].estimated_size).toString(),
412
+ this.metadata.depositTokens[0].decimals,
413
+ );
414
+
415
+ logger.verbose(
416
+ `${SenseiVault.name}: getUserRealizedAPY => currentEstimatedSize=${currentEstimatedSize.toString()}, previousEstimatedSize=${previousEstimatedSize.toString()}`
417
+ );
418
+
419
+ // Query GraphQL for deposits and withdrawals between blockBefore and blockNow
420
+ let newDeposits = Web3Number.fromWei("0", this.metadata.depositTokens[0].decimals);
421
+ let newWithdrawals = Web3Number.fromWei("0", this.metadata.depositTokens[0].decimals);
422
+
423
+ try {
424
+ const { data } = await apolloClient.query({
425
+ query: gql`
426
+ query Query($where: Investment_flowsWhereInput) {
427
+ findManyInvestment_flows(where: $where) {
428
+ block_number
429
+ amount
430
+ type
431
+ }
432
+ }
433
+ `,
434
+ variables: {
435
+ where: {
436
+ contract: {
437
+ equals: this.address.address.toLowerCase(),
438
+ },
439
+ owner: {
440
+ equals: userAddress.address.toLowerCase(),
441
+ },
442
+ block_number: {
443
+ gte: blockBefore,
444
+ lte: blockNow,
445
+ },
446
+ },
447
+ },
448
+ fetchPolicy: 'no-cache',
449
+ });
450
+
451
+ // Sum deposits and withdrawals
452
+ for (const flow of data.findManyInvestment_flows) {
453
+ const amount = Web3Number.fromWei(
454
+ flow.amount,
455
+ this.metadata.depositTokens[0].decimals
456
+ );
457
+ if (flow.type === 'deposit') {
458
+ newDeposits = newDeposits.plus(amount);
459
+ } else if (flow.type === 'withdraw') {
460
+ newWithdrawals = newWithdrawals.plus(amount);
461
+ }
462
+ }
463
+
464
+ logger.verbose(
465
+ `${SenseiVault.name}: getUserRealizedAPY => newDeposits=${newDeposits.toString()}, newWithdrawals=${newWithdrawals.toString()}`
466
+ );
467
+ } catch (error) {
468
+ logger.verbose(
469
+ `${SenseiVault.name}: getUserRealizedAPY => Error querying GraphQL, continuing with zero deposits/withdrawals:`,
470
+ error
471
+ );
472
+ // Continue with zero deposits/withdrawals if GraphQL query fails
473
+ }
474
+
475
+ // Calculate growth: Current estimated size - new deposits + new withdrawals - previous estimated size
476
+ const growth = currentEstimatedSize
477
+ .minus(newDeposits)
478
+ .plus(newWithdrawals)
479
+ .minus(previousEstimatedSize);
480
+
481
+ logger.verbose(
482
+ `${SenseiVault.name}: getUserRealizedAPY => growth=${growth.toString()}`
483
+ );
484
+
485
+ // Handle edge case where previous position is zero
486
+ if (previousEstimatedSize.isZero() || previousEstimatedSize.lte(0)) {
487
+ logger.verbose(
488
+ `${SenseiVault.name}: getUserRealizedAPY => Previous position is zero, returning 0`
489
+ );
490
+ return 0;
491
+ }
492
+
493
+ // Calculate actual block difference (in case limited by launch block)
494
+ const actualBlockDiff = blockNow - blockBefore;
495
+
496
+ // Calculate APY: 100 * 365/n * growth / previousEstimatedSize
497
+ // where n is the number of blocks (sinceBlocks or actual block difference)
498
+ const growthRatio = growth.dividedBy(previousEstimatedSize);
499
+ const apy = Number(growthRatio) * 100 * 365 / actualBlockDiff;
500
+
501
+ logger.verbose(
502
+ `${SenseiVault.name}: getUserRealizedAPY => actualBlockDiff=${actualBlockDiff}, growthRatio=${Number(growthRatio)}, apy=${apy}`
503
+ );
504
+
505
+ return apy;
506
+ */
507
+ }
508
+
509
+ async getUserPositionCards(_input: UserPositionCardsInput): Promise<UserPositionCard[]> {
510
+ return [];
511
+ }
227
512
  }
228
513
 
229
514
  const senseiDescription = `Deposit your {{token1}} to automatically loop your funds via Endur ({{token2}}) and Vesu to create a delta neutral position. This strategy is designed to maximize your yield on {{token1}}. Your position is automatically adjusted periodically to maintain a healthy health factor. You receive a NFT as representation for your stake on Troves. You can withdraw anytime by redeeming your NFT for {{token1}}.`;
@@ -289,9 +574,10 @@ const FAQS = [
289
574
  export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
290
575
  [
291
576
  {
577
+ id: "xstrk_sensei",
292
578
  name: "xSTRK Sensei",
293
579
  description: highlightTextWithLinks(
294
- senseiDescription.replaceAll('{{token1}}', 'STRK').replaceAll('{{token2}}', 'xSTRK'),
580
+ senseiDescription.replace('{{token1}}', 'STRK').replace('{{token2}}', 'xSTRK'),
295
581
  [{
296
582
  highlight: "Endur",
297
583
  link: "https://endur.fi"
@@ -308,11 +594,33 @@ export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
308
594
  ),
309
595
  launchBlock: 1053811,
310
596
  type: "Other",
597
+ curator: UnwrapLabsCurator,
598
+ vaultType: {
599
+ type: VaultType.LOOPING,
600
+ description: "Creates leveraged looping position on xSTRK by borrowing STRK to increase yield"
601
+ },
311
602
  depositTokens: [
312
603
  Global.getDefaultTokens().find((t) => t.symbol === "STRK")!
313
604
  ],
314
605
  protocols: [endurProtocol, vesuProtocol],
315
- maxTVL: new Web3Number("1500000", 18),
606
+ settings: {
607
+ alerts: [
608
+ {
609
+ type: "info",
610
+ text: "Depeg-risk: If xSTRK price on DEXes deviates from expected price, you may lose money or may have to wait for the price to recover.",
611
+ tab: "all"
612
+ }
613
+ ],
614
+ liveStatus: StrategyLiveStatus.RETIRED,
615
+ isPaused: false,
616
+ isInMaintenance: false,
617
+ isAudited: false,
618
+ isInstantWithdrawal: true,
619
+ isTransactionHistDisabled: true,
620
+ quoteToken: Global.getDefaultTokens().find(
621
+ (t) => t.symbol === "STRK"
622
+ )!
623
+ },
316
624
  risk: {
317
625
  riskFactor: _riskFactor,
318
626
  netRisk:
@@ -335,6 +643,42 @@ export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
335
643
  "Buy more xSTRK with borrowed STRK",
336
644
  "Repeat the process to loop your position",
337
645
  "Claim DeFi spring (STRK) rewards weekly and reinvest",
338
- ]
646
+ ],
647
+ tags: [StrategyTag.LEVERED],
648
+ security: {
649
+ auditStatus: AuditStatus.AUDITED,
650
+ sourceCode: {
651
+ type: SourceCodeType.CLOSED_SOURCE,
652
+ contractLink: "https://github.com/trovesfi/troves-contracts",
653
+ },
654
+ accessControl: {
655
+ type: AccessControlType.ROLE_BASED_ACCESS,
656
+ addresses: [ContractAddr.from("0x0")],
657
+ timeLock: "2 Days",
658
+ },
659
+ },
660
+ redemptionInfo: {
661
+ instantWithdrawalVault: InstantWithdrawalVault.YES,
662
+ redemptionsInfo: [],
663
+ alerts: [],
664
+ },
665
+ usualTimeToEarnings: "2 weeks",
666
+ usualTimeToEarningsDescription: "Strategy returns depend on LST price on DEXes. Even though the true price of LST on Endur increases continuously, the DEX price may lag sometimes, and historically is seen to rebase at least once every 2 weeks. This is when you realise your earnings.",
667
+ points: [{
668
+ multiplier: 4,
669
+ logo: 'https://endur.fi/favicon.ico',
670
+ toolTip: "This strategy holds xSTRK. Earn 3-4x Endur points on your xSTRK due to the leverage. Points can be found on endur.fi.",
671
+ }],
672
+ discontinuationInfo: {
673
+ info: highlightTextWithLinks(
674
+ "This strategy is retired. All funds have been moved to Hyper xSTRK.",
675
+ [
676
+ {
677
+ highlight: "Hyper xSTRK",
678
+ link: "/strategy/hyper_xstrk",
679
+ },
680
+ ]
681
+ ),
682
+ },
339
683
  },
340
- ];
684
+ ];
@@ -1,9 +1,9 @@
1
1
  import { ContractAddr, Web3Number } from "@/dataTypes";
2
- import { IConfig, IStrategyMetadata, TokenInfo, VaultPosition } from "@/interfaces";
2
+ import { IConfig, IStrategyMetadata, Protocols, TokenInfo, VaultPosition } from "@/interfaces";
3
3
  import { PricerBase } from "@/modules/pricerBase";
4
4
  import { ERC20 } from "@/modules";
5
5
  import { BaseStrategy, SingleActionAmount, SingleTokenInfo } from "./base-strategy";
6
- import { Call, Contract, num } from "starknet";
6
+ import { Call, Contract, num, uint256 } from "starknet";
7
7
  import { assert, LeafData, logger, StandardMerkleTree } from "@/utils";
8
8
  import { UniversalStrategySettings } from "./universal-strategy";
9
9
  import { UNIVERSAL_MANAGE_IDS } from "./universal-strategy";
@@ -15,9 +15,9 @@ import { ManageCall, PositionInfo } from "./universal-adapters";
15
15
  * Base class for all SVK (Starknet Vault Kit) strategies.
16
16
  * Contains common functions that are shared across all SVK strategies.
17
17
  */
18
- export abstract class SVKStrategy<S extends UniversalStrategySettings>
18
+ export abstract class SVKStrategy<S extends UniversalStrategySettings>
19
19
  extends BaseStrategy<SingleTokenInfo, SingleActionAmount> {
20
-
20
+
21
21
  /** Contract address of the strategy */
22
22
  readonly address: ContractAddr;
23
23
  /** Pricer instance for token price calculations */
@@ -36,13 +36,13 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
36
36
  this.pricer = pricer;
37
37
  this.metadata = metadata;
38
38
  this.address = metadata.address;
39
-
39
+
40
40
  this.contract = new Contract({
41
41
  abi: UniversalVaultAbi,
42
42
  address: this.address.address,
43
43
  providerOrAccount: this.config.provider
44
44
  });
45
-
45
+
46
46
  this.managerContract = new Contract({
47
47
  abi: ManagerAbi,
48
48
  address: this.metadata.additionalInfo.manager.address,
@@ -57,14 +57,51 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
57
57
  return this.metadata.depositTokens[0];
58
58
  }
59
59
 
60
+ async depositCall(amountInfo: SingleActionAmount, receiver: ContractAddr): Promise<Call[]> {
61
+ // Technically its not erc4626 abi, but we just need approve call
62
+ // so, its ok to use it
63
+ assert(
64
+ amountInfo.tokenInfo.address.eq(this.asset().address),
65
+ "Deposit token mismatch"
66
+ );
67
+ const assetContract = new Contract({
68
+ abi: UniversalVaultAbi,
69
+ address: this.asset().address.address,
70
+ providerOrAccount: this.config.provider
71
+ });
72
+ const call1 = assetContract.populate("approve", [
73
+ this.address.address,
74
+ uint256.bnToUint256(amountInfo.amount.toWei())
75
+ ]);
76
+ const call2 = this.contract.populate("deposit", [
77
+ uint256.bnToUint256(amountInfo.amount.toWei()),
78
+ receiver.address
79
+ ]);
80
+ return [call1, call2];
81
+ }
82
+
83
+ async withdrawCall(amountInfo: SingleActionAmount, receiver: ContractAddr, owner: ContractAddr): Promise<Call[]> {
84
+ assert(
85
+ amountInfo.tokenInfo.address.eq(this.asset().address),
86
+ "Withdraw token mismatch"
87
+ );
88
+ const shares = await this.contract.call('convert_to_shares', [uint256.bnToUint256(amountInfo.amount.toWei())]);
89
+ const call = this.contract.populate("request_redeem", [
90
+ uint256.bnToUint256(shares.toString()),
91
+ receiver.address,
92
+ owner.address
93
+ ]);
94
+ return [call];
95
+ }
96
+
60
97
  /**
61
98
  * Returns the unused balance in the vault allocator.
62
99
  * Note: This function is common for any SVK strategy.
63
100
  */
64
101
  async getUnusedBalance(): Promise<SingleTokenInfo> {
65
102
  const balance = await (new ERC20(this.config)).balanceOf(
66
- this.asset().address,
67
- this.metadata.additionalInfo.vaultAllocator,
103
+ this.asset().address,
104
+ this.metadata.additionalInfo.vaultAllocator,
68
105
  this.asset().decimals
69
106
  );
70
107
  const price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
@@ -76,6 +113,24 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
76
113
  };
77
114
  }
78
115
 
116
+ /**
117
+ * Builds a manage call from an adapter's proof tree.
118
+ * DRY helper for the repeated getProofs → getManageCall pattern.
119
+ */
120
+ protected async buildManageCallFromAdapter(
121
+ adapter: { getProofs: (isDeposit: boolean, tree: any) => any },
122
+ isDeposit: boolean,
123
+ amount: Web3Number,
124
+ additionalParams?: Record<string, any>,
125
+ ): Promise<Call> {
126
+ const proofsInfo = adapter.getProofs(isDeposit, this.getMerkleTree());
127
+ const manageCalls = await proofsInfo.callConstructor({ amount, ...additionalParams });
128
+ return this.getManageCall(
129
+ this.getProofGroupsForManageCalls(manageCalls),
130
+ manageCalls,
131
+ );
132
+ }
133
+
79
134
  /**
80
135
  * Bridges liquidity from the vault allocator back to the vault.
81
136
  * Note: This function is common for any SVK strategy.
@@ -85,37 +140,54 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
85
140
  const approveAdapter = this.metadata.additionalInfo.leafAdapters.find(
86
141
  adapter => adapter().leaves.find(leaf => leaf.readableId === UNIVERSAL_MANAGE_IDS.APPROVE_BRING_LIQUIDITY)
87
142
  );
88
-
143
+
89
144
  if (!approveAdapter) {
90
145
  throw new Error(`${this.getTag()}::getBringLiquidityCall: approve adapter not found`);
91
146
  }
92
-
147
+
93
148
  // Find bring liquidity leaf adapter
94
149
  const bringLiquidityAdapter = this.metadata.additionalInfo.leafAdapters.find(
95
150
  adapter => adapter().leaves.find(leaf => leaf.readableId === UNIVERSAL_MANAGE_IDS.BRING_LIQUIDITY)
96
151
  );
97
-
152
+
98
153
  if (!bringLiquidityAdapter) {
99
154
  throw new Error(`${this.getTag()}::getBringLiquidityCall: bring liquidity adapter not found`);
100
155
  }
101
-
102
- // Get proofs
103
- const tree = this.getMerkleTree();
104
- const proofGroups: string[][] = [];
105
- for (const [i, v] of tree.entries()) {
106
- if (v.readableId === UNIVERSAL_MANAGE_IDS.APPROVE_BRING_LIQUIDITY ||
107
- v.readableId === UNIVERSAL_MANAGE_IDS.BRING_LIQUIDITY) {
108
- proofGroups.push(tree.getProof(i));
109
- }
110
- }
111
-
156
+
112
157
  // Construct calls
113
158
  logger.verbose("approve adapter amount", params.amount);
114
159
  const approveCalls = await approveAdapter().callConstructor({ amount: params.amount });
115
160
  const bringLiquidityCalls = await bringLiquidityAdapter().callConstructor({ amount: params.amount });
116
-
161
+ const manageCalls = [...approveCalls, ...bringLiquidityCalls];
162
+
117
163
  // Combine into final call
118
- return this.getManageCall(proofGroups, [...approveCalls, ...bringLiquidityCalls]);
164
+ return this.getManageCall(this.getProofGroupsForManageCalls(manageCalls), manageCalls);
165
+ }
166
+
167
+ /**
168
+ * Resolves ordered merkle proofs from emitted manage calls.
169
+ * This ensures only runtime-required proofs are passed to manager calls.
170
+ */
171
+ protected getProofGroupsForManageCalls(manageCalls: ManageCall[]): string[][] {
172
+ const tree = this.getMerkleTree();
173
+ const proofByReadableId = new Map<string, string[]>();
174
+ for (const [i, v] of tree.entries()) {
175
+ if (proofByReadableId.has(v.readableId)) {
176
+ throw new Error(`Duplicate readableId found in merkle tree: ${v.readableId}`);
177
+ }
178
+ proofByReadableId.set(v.readableId, tree.getProof(i));
179
+ }
180
+
181
+ return manageCalls.map((manageCall, index) => {
182
+ if (!manageCall.proofReadableId) {
183
+ throw new Error(`Missing proofReadableId for manage call at index ${index}`);
184
+ }
185
+ const proof = proofByReadableId.get(manageCall.proofReadableId);
186
+ if (!proof) {
187
+ throw new Error(`Proof not found for readableId: ${manageCall.proofReadableId}`);
188
+ }
189
+ return proof;
190
+ });
119
191
  }
120
192
 
121
193
  /**
@@ -157,10 +229,10 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
157
229
  */
158
230
  getManageCall(proofGroups: string[][], manageCalls: ManageCall[]): Call {
159
231
  assert(
160
- proofGroups.length == manageCalls.length,
232
+ proofGroups.length == manageCalls.length,
161
233
  `Proof IDs and Manage Calls length mismatch: ${proofGroups.length} != ${manageCalls.length}`
162
234
  );
163
-
235
+
164
236
  return this.managerContract.populate('manage_vault_with_merkle_verification', {
165
237
  proofs: proofGroups.map(group => group),
166
238
  decoder_and_sanitizers: manageCalls.map(call => call.sanitizer.address),
@@ -176,7 +248,7 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
176
248
  */
177
249
  getSetManagerCall(strategist: ContractAddr, root = this.getMerkleRoot()): Call {
178
250
  return this.managerContract.populate('set_manage_root', [
179
- strategist.address,
251
+ strategist.address,
180
252
  num.getHexString(root)
181
253
  ]);
182
254
  }
@@ -194,7 +266,19 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
194
266
  */
195
267
  async getVaultPositions(): Promise<VaultPosition[]> {
196
268
  const positions = await Promise.all(
197
- this.metadata.additionalInfo.adapters.map(adapter => adapter.adapter.getPositions())
269
+ [
270
+ ...this.metadata.additionalInfo.adapters.map(adapter => adapter.adapter.getPositions()),
271
+ (async () => {
272
+ const unused = await this.getUnusedBalance();
273
+ return {
274
+ amount: unused.amount,
275
+ usdValue: unused.usdValue,
276
+ remarks: "Unused Balance",
277
+ tokenInfo: this.asset(),
278
+ protocol: Protocols.NONE
279
+ };
280
+ })()
281
+ ]
198
282
  )
199
283
  return positions.flat().map((position) => ({
200
284
  amount: position.amount,
@@ -222,7 +306,19 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
222
306
  const netAPY = weightedAPYs / totalHoldingsUSDValue;
223
307
  return {
224
308
  net: netAPY,
225
- splits: allPositions.map(p => ({apy: p.apy.apy, id: p.remarks ?? ''}))
309
+ splits: allPositions.map(p => ({ apy: p.apy.apy, id: p.remarks ?? '' }))
310
+ };
311
+ }
312
+
313
+ async getTVL(): Promise<SingleTokenInfo> {
314
+ const tvl = await this.contract.call('total_assets', []) as bigint;
315
+ const amount = Web3Number.fromWei(tvl.toString(), this.asset().decimals);
316
+ const price = await this.pricer.getPrice(this.asset().symbol);
317
+ const usdValue = Number(amount.toFixed(6)) * price.price;
318
+ return {
319
+ tokenInfo: this.asset(),
320
+ amount,
321
+ usdValue
226
322
  };
227
323
  }
228
324
 
@@ -244,4 +340,3 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
244
340
  }
245
341
 
246
342
  }
247
-