@strkfarm/sdk 2.0.0-dev.27 → 2.0.0-dev.28

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 (70) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +79130 -49357
  4. package/dist/index.browser.mjs +18039 -11434
  5. package/dist/index.d.ts +2869 -898
  6. package/dist/index.js +19036 -12210
  7. package/dist/index.mjs +18942 -12161
  8. package/package.json +1 -1
  9. package/src/data/avnu.abi.json +840 -0
  10. package/src/data/ekubo-price-fethcer.abi.json +265 -0
  11. package/src/dataTypes/_bignumber.ts +13 -4
  12. package/src/dataTypes/index.ts +3 -2
  13. package/src/dataTypes/mynumber.ts +141 -0
  14. package/src/global.ts +76 -41
  15. package/src/index.browser.ts +2 -1
  16. package/src/interfaces/common.tsx +167 -2
  17. package/src/modules/ExtendedWrapperSDk/types.ts +26 -4
  18. package/src/modules/ExtendedWrapperSDk/wrapper.ts +110 -67
  19. package/src/modules/apollo-client-config.ts +28 -0
  20. package/src/modules/avnu.ts +4 -4
  21. package/src/modules/ekubo-pricer.ts +79 -0
  22. package/src/modules/ekubo-quoter.ts +46 -30
  23. package/src/modules/erc20.ts +17 -0
  24. package/src/modules/harvests.ts +43 -29
  25. package/src/modules/pragma.ts +23 -8
  26. package/src/modules/pricer-from-api.ts +156 -15
  27. package/src/modules/pricer-lst.ts +1 -1
  28. package/src/modules/pricer.ts +40 -4
  29. package/src/modules/pricerBase.ts +2 -1
  30. package/src/node/deployer.ts +36 -1
  31. package/src/node/pricer-redis.ts +2 -1
  32. package/src/strategies/base-strategy.ts +78 -10
  33. package/src/strategies/ekubo-cl-vault.tsx +906 -347
  34. package/src/strategies/factory.ts +159 -0
  35. package/src/strategies/index.ts +6 -1
  36. package/src/strategies/registry.ts +239 -0
  37. package/src/strategies/sensei.ts +335 -7
  38. package/src/strategies/svk-strategy.ts +97 -27
  39. package/src/strategies/types.ts +4 -0
  40. package/src/strategies/universal-adapters/adapter-utils.ts +2 -1
  41. package/src/strategies/universal-adapters/avnu-adapter.ts +177 -268
  42. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  43. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  44. package/src/strategies/universal-adapters/extended-adapter.ts +155 -336
  45. package/src/strategies/universal-adapters/index.ts +9 -8
  46. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  47. package/src/strategies/universal-adapters/usdc<>usdce-adapter.ts +200 -0
  48. package/src/strategies/universal-adapters/vesu-adapter.ts +110 -75
  49. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +476 -0
  50. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +762 -844
  51. package/src/strategies/universal-adapters/vesu-position-common.ts +251 -0
  52. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  53. package/src/strategies/universal-lst-muliplier-strategy.tsx +396 -204
  54. package/src/strategies/universal-strategy.tsx +1426 -1178
  55. package/src/strategies/vesu-extended-strategy/services/executionService.ts +2251 -0
  56. package/src/strategies/vesu-extended-strategy/services/extended-vesu-state-manager.ts +2941 -0
  57. package/src/strategies/vesu-extended-strategy/services/operationService.ts +12 -1
  58. package/src/strategies/vesu-extended-strategy/types/transaction-metadata.ts +52 -0
  59. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +1 -0
  60. package/src/strategies/vesu-extended-strategy/utils/constants.ts +2 -0
  61. package/src/strategies/vesu-extended-strategy/utils/helper.ts +158 -124
  62. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +377 -1788
  63. package/src/strategies/vesu-rebalance.tsx +255 -152
  64. package/src/utils/health-factor-math.ts +4 -1
  65. package/src/utils/index.ts +2 -1
  66. package/src/utils/logger.browser.ts +22 -4
  67. package/src/utils/logger.node.ts +259 -24
  68. package/src/utils/starknet-call-parser.ts +1036 -0
  69. package/src/utils/strategy-utils.ts +61 -0
  70. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
@@ -1,7 +1,7 @@
1
- import { getNoRiskTags, highlightTextWithLinks, IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType, TokenInfo } from "@/interfaces";
1
+ import { getNoRiskTags, highlightTextWithLinks, IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType, StrategyTag, TokenInfo, AuditStatus, SourceCodeType, AccessControlType, InstantWithdrawalVault, StrategyLiveStatus, VaultType } from "@/interfaces";
2
2
  import { BaseStrategy, SingleActionAmount, SingleTokenInfo } from "./base-strategy";
3
3
  import { ContractAddr, Web3Number } from "@/dataTypes";
4
- import { Call, Contract, uint256 } from "starknet";
4
+ import { Call, Contract, num, uint256, BlockIdentifier } from "starknet";
5
5
  import SenseiABI from "@/data/sensei.abi.json";
6
6
  import { getTrovesEndpoint, logger } from "@/utils";
7
7
  import { Global } from "@/global";
@@ -9,6 +9,10 @@ import { QuoteRequest } from "@avnu/avnu-sdk";
9
9
  import { PricerBase } from "@/modules/pricerBase";
10
10
  import ERC20ABI from "@/data/erc20.abi.json";
11
11
  import { AvnuWrapper } from "@/modules";
12
+ import { gql } from "@apollo/client";
13
+ import apolloClient from "@/modules/apollo-client";
14
+ import { VesuAdapter, VesuPools } from "./universal-adapters/vesu-adapter";
15
+ import { LSTAPRService } from "@/modules/lst-apr";
12
16
 
13
17
  export interface SenseiVaultSettings {
14
18
  mainToken: TokenInfo;
@@ -37,17 +41,27 @@ export class SenseiVault extends BaseStrategy<
37
41
  }
38
42
  }
39
43
 
40
- async getUserTVL(user: ContractAddr): Promise<SingleTokenInfo> {
44
+ async getUserTVL(user: ContractAddr, blockIdentifier: BlockIdentifier = "latest"): Promise<SingleTokenInfo> {
41
45
  const result: any = await this.contract.call(
42
46
  "describe_position",
43
47
  [user.address],
48
+ {
49
+ blockIdentifier,
50
+ }
44
51
  );
45
52
  const amount = Web3Number.fromWei(
46
53
  uint256.uint256ToBN(result[1].estimated_size).toString(),
47
54
  this.metadata.depositTokens[0].decimals,
48
55
  )
56
+
57
+ // Convert blockIdentifier to block number for pricer if it's a number
58
+ const blockNumber = typeof blockIdentifier === 'number' || typeof blockIdentifier === 'bigint'
59
+ ? Number(blockIdentifier)
60
+ : undefined;
61
+
49
62
  const price = await this.pricer.getPrice(
50
63
  this.metadata.depositTokens[0].symbol,
64
+ blockNumber
51
65
  );
52
66
  return {
53
67
  usdValue: Number(amount.toFixed(6)) * price.price,
@@ -223,6 +237,268 @@ export class SenseiVault extends BaseStrategy<
223
237
  logger.verbose('getSettings', settings);
224
238
  return settings;
225
239
  };
240
+
241
+ /**
242
+ * Calculate lifetime earnings for a user
243
+ * Not yet implemented for Sensei Vault strategy
244
+ */
245
+ getLifetimeEarnings(
246
+ userTVL: SingleTokenInfo,
247
+ investmentFlows: Array<{ amount: string; type: string; timestamp: number; tx_hash: string }>
248
+ ): any {
249
+ throw new Error("getLifetimeEarnings is not implemented yet for this strategy");
250
+ }
251
+
252
+ async netAPY(): Promise<number> {
253
+ try {
254
+ // Fetch Vesu pools and select the Re7 xSTRK pool
255
+ const { pools } = await VesuAdapter.getVesuPools();
256
+ const re7PoolId = VesuPools.Re7xSTRK;
257
+ const pool = pools.find((p: any) =>
258
+ ContractAddr.from(num.getHexString(p.id)).eq(re7PoolId),
259
+ );
260
+
261
+ if (!pool) {
262
+ logger.warn(`${SenseiVault.name}::netAPY - Re7 xSTRK pool not found`);
263
+ return 0;
264
+ }
265
+
266
+ const mainSymbol = this.metadata.additionalInfo.mainToken.symbol.toLowerCase(); // STRK
267
+ const secondarySymbol =
268
+ this.metadata.additionalInfo.secondaryToken.symbol.toLowerCase(); // xSTRK
269
+
270
+ const collateralAssetStats = pool.assets.find(
271
+ (a: any) => String(a.symbol).toLowerCase() === secondarySymbol,
272
+ )?.stats;
273
+ const debtAssetStats = pool.assets.find(
274
+ (a: any) => String(a.symbol).toLowerCase() === mainSymbol,
275
+ )?.stats;
276
+
277
+ if (!collateralAssetStats || !debtAssetStats) {
278
+ logger.warn(
279
+ `${SenseiVault.name}::netAPY - Missing collateral/debt stats on Vesu pool`,
280
+ );
281
+ return 0;
282
+ }
283
+
284
+ // Base xSTRK lending APY from Vesu
285
+ const xstrkSupplyAPY =
286
+ Number(collateralAssetStats.supplyApy?.value || 0) / 1e18;
287
+
288
+ // STRK rewards APR on xSTRK collateral from Vesu
289
+ const strkRewardsAPR = collateralAssetStats.defiSpringSupplyApr
290
+ ? Number(collateralAssetStats.defiSpringSupplyApr.value || 0) / 1e18
291
+ : 0;
292
+
293
+ // STRK borrow APY from Vesu
294
+ const borrowAPY =
295
+ Number(debtAssetStats.borrowApr?.value || 0) / 1e18;
296
+
297
+ // LST APR for xSTRK from Endur (based on underlying STRK asset)
298
+ const lstAPY = await LSTAPRService.getLSTAPR(
299
+ this.metadata.additionalInfo.mainToken.address,
300
+ );
301
+
302
+ // Collateral APY = Vesu xSTRK supply + Endur xSTRK APR + STRK rewards
303
+ const collateralAPY = xstrkSupplyAPY + lstAPY + strkRewardsAPR;
304
+
305
+ const feeFactor = this.metadata.additionalInfo.feeBps / 10000; // convert bps to decimal
306
+ const feeAdjustedColAPY =
307
+ collateralAPY - strkRewardsAPR * feeFactor;
308
+
309
+ // Position info (collateral & debt in USD terms)
310
+ const { collateralUSDValue, debtUSDValue } =
311
+ await this.getPositionInfo();
312
+
313
+ const collateralUSD = Number(collateralUSDValue.toFixed(6));
314
+ const debtUSD = Number(debtUSDValue.toFixed(6));
315
+
316
+ // Compute expected leverage using the same math as app-side strategy
317
+ const targetHf = this.metadata.additionalInfo.targetHfBps / 10000;
318
+ const xSTRKPrice = await this.getSecondaryTokenPriceRelativeToMain();
319
+ const denominator = targetHf * xSTRKPrice - 0.87;
320
+ if (denominator <= 0) {
321
+ logger.warn(
322
+ `${SenseiVault.name}::netAPY - Invalid denominator in leverage calc`,
323
+ );
324
+ return 0;
325
+ }
326
+ const borrowedSTRK = (0.87 * xSTRKPrice) / denominator;
327
+ const expectedLeverage = 1 + borrowedSTRK;
328
+ if (!Number.isFinite(expectedLeverage) || expectedLeverage <= 0) {
329
+ logger.warn(
330
+ `${SenseiVault.name}::netAPY - Non-positive or invalid expectedLeverage`,
331
+ );
332
+ return 0;
333
+ }
334
+
335
+ const payoff =
336
+ collateralUSD * feeAdjustedColAPY - debtUSD * borrowAPY;
337
+ const investment = collateralUSD - debtUSD;
338
+
339
+ if (investment === 0) {
340
+ return 0;
341
+ }
342
+
343
+ const netAPY = payoff / investment;
344
+ return Number.isFinite(netAPY) ? netAPY : 0;
345
+ } catch (error) {
346
+ logger.error(`${SenseiVault.name}::netAPY - Error`, error);
347
+ return 0;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Calculates user realized APY based on position growth accounting for deposits and withdrawals.
353
+ * Returns the APY as a number.
354
+ * Not implemented for Sensei Strategy yet.
355
+ */
356
+ async getUserRealizedAPY(
357
+ blockIdentifier: BlockIdentifier = "latest",
358
+ sinceBlocks = 600000
359
+ ): Promise<number> {
360
+ throw new Error("getUserRealizedAPY not implemented yet for Sensei strategy");
361
+
362
+ /*
363
+ logger.verbose(
364
+ `${SenseiVault.name}: getUserRealizedAPY => starting with userAddress=${userAddress.address}, blockIdentifier=${blockIdentifier}, sinceBlocks=${sinceBlocks}`
365
+ );
366
+
367
+ // Determine current block number
368
+ let blockNow =
369
+ typeof blockIdentifier === "number" || typeof blockIdentifier === "bigint"
370
+ ? Number(blockIdentifier)
371
+ : (await this.config.provider.getBlockLatestAccepted()).block_number;
372
+
373
+ // Look back window, but never before launch block
374
+ const blockBefore = Math.max(
375
+ blockNow - sinceBlocks,
376
+ this.metadata.launchBlock
377
+ );
378
+
379
+ logger.verbose(
380
+ `${SenseiVault.name}: getUserRealizedAPY => blockNow=${blockNow}, blockBefore=${blockBefore}`
381
+ );
382
+
383
+ // Get current estimated size
384
+ const currentResult: any = await this.contract.call(
385
+ "describe_position",
386
+ [userAddress.address],
387
+ {
388
+ blockIdentifier,
389
+ }
390
+ );
391
+ const currentEstimatedSize = Web3Number.fromWei(
392
+ uint256.uint256ToBN(currentResult[1].estimated_size).toString(),
393
+ this.metadata.depositTokens[0].decimals,
394
+ );
395
+
396
+ // Get previous estimated size
397
+ const previousResult: any = await this.contract.call(
398
+ "describe_position",
399
+ [userAddress.address],
400
+ {
401
+ blockIdentifier: blockBefore,
402
+ }
403
+ );
404
+ const previousEstimatedSize = Web3Number.fromWei(
405
+ uint256.uint256ToBN(previousResult[1].estimated_size).toString(),
406
+ this.metadata.depositTokens[0].decimals,
407
+ );
408
+
409
+ logger.verbose(
410
+ `${SenseiVault.name}: getUserRealizedAPY => currentEstimatedSize=${currentEstimatedSize.toString()}, previousEstimatedSize=${previousEstimatedSize.toString()}`
411
+ );
412
+
413
+ // Query GraphQL for deposits and withdrawals between blockBefore and blockNow
414
+ let newDeposits = Web3Number.fromWei("0", this.metadata.depositTokens[0].decimals);
415
+ let newWithdrawals = Web3Number.fromWei("0", this.metadata.depositTokens[0].decimals);
416
+
417
+ try {
418
+ const { data } = await apolloClient.query({
419
+ query: gql`
420
+ query Query($where: Investment_flowsWhereInput) {
421
+ findManyInvestment_flows(where: $where) {
422
+ block_number
423
+ amount
424
+ type
425
+ }
426
+ }
427
+ `,
428
+ variables: {
429
+ where: {
430
+ contract: {
431
+ equals: this.address.address.toLowerCase(),
432
+ },
433
+ owner: {
434
+ equals: userAddress.address.toLowerCase(),
435
+ },
436
+ block_number: {
437
+ gte: blockBefore,
438
+ lte: blockNow,
439
+ },
440
+ },
441
+ },
442
+ fetchPolicy: 'no-cache',
443
+ });
444
+
445
+ // Sum deposits and withdrawals
446
+ for (const flow of data.findManyInvestment_flows) {
447
+ const amount = Web3Number.fromWei(
448
+ flow.amount,
449
+ this.metadata.depositTokens[0].decimals
450
+ );
451
+ if (flow.type === 'deposit') {
452
+ newDeposits = newDeposits.plus(amount);
453
+ } else if (flow.type === 'withdraw') {
454
+ newWithdrawals = newWithdrawals.plus(amount);
455
+ }
456
+ }
457
+
458
+ logger.verbose(
459
+ `${SenseiVault.name}: getUserRealizedAPY => newDeposits=${newDeposits.toString()}, newWithdrawals=${newWithdrawals.toString()}`
460
+ );
461
+ } catch (error) {
462
+ logger.verbose(
463
+ `${SenseiVault.name}: getUserRealizedAPY => Error querying GraphQL, continuing with zero deposits/withdrawals:`,
464
+ error
465
+ );
466
+ // Continue with zero deposits/withdrawals if GraphQL query fails
467
+ }
468
+
469
+ // Calculate growth: Current estimated size - new deposits + new withdrawals - previous estimated size
470
+ const growth = currentEstimatedSize
471
+ .minus(newDeposits)
472
+ .plus(newWithdrawals)
473
+ .minus(previousEstimatedSize);
474
+
475
+ logger.verbose(
476
+ `${SenseiVault.name}: getUserRealizedAPY => growth=${growth.toString()}`
477
+ );
478
+
479
+ // Handle edge case where previous position is zero
480
+ if (previousEstimatedSize.isZero() || previousEstimatedSize.lte(0)) {
481
+ logger.verbose(
482
+ `${SenseiVault.name}: getUserRealizedAPY => Previous position is zero, returning 0`
483
+ );
484
+ return 0;
485
+ }
486
+
487
+ // Calculate actual block difference (in case limited by launch block)
488
+ const actualBlockDiff = blockNow - blockBefore;
489
+
490
+ // Calculate APY: 100 * 365/n * growth / previousEstimatedSize
491
+ // where n is the number of blocks (sinceBlocks or actual block difference)
492
+ const growthRatio = growth.dividedBy(previousEstimatedSize);
493
+ const apy = Number(growthRatio) * 100 * 365 / actualBlockDiff;
494
+
495
+ logger.verbose(
496
+ `${SenseiVault.name}: getUserRealizedAPY => actualBlockDiff=${actualBlockDiff}, growthRatio=${Number(growthRatio)}, apy=${apy}`
497
+ );
498
+
499
+ return apy;
500
+ */
501
+ }
226
502
 
227
503
  }
228
504
 
@@ -289,9 +565,10 @@ const FAQS = [
289
565
  export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
290
566
  [
291
567
  {
568
+ id: "xstrk_sensei",
292
569
  name: "xSTRK Sensei",
293
570
  description: highlightTextWithLinks(
294
- senseiDescription.replaceAll('{{token1}}', 'STRK').replaceAll('{{token2}}', 'xSTRK'),
571
+ senseiDescription.replace('{{token1}}', 'STRK').replace('{{token2}}', 'xSTRK'),
295
572
  [{
296
573
  highlight: "Endur",
297
574
  link: "https://endur.fi"
@@ -308,11 +585,37 @@ export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
308
585
  ),
309
586
  launchBlock: 1053811,
310
587
  type: "Other",
588
+ curator: {
589
+ name: "Unwrap Labs",
590
+ logo: "https://assets.troves.fi/integrations/unwraplabs/white.png"
591
+ },
592
+ vaultType: {
593
+ type: VaultType.LOOPING,
594
+ description: "Creates leveraged looping position on xSTRK by borrowing STRK to increase yield"
595
+ },
311
596
  depositTokens: [
312
597
  Global.getDefaultTokens().find((t) => t.symbol === "STRK")!
313
598
  ],
314
599
  protocols: [endurProtocol, vesuProtocol],
315
- maxTVL: new Web3Number("1500000", 18),
600
+ settings: {
601
+ maxTVL: new Web3Number("1500000", 18),
602
+ alerts: [
603
+ {
604
+ type: "info",
605
+ 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.",
606
+ tab: "all"
607
+ }
608
+ ],
609
+ liveStatus: StrategyLiveStatus.ACTIVE,
610
+ isPaused: false,
611
+ isInMaintenance: false,
612
+ isAudited: false,
613
+ isInstantWithdrawal: true,
614
+ isTransactionHistDisabled: true,
615
+ quoteToken: Global.getDefaultTokens().find(
616
+ (t) => t.symbol === "STRK"
617
+ )!
618
+ },
316
619
  risk: {
317
620
  riskFactor: _riskFactor,
318
621
  netRisk:
@@ -335,6 +638,31 @@ export const SenseiStrategies: IStrategyMetadata<SenseiVaultSettings>[] =
335
638
  "Buy more xSTRK with borrowed STRK",
336
639
  "Repeat the process to loop your position",
337
640
  "Claim DeFi spring (STRK) rewards weekly and reinvest",
338
- ]
641
+ ],
642
+ tags: [StrategyTag.LEVERED],
643
+ security: {
644
+ auditStatus: AuditStatus.AUDITED,
645
+ sourceCode: {
646
+ type: SourceCodeType.CLOSED_SOURCE,
647
+ contractLink: "https://github.com/trovesfi/troves-contracts",
648
+ },
649
+ accessControl: {
650
+ type: AccessControlType.STANDARD_ACCOUNT,
651
+ addresses: [ContractAddr.from("0x0")],
652
+ timeLock: "2 Days",
653
+ },
654
+ },
655
+ redemptionInfo: {
656
+ instantWithdrawalVault: InstantWithdrawalVault.YES,
657
+ redemptionsInfo: [],
658
+ alerts: [],
659
+ },
660
+ usualTimeToEarnings: "2 weeks",
661
+ 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 hours. This is when you realise your earnings.",
662
+ points: [{
663
+ multiplier: 4,
664
+ logo: 'https://endur.fi/favicon.ico',
665
+ toolTip: "This strategy holds xSTRK. Earn 3-4x Endur points on your xSTRK due to the leverage. Points can be found on endur.fi.",
666
+ }]
339
667
  },
340
- ];
668
+ ];
@@ -3,7 +3,7 @@ import { IConfig, IStrategyMetadata, TokenInfo, VaultPosition } from "@/interfac
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,23 @@ 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
+ ): Promise<Call> {
125
+ const proofsInfo = adapter.getProofs(isDeposit, this.getMerkleTree());
126
+ const manageCalls = await proofsInfo.callConstructor({ amount });
127
+ return this.getManageCall(
128
+ this.getProofGroupsForManageCalls(manageCalls),
129
+ manageCalls,
130
+ );
131
+ }
132
+
79
133
  /**
80
134
  * Bridges liquidity from the vault allocator back to the vault.
81
135
  * Note: This function is common for any SVK strategy.
@@ -85,37 +139,54 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
85
139
  const approveAdapter = this.metadata.additionalInfo.leafAdapters.find(
86
140
  adapter => adapter().leaves.find(leaf => leaf.readableId === UNIVERSAL_MANAGE_IDS.APPROVE_BRING_LIQUIDITY)
87
141
  );
88
-
142
+
89
143
  if (!approveAdapter) {
90
144
  throw new Error(`${this.getTag()}::getBringLiquidityCall: approve adapter not found`);
91
145
  }
92
-
146
+
93
147
  // Find bring liquidity leaf adapter
94
148
  const bringLiquidityAdapter = this.metadata.additionalInfo.leafAdapters.find(
95
149
  adapter => adapter().leaves.find(leaf => leaf.readableId === UNIVERSAL_MANAGE_IDS.BRING_LIQUIDITY)
96
150
  );
97
-
151
+
98
152
  if (!bringLiquidityAdapter) {
99
153
  throw new Error(`${this.getTag()}::getBringLiquidityCall: bring liquidity adapter not found`);
100
154
  }
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
-
155
+
112
156
  // Construct calls
113
157
  logger.verbose("approve adapter amount", params.amount);
114
158
  const approveCalls = await approveAdapter().callConstructor({ amount: params.amount });
115
159
  const bringLiquidityCalls = await bringLiquidityAdapter().callConstructor({ amount: params.amount });
116
-
160
+ const manageCalls = [...approveCalls, ...bringLiquidityCalls];
161
+
117
162
  // Combine into final call
118
- return this.getManageCall(proofGroups, [...approveCalls, ...bringLiquidityCalls]);
163
+ return this.getManageCall(this.getProofGroupsForManageCalls(manageCalls), manageCalls);
164
+ }
165
+
166
+ /**
167
+ * Resolves ordered merkle proofs from emitted manage calls.
168
+ * This ensures only runtime-required proofs are passed to manager calls.
169
+ */
170
+ protected getProofGroupsForManageCalls(manageCalls: ManageCall[]): string[][] {
171
+ const tree = this.getMerkleTree();
172
+ const proofByReadableId = new Map<string, string[]>();
173
+ for (const [i, v] of tree.entries()) {
174
+ if (proofByReadableId.has(v.readableId)) {
175
+ throw new Error(`Duplicate readableId found in merkle tree: ${v.readableId}`);
176
+ }
177
+ proofByReadableId.set(v.readableId, tree.getProof(i));
178
+ }
179
+
180
+ return manageCalls.map((manageCall, index) => {
181
+ if (!manageCall.proofReadableId) {
182
+ throw new Error(`Missing proofReadableId for manage call at index ${index}`);
183
+ }
184
+ const proof = proofByReadableId.get(manageCall.proofReadableId);
185
+ if (!proof) {
186
+ throw new Error(`Proof not found for readableId: ${manageCall.proofReadableId}`);
187
+ }
188
+ return proof;
189
+ });
119
190
  }
120
191
 
121
192
  /**
@@ -157,10 +228,10 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
157
228
  */
158
229
  getManageCall(proofGroups: string[][], manageCalls: ManageCall[]): Call {
159
230
  assert(
160
- proofGroups.length == manageCalls.length,
231
+ proofGroups.length == manageCalls.length,
161
232
  `Proof IDs and Manage Calls length mismatch: ${proofGroups.length} != ${manageCalls.length}`
162
233
  );
163
-
234
+
164
235
  return this.managerContract.populate('manage_vault_with_merkle_verification', {
165
236
  proofs: proofGroups.map(group => group),
166
237
  decoder_and_sanitizers: manageCalls.map(call => call.sanitizer.address),
@@ -176,7 +247,7 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
176
247
  */
177
248
  getSetManagerCall(strategist: ContractAddr, root = this.getMerkleRoot()): Call {
178
249
  return this.managerContract.populate('set_manage_root', [
179
- strategist.address,
250
+ strategist.address,
180
251
  num.getHexString(root)
181
252
  ]);
182
253
  }
@@ -244,4 +315,3 @@ export abstract class SVKStrategy<S extends UniversalStrategySettings>
244
315
  }
245
316
 
246
317
  }
247
-
@@ -0,0 +1,4 @@
1
+ export enum LSTPriceType {
2
+ ENDUR_PRICE = 'ENDUR_PRICE',
3
+ AVNU_PRICE = 'AVNU_PRICE'
4
+ }
@@ -15,6 +15,7 @@ export const AVNU_EXCHANGE_FOR_LEGACY_USDC = ContractAddr.from('0x7bffc7f6bda62b
15
15
  export const AVNU_QUOTE_URL = "https://starknet.api.avnu.fi/swap/v3/quotes";
16
16
  export const EXTENDED_CONTRACT = ContractAddr.from('0x062da0780fae50d68cecaa5a051606dc21217ba290969b302db4dd99d2e9b470');
17
17
  export const VESU_SINGLETON = ContractAddr.from('0x000d8d6dfec4d33bfb6895de9f3852143a17c6f92fd2a21da3d6924d34870160');
18
+ export const TRANSFER_SANITIZER = ContractAddr.from('0x07c94fd8762b37b02c2afeaf37e5d77523a2aedd370ccff6e130b2d5e9e23037');
18
19
  export function toBigInt(value: string | number): bigint {
19
20
  if (typeof value === 'string') {
20
21
  return BigInt(value);
@@ -23,4 +24,4 @@ export function toBigInt(value: string | number): bigint {
23
24
  } else {
24
25
  throw new Error('Value must be a string or number');
25
26
  }
26
- }
27
+ }