@strkfarm/sdk 1.0.15 → 1.0.17

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.
@@ -0,0 +1,466 @@
1
+ import { ContractAddr, Web3Number } from "@/dataTypes";
2
+ import { IConfig, IInvestmentFlow, IProtocol, IStrategyMetadata, RiskFactor, RiskType } from "@/interfaces";
3
+ import { Pricer } from "@/modules";
4
+ import { CairoCustomEnum, Contract, num, uint256 } from "starknet";
5
+ import VesuRebalanceAbi from '@/data/vesu-rebalance.abi.json';
6
+ import { Global } from "@/global";
7
+ import { assert } from "@/utils";
8
+ import axios from "axios";
9
+ import { PricerBase } from "@/modules/pricerBase";
10
+
11
+ interface PoolProps {
12
+ pool_id: ContractAddr;
13
+ max_weight: number;
14
+ v_token: ContractAddr;
15
+ }
16
+
17
+ interface Change {
18
+ pool_id: ContractAddr;
19
+ changeAmt: Web3Number;
20
+ finalAmt: Web3Number;
21
+ isDeposit: boolean;
22
+ }
23
+
24
+ interface PoolInfoFull {
25
+ pool_id: ContractAddr;
26
+ pool_name: string | undefined;
27
+ max_weight: number;
28
+ current_weight: number;
29
+ v_token: ContractAddr;
30
+ amount: Web3Number;
31
+ usdValue: Web3Number;
32
+ APY: {
33
+ baseApy: number;
34
+ defiSpringApy: number;
35
+ netApy: number;
36
+ };
37
+ currentUtilization: number;
38
+ maxUtilization: number;
39
+ }
40
+ /**
41
+ * Represents a VesuRebalance strategy.
42
+ * This class implements an automated rebalancing strategy for Vesu pools,
43
+ * managing deposits and withdrawals while optimizing yield through STRK rewards.
44
+ */
45
+ export class VesuRebalance {
46
+ /** Configuration object for the strategy */
47
+ readonly config: IConfig;
48
+ /** Contract address of the strategy */
49
+ readonly address: ContractAddr;
50
+ /** Pricer instance for token price calculations */
51
+ readonly pricer: PricerBase;
52
+ /** Metadata containing strategy information */
53
+ readonly metadata: IStrategyMetadata;
54
+ /** Contract instance for interacting with the strategy */
55
+ readonly contract: Contract;
56
+ readonly BASE_WEIGHT = 10000; // 10000 bps = 100%
57
+
58
+ /**
59
+ * Creates a new VesuRebalance strategy instance.
60
+ * @param config - Configuration object containing provider and other settings
61
+ * @param pricer - Pricer instance for token price calculations
62
+ * @param metadata - Strategy metadata including deposit tokens and address
63
+ * @throws {Error} If more than one deposit token is specified
64
+ */
65
+ constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata) {
66
+ this.config = config;
67
+ this.pricer = pricer;
68
+
69
+ assert(metadata.depositTokens.length === 1, 'VesuRebalance only supports 1 deposit token');
70
+ this.metadata = metadata;
71
+ this.address = metadata.address;
72
+
73
+ this.contract = new Contract(VesuRebalanceAbi, this.address.address, this.config.provider);
74
+ }
75
+
76
+ /**
77
+ * Creates a deposit call to the strategy contract.
78
+ * @param assets - Amount of assets to deposit
79
+ * @param receiver - Address that will receive the strategy tokens
80
+ * @returns Populated contract call for deposit
81
+ */
82
+ depositCall(assets: Web3Number, receiver: ContractAddr) {
83
+ // Technically its not erc4626 abi, but we just need approve call
84
+ // so, its ok to use it
85
+ const assetContract = new Contract(VesuRebalanceAbi, this.metadata.depositTokens[0].address, this.config.provider);
86
+ const call1 = assetContract.populate('approve', [this.address.address, uint256.bnToUint256(assets.toWei())]);
87
+ const call2 = this.contract.populate('deposit', [uint256.bnToUint256(assets.toWei()), receiver.address]);
88
+ return [call1, call2];
89
+ }
90
+
91
+ /**
92
+ * Creates a withdrawal call to the strategy contract.
93
+ * @param assets - Amount of assets to withdraw
94
+ * @param receiver - Address that will receive the withdrawn assets
95
+ * @param owner - Address that owns the strategy tokens
96
+ * @returns Populated contract call for withdrawal
97
+ */
98
+ withdrawCall(assets: Web3Number, receiver: ContractAddr, owner: ContractAddr) {
99
+ return [this.contract.populate('withdraw', [uint256.bnToUint256(assets.toWei()), receiver.address, owner.address])];
100
+ }
101
+
102
+ /**
103
+ * Returns the underlying asset token of the strategy.
104
+ * @returns The deposit token supported by this strategy
105
+ */
106
+ asset() {
107
+ return this.metadata.depositTokens[0];
108
+ }
109
+
110
+ /**
111
+ * Returns the number of decimals used by the strategy token.
112
+ * @returns Number of decimals (same as the underlying token)
113
+ */
114
+ decimals() {
115
+ return this.metadata.depositTokens[0].decimals; // same as underlying token
116
+ }
117
+
118
+ /**
119
+ * Calculates the Total Value Locked (TVL) for a specific user.
120
+ * @param user - Address of the user
121
+ * @returns Object containing the amount in token units and USD value
122
+ */
123
+ async getUserTVL(user: ContractAddr) {
124
+ const shares = await this.contract.balanceOf(user.address);
125
+ const assets = await this.contract.convert_to_assets(uint256.bnToUint256(shares));
126
+ const amount = Web3Number.fromWei(assets.toString(), this.metadata.depositTokens[0].decimals);
127
+ let price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
128
+ const usdValue = Number(amount.toFixed(6)) * price.price;
129
+ return {
130
+ amount,
131
+ usdValue
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Calculates the total TVL of the strategy.
137
+ * @returns Object containing the total amount in token units and USD value
138
+ */
139
+ async getTVL() {
140
+ const assets = await this.contract.total_assets();
141
+ const amount = Web3Number.fromWei(assets.toString(), this.metadata.depositTokens[0].decimals);
142
+ let price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
143
+ const usdValue = Number(amount.toFixed(6)) * price.price;
144
+ return {
145
+ amount,
146
+ usdValue
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Retrieves the list of allowed pools and their detailed information from multiple sources:
152
+ * 1. Contract's allowed pools
153
+ * 2. Vesu positions API for current positions
154
+ * 3. Vesu pools API for APY and utilization data
155
+ *
156
+ * @returns {Promise<{
157
+ * data: Array<PoolInfoFull>,
158
+ * isErrorPositionsAPI: boolean
159
+ * }>} Object containing:
160
+ * - data: Array of pool information including IDs, weights, amounts, APYs and utilization
161
+ * - isErrorPositionsAPI: Boolean indicating if there was an error fetching position data
162
+ */
163
+ async getPools() {
164
+ const allowedPools: PoolProps[] = (await this.contract.get_allowed_pools()).map((p: any) => ({
165
+ pool_id: ContractAddr.from(p.pool_id),
166
+ max_weight: Number(p.max_weight) / this.BASE_WEIGHT,
167
+ v_token: ContractAddr.from(p.v_token),
168
+ }));
169
+
170
+ /*
171
+ Vesu Positions API Response Schema (/positions?walletAddress=):
172
+ {
173
+ "data": [{
174
+ "pool": {
175
+ "id": string, // Pool identifier
176
+ "name": string // Pool name
177
+ },
178
+ "collateral": {
179
+ "value": string, // Amount of collateral in wei
180
+ "usdPrice": {
181
+ "value": string, // USD value in wei
182
+ "decimals": number // Decimals for USD value
183
+ }
184
+ }
185
+ }]
186
+ }
187
+
188
+ Vesu Pools API Response Schema (/pools):
189
+ {
190
+ "data": [{
191
+ "id": string,
192
+ "assets": [{
193
+ "stats": {
194
+ "supplyApy": {
195
+ "value": string,
196
+ "decimals": number
197
+ },
198
+ "defiSpringSupplyApr": {
199
+ "value": string,
200
+ "decimals": number
201
+ },
202
+ "currentUtilization": {
203
+ "value": string,
204
+ "decimals": number
205
+ }
206
+ },
207
+ "config": {
208
+ "maxUtilization": {
209
+ "value": string,
210
+ "decimals": number
211
+ }
212
+ }
213
+ }],
214
+ }]
215
+ }
216
+ */
217
+ let isErrorPositionsAPI = false;
218
+ let vesuPositions: any[] = [];
219
+ try {
220
+ const res = await axios.get(`https://api.vesu.xyz/positions?walletAddress=${this.address.address}`)
221
+ const data = await res.data;
222
+ vesuPositions = data.data;
223
+ } catch (e) {
224
+ console.error(`${VesuRebalance.name}: Error fetching pools for ${this.address.address}`, e);
225
+ isErrorPositionsAPI = true;
226
+ }
227
+
228
+
229
+ let isErrorPoolsAPI = false;
230
+ let pools: any[] = [];
231
+ try {
232
+ const res = await axios.get(`https://api.vesu.xyz/pools`);
233
+ const data = await res.data;
234
+ pools = data.data;
235
+ } catch (e) {
236
+ console.error(`${VesuRebalance.name}: Error fetching pools for ${this.address.address}`, e);
237
+ isErrorPoolsAPI = true;
238
+ }
239
+
240
+ const totalAssets = (await this.getTVL()).amount;
241
+
242
+ const info = allowedPools.map(async (p) => {
243
+ const vesuPosition = vesuPositions.find((d: any) => d.pool.id.toString() === num.getDecimalString(p.pool_id.address.toString()));
244
+ const pool = pools.find((d: any) => d.id == num.getDecimalString(p.pool_id.address));
245
+ const assetInfo = pool?.assets.find((d: any) => ContractAddr.from(this.asset().address).eqString(d.address));
246
+ let vTokenContract = new Contract(VesuRebalanceAbi, p.v_token.address, this.config.provider);
247
+ const bal = await vTokenContract.balanceOf(this.address.address);
248
+ const assets = await vTokenContract.convert_to_assets(uint256.bnToUint256(bal.toString()));
249
+ const item = {
250
+ pool_id: p.pool_id,
251
+ pool_name: vesuPosition?.pool.name,
252
+ max_weight: p.max_weight,
253
+ current_weight: isErrorPositionsAPI || !vesuPosition ? 0 : Number(Web3Number.fromWei(vesuPosition.collateral.value, this.decimals()).dividedBy(totalAssets.toString()).toFixed(6)),
254
+ v_token: p.v_token,
255
+ amount: Web3Number.fromWei(assets.toString(), this.decimals()),
256
+ usdValue: isErrorPositionsAPI || !vesuPosition ? Web3Number.fromWei("0", this.decimals()) : Web3Number.fromWei(vesuPosition.collateral.usdPrice.value, vesuPosition.collateral.usdPrice.decimals),
257
+ APY: isErrorPoolsAPI || !assetInfo ? {
258
+ baseApy: 0,
259
+ defiSpringApy: 0,
260
+ netApy: 0,
261
+ } : {
262
+ baseApy: Number(Web3Number.fromWei(assetInfo.stats.supplyApy.value, assetInfo.stats.supplyApy.decimals).toFixed(6)),
263
+ defiSpringApy: Number(Web3Number.fromWei(assetInfo.stats.defiSpringSupplyApr.value, assetInfo.stats.defiSpringSupplyApr.decimals).toFixed(6)),
264
+ netApy: 0,
265
+ },
266
+ currentUtilization: isErrorPoolsAPI || !assetInfo ? 0 : Number(Web3Number.fromWei(assetInfo.stats.currentUtilization.value, assetInfo.stats.currentUtilization.decimals).toFixed(6)),
267
+ maxUtilization: isErrorPoolsAPI || !assetInfo ? 0 : Number(Web3Number.fromWei(assetInfo.config.maxUtilization.value, assetInfo.config.maxUtilization.decimals).toFixed(6)),
268
+ }
269
+ item.APY.netApy = item.APY.baseApy + item.APY.defiSpringApy;
270
+ return item;
271
+ });
272
+ const data = await Promise.all(info);
273
+ return {
274
+ data,
275
+ isErrorPositionsAPI,
276
+ isErrorPoolsAPI,
277
+ isError: isErrorPositionsAPI || isErrorPoolsAPI,
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Calculates the weighted average APY across all pools based on USD value.
283
+ * @returns {Promise<number>} The weighted average APY across all pools
284
+ */
285
+ async netAPY(): Promise<number> {
286
+ const { data: pools } = await this.getPools();
287
+ return this.netAPYGivenPools(pools);
288
+ }
289
+
290
+ /**
291
+ * Calculates the weighted average APY across all pools based on USD value.
292
+ * @returns {Promise<number>} The weighted average APY across all pools
293
+ */
294
+ netAPYGivenPools(pools: PoolInfoFull[]): number {
295
+ const weightedApy = pools.reduce((acc: number, curr) => {
296
+ const weight = curr.current_weight;
297
+ return acc + (curr.APY.netApy * weight);
298
+ }, 0);
299
+
300
+ return weightedApy;
301
+ }
302
+
303
+ /**
304
+ * Calculates optimal position changes to maximize APY while respecting max weights.
305
+ * The algorithm:
306
+ * 1. Sorts pools by APY (highest first)
307
+ * 2. Calculates target amounts based on max weights
308
+ * 3. For each pool that needs more funds:
309
+ * - Takes funds from lowest APY pools that are over their target
310
+ * 4. Validates that total assets remain constant
311
+ *
312
+ * @returns {Promise<{
313
+ * changes: Change[],
314
+ * finalPools: PoolInfoFull[],
315
+ * isAnyPoolOverMaxWeight: boolean
316
+ * }>} Object containing:
317
+ * - changes: Array of position changes
318
+ * - finalPools: Array of pool information after rebalance
319
+ * @throws Error if rebalance is not possible while maintaining constraints
320
+ */
321
+ async getRebalancedPositions() {
322
+ const { data: pools } = await this.getPools();
323
+ const totalAssets = (await this.getTVL()).amount;
324
+ if (totalAssets.eq(0)) return {
325
+ changes: [],
326
+ finalPools: [],
327
+ }
328
+
329
+ // assert sum of pools.amount <= totalAssets
330
+ const sumPools = pools.reduce((acc, curr) => acc.plus(curr.amount.toString()), Web3Number.fromWei("0", this.decimals()));
331
+ assert(sumPools.lte(totalAssets), 'Sum of pools.amount must be less than or equal to totalAssets');
332
+
333
+ // Sort pools by APY and calculate target amounts
334
+ const sortedPools = [...pools].sort((a, b) => b.APY.netApy - a.APY.netApy);
335
+ const targetAmounts: Record<string, Web3Number> = {};
336
+ let remainingAssets = totalAssets;
337
+
338
+ // First pass: Allocate to high APY pools up to their max weight
339
+ let isAnyPoolOverMaxWeight = false;
340
+ for (const pool of sortedPools) {
341
+ const maxAmount = totalAssets.multipliedBy(pool.max_weight * 0.9); // 10% tolerance
342
+ const targetAmount = remainingAssets.gte(maxAmount) ? maxAmount : remainingAssets;
343
+ targetAmounts[pool.pool_id.address.toString()] = targetAmount;
344
+ remainingAssets = remainingAssets.minus(targetAmount.toString());
345
+ if (pool.current_weight > pool.max_weight) {
346
+ isAnyPoolOverMaxWeight = true;
347
+ }
348
+ }
349
+
350
+ assert(remainingAssets.lt(0.00001), 'Remaining assets must be 0');
351
+
352
+ // Calculate required changes
353
+ const changes: Change[] = sortedPools.map(pool => {
354
+ const target = targetAmounts[pool.pool_id.address.toString()] || Web3Number.fromWei("0", this.decimals());
355
+ const change = Web3Number.fromWei(target.minus(pool.amount.toString()).toWei(), this.decimals());
356
+ return {
357
+ pool_id: pool.pool_id,
358
+ changeAmt: change,
359
+ finalAmt: target,
360
+ isDeposit: change.gt(0)
361
+ };
362
+ });
363
+
364
+ // Validate changes
365
+ const sumChanges = changes.reduce((sum, c) => sum.plus(c.changeAmt.toString()), Web3Number.fromWei("0", this.decimals()));
366
+ const sumFinal = changes.reduce((sum, c) => sum.plus(c.finalAmt.toString()), Web3Number.fromWei("0", this.decimals()));
367
+ const hasChanges = changes.some(c => !c.changeAmt.eq(0));
368
+
369
+ if (!sumChanges.eq(0)) throw new Error('Sum of changes must be zero');
370
+ if (!sumFinal.eq(totalAssets)) throw new Error('Sum of final amounts must equal total assets');
371
+ if (!hasChanges) throw new Error('No changes required');
372
+
373
+ const finalPools: PoolInfoFull[] = pools.map((p) => {
374
+ const target = targetAmounts[p.pool_id.address.toString()] || Web3Number.fromWei("0", this.decimals());
375
+ return {
376
+ ...p,
377
+ amount: target,
378
+ usdValue: Web3Number.fromWei("0", this.decimals()),
379
+ }
380
+ });
381
+ return {
382
+ changes,
383
+ finalPools,
384
+ isAnyPoolOverMaxWeight,
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Creates a rebalance Call object for the strategy contract
390
+ * @param pools - Array of pool information including IDs, weights, amounts, APYs and utilization
391
+ * @returns Populated contract call for rebalance
392
+ */
393
+ async getRebalanceCall(
394
+ pools: Awaited<ReturnType<typeof this.getRebalancedPositions>>['changes'],
395
+ isOverWeightAdjustment: boolean // here, yield increase doesnt matter
396
+ ) {
397
+ const actions: any[] = [];
398
+ // sort to put withdrawals first
399
+ pools.sort((a, b) => b.isDeposit ? -1 : 1);
400
+ console.log('pools', pools);
401
+ pools.forEach((p) => {
402
+ if (p.changeAmt.eq(0)) return null;
403
+ actions.push({
404
+ pool_id: p.pool_id.address,
405
+ feature: new CairoCustomEnum(p.isDeposit ? {DEPOSIT: {}} : {WITHDRAW: {}}),
406
+ token: this.asset().address,
407
+ amount: uint256.bnToUint256(p.changeAmt.multipliedBy(p.isDeposit ? 1 : -1).toWei()),
408
+ });
409
+ });
410
+ if (actions.length === 0) return null;
411
+ if (isOverWeightAdjustment) {
412
+ return this.contract.populate('rebalance_weights', [actions]);
413
+ }
414
+ return this.contract.populate('rebalance', [actions]);
415
+ }
416
+
417
+ async getInvestmentFlows(pools: PoolInfoFull[]) {
418
+ const netYield = this.netAPYGivenPools(pools);
419
+
420
+ const baseFlow: IInvestmentFlow = {
421
+ title: "Deposit $1000",
422
+ subItems: [`Net yield: ${(netYield * 100).toFixed(2)}%`],
423
+ linkedFlows: [],
424
+ };
425
+
426
+ pools.forEach((p) => {
427
+ if (p.amount.eq(0)) return;
428
+ const flow: IInvestmentFlow = {
429
+ title: `${p.pool_name} - $${(p.current_weight * 1000).toFixed(2)}`,
430
+ subItems: [
431
+ `APY: ${(p.APY.netApy * 100).toFixed(2)}%`,
432
+ `Weight: ${(p.current_weight * 100).toFixed(2)}% / ${(p.max_weight * 100).toFixed(2)}%`,
433
+ ],
434
+ linkedFlows: [],
435
+ };
436
+ baseFlow.linkedFlows.push(flow);
437
+ });
438
+
439
+ return [baseFlow];
440
+ }
441
+ }
442
+
443
+ const _description = 'Automatically diversify {{TOKEN}} holdings into different Vesu pools while reducing risk and maximizing yield. Defi spring STRK Rewards are auto-compounded as well.'
444
+ const _protocol: IProtocol = {name: 'Vesu', logo: 'https://static-assets-8zct.onrender.com/integrations/vesu/logo.png'}
445
+ // need to fine tune better
446
+ const _riskFactor: RiskFactor[] = [
447
+ {type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25},
448
+ {type: RiskType.TECHNICAL_RISK, value: 0.5, weight: 25},
449
+ {type: RiskType.COUNTERPARTY_RISK, value: 1, weight: 50},
450
+ ]
451
+ /**
452
+ * Represents the Vesu Rebalance Strategies.
453
+ */
454
+ export const VesuRebalanceStrategies: IStrategyMetadata[] = [{
455
+ name: 'Vesu STRK',
456
+ description: _description.replace('{{TOKEN}}', 'STRK'),
457
+ address: ContractAddr.from('0xeeb729d554ae486387147b13a9c8871bc7991d454e8b5ff570d4bf94de71e1'),
458
+ type: 'ERC4626',
459
+ depositTokens: [Global.getDefaultTokens().find(t => t.symbol === 'STRK')!],
460
+ protocols: [_protocol],
461
+ maxTVL: Web3Number.fromWei('0', 18),
462
+ risk: {
463
+ riskFactor: _riskFactor,
464
+ netRisk: _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) / 100,
465
+ }
466
+ }]
@@ -1,6 +1,3 @@
1
- export * from './store';
2
- export * from './encrypt';
3
-
4
1
  // Utility type to make all optional properties required
5
2
  export type RequiredFields<T> = {
6
3
  [K in keyof T]-?: T[K]
@@ -9,4 +6,10 @@ export type RequiredFields<T> = {
9
6
  // Utility type to get only the required fields of a type
10
7
  export type RequiredKeys<T> = {
11
8
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K
12
- }[keyof T]
9
+ }[keyof T]
10
+
11
+ export function assert(condition: boolean, message: string) {
12
+ if (!condition) {
13
+ throw new Error(message);
14
+ }
15
+ }