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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strkfarm/sdk",
3
- "version": "2.0.0-dev.40",
3
+ "version": "2.0.0-dev.41",
4
4
  "description": "STRKFarm TS SDK (Meant for our internal use, but feel free to use it)",
5
5
  "typings": "dist/index.d.ts",
6
6
  "types": "dist/index.d.ts",
@@ -38,9 +38,6 @@
38
38
  "keywords": [],
39
39
  "author": "",
40
40
  "license": "ISC",
41
- "publishConfig": {
42
- "tag": "dev"
43
- },
44
41
  "devDependencies": {
45
42
  "@types/jest": "^29.5.12",
46
43
  "@types/node-telegram-bot-api": "^0.64.7",
@@ -59,6 +56,9 @@
59
56
  "react": "19.1.2",
60
57
  "starknet": "9.2.1"
61
58
  },
59
+ "publishConfig": {
60
+ "tag": "staging"
61
+ },
62
62
  "dependencies": {
63
63
  "@apollo/client": "3.11.8",
64
64
  "@avnu/avnu-sdk": "3.0.2",
@@ -70,7 +70,6 @@
70
70
  "browser-assert": "^1.2.1",
71
71
  "chalk": "^4.1.2",
72
72
  "commander": "^12.1.0",
73
- "dotenv": "^17.2.3",
74
73
  "ethers": "^6.13.5",
75
74
  "graphql": "16.9.0",
76
75
  "inquirer": "^10.1.2",
package/src/global.ts CHANGED
@@ -48,7 +48,7 @@ const defaultTokens: TokenInfo[] = [
48
48
  coingeckId: undefined,
49
49
  priceCheckAmount: 0.1,
50
50
  displayDecimals: 6,
51
- }, {
51
+ }, {
52
52
  name: 'USDC.e',
53
53
  symbol: 'USDC.e',
54
54
  logo: 'https://assets.troves.fi/integrations/tokens/usdc.svg',
@@ -58,7 +58,7 @@ const defaultTokens: TokenInfo[] = [
58
58
  displayDecimals: 2,
59
59
  priceCheckAmount: 1000,
60
60
  priceProxySymbol: 'USDC'
61
- }, {
61
+ }, {
62
62
  name: 'USDC',
63
63
  symbol: 'USDC',
64
64
  logo: 'https://assets.troves.fi/integrations/tokens/usdc.svg',
@@ -169,7 +169,7 @@ const defaultTokens: TokenInfo[] = [
169
169
  displayDecimals: 6,
170
170
  priceCheckAmount: 0.001, // 112000 * 0.0001 = $110.2
171
171
  priceProxySymbol: 'WBTC',
172
- }, {
172
+ }, {
173
173
  name: 'xLBTC',
174
174
  symbol: 'xLBTC',
175
175
  logo: 'https://assets.strkfarm.com/integrations/tokens/xlbtc.svg',
@@ -191,19 +191,18 @@ const defaultTokens: TokenInfo[] = [
191
191
  coingeckId: undefined,
192
192
  displayDecimals: 6,
193
193
  priceCheckAmount: 0.0001, // 112000 * 0.0001 = $11.2
194
- },
195
- {
196
- name: "mRe7YIELD",
197
- symbol: "mRe7YIELD",
198
- logo: "https://midas.app/assets/mre7-BcOOHm7i.svg",
199
- address: ContractAddr.from(
200
- "0x4be8945e61dc3e19ebadd1579a6bd53b262f51ba89e6f8b0c4bc9a7e3c633fc"
201
- ),
194
+ dontPrice: true,
195
+ }, {
196
+ name: 'mRe7YIELD',
197
+ symbol: 'mRe7YIELD',
198
+ logo: 'https://midas.app/assets/mre7-BcOOHm7i.svg',
199
+ address: ContractAddr.from('0x4be8945e61dc3e19ebadd1579a6bd53b262f51ba89e6f8b0c4bc9a7e3c633fc'),
202
200
  decimals: 18,
203
201
  coingeckId: undefined,
204
202
  displayDecimals: 2,
205
203
  priceCheckAmount: 100,
206
- }, {
204
+ dontPrice: true,
205
+ }, {
207
206
  name: "fyWBTC",
208
207
  symbol: "fyWBTC",
209
208
  logo: 'https://assets.strkfarm.com/integrations/tokens/wbtc.svg',
@@ -212,7 +211,8 @@ const defaultTokens: TokenInfo[] = [
212
211
  coingeckId: undefined,
213
212
  displayDecimals: 6,
214
213
  priceCheckAmount: 0.001, // 112000 * 0.0001 = $110.2
215
- }, {
214
+ dontPrice: true,
215
+ }, {
216
216
  name: "fyETH",
217
217
  symbol: "fyETH",
218
218
  logo: 'https://assets.strkfarm.com/integrations/tokens/eth.svg',
@@ -221,7 +221,8 @@ const defaultTokens: TokenInfo[] = [
221
221
  coingeckId: undefined,
222
222
  displayDecimals: 4,
223
223
  priceCheckAmount: 0.1,
224
- }, {
224
+ dontPrice: true,
225
+ }, {
225
226
  name: "fyUSDC",
226
227
  symbol: "fyUSDC",
227
228
  logo: 'https://assets.strkfarm.com/integrations/tokens/usdc.svg',
@@ -230,7 +231,8 @@ const defaultTokens: TokenInfo[] = [
230
231
  coingeckId: undefined,
231
232
  displayDecimals: 2,
232
233
  priceCheckAmount: 100,
233
- }, {
234
+ dontPrice: true,
235
+ }, {
234
236
  name: 'strkBTC',
235
237
  symbol: 'strkBTC',
236
238
  logo: 'https://assets.troves.fi/integrations/tokens/strkbtc.svg',
@@ -240,7 +242,7 @@ const defaultTokens: TokenInfo[] = [
240
242
  displayDecimals: 6,
241
243
  priceCheckAmount: 0.001, // 112000 * 0.0001 = $110.2
242
244
  priceProxySymbol: 'WBTC',
243
- }, {
245
+ }, {
244
246
  name: 'xstrkBTC',
245
247
  symbol: 'xstrkBTC',
246
248
  logo: 'https://assets.troves.fi/integrations/tokens/xstrkbtc.svg',
@@ -250,7 +252,7 @@ const defaultTokens: TokenInfo[] = [
250
252
  displayDecimals: 6,
251
253
  priceCheckAmount: 0.001,
252
254
  priceProxySymbol: 'WBTC',
253
- },]
255
+ },]
254
256
  const tokens: TokenInfo[] = defaultTokens;
255
257
 
256
258
  /** Contains globally useful functions.
@@ -333,14 +335,14 @@ export class Global {
333
335
  static async getTokenInfoFromName(tokenName: string) {
334
336
  // if tokens are not loaded, load them
335
337
  if (tokens.length == defaultTokens.length) {
336
- await Global.getTokens();
338
+ await Global.getTokens();
337
339
  }
338
340
 
339
341
  const token = tokens.find(
340
- (token) => token.name.toLowerCase() === tokenName.toLowerCase()
342
+ (token) => token.name.toLowerCase() === tokenName.toLowerCase()
341
343
  );
342
344
  if (!token) {
343
- throw new FatalError(`Token not found: ${tokenName}`);
345
+ throw new FatalError(`Token not found: ${tokenName}`);
344
346
  }
345
347
  return token;
346
348
  }
@@ -348,30 +350,30 @@ export class Global {
348
350
  static async getTokenInfoFromAddr(addr: ContractAddr) {
349
351
  // if tokens are not loaded, load them
350
352
  if (tokens.length == defaultTokens.length) {
351
- await Global.getTokens();
353
+ await Global.getTokens();
352
354
  }
353
355
  const token = tokens.find((token) => addr.eq(token.address));
354
356
  if (!token) {
355
- throw new FatalError(`Token not found: ${addr.address}`);
357
+ throw new FatalError(`Token not found: ${addr.address}`);
356
358
  }
357
359
  return token;
358
360
  }
359
361
 
360
362
  static setGlobalCache(key: string, data: any, ttl: number = 60000) {
361
- Global.cache[key] = {
362
- value: data,
363
- ttl,
364
- timestamp: Date.now()
365
- };
363
+ Global.cache[key] = {
364
+ value: data,
365
+ ttl,
366
+ timestamp: Date.now()
367
+ };
366
368
  }
367
369
 
368
370
  static getGlobalCache<T>(key: string): T | null {
369
- const cached = Global.cache[key];
370
- if (!cached) return null;
371
- if (Date.now() - cached.timestamp > cached.ttl) {
372
- delete Global.cache[key];
373
- return null;
374
- }
375
- return cached.value;
371
+ const cached = Global.cache[key];
372
+ if (!cached) return null;
373
+ if (Date.now() - cached.timestamp > cached.ttl) {
374
+ delete Global.cache[key];
375
+ return null;
376
+ }
377
+ return cached.value;
376
378
  }
377
379
  }
@@ -35,6 +35,7 @@ export interface TokenInfo {
35
35
  displayDecimals: number;
36
36
  priceProxySymbol?: string; // for tokens like illiquid tokens, we use a proxy symbol to get the price
37
37
  priceCheckAmount?: number; // for tokens like BTC, doing 1BTC price check may not be ideal, esp on illiquid netwrks like sn
38
+ dontPrice?: boolean; // a flag that skips pricer to check these tokens.
38
39
  }
39
40
 
40
41
  export enum Network {
@@ -174,6 +175,10 @@ export interface StrategyApyHistoryUIConfig {
174
175
  noApyHistoryMessage?: string;
175
176
  }
176
177
 
178
+ export interface FeeBps {
179
+ performanceFeeBps: number;
180
+ }
181
+
177
182
  /**
178
183
  * @property risk.riskFactor.factor - The risk factors that are considered for the strategy.
179
184
  * @property risk.riskFactor.factor - The value of the risk factor from 0 to 10, 0 being the lowest and 10 being the highest.
@@ -214,6 +219,7 @@ export interface IStrategyMetadata<T> {
214
219
  };
215
220
  apyMethodology?: string;
216
221
  realizedApyMethodology?: string;
222
+ feeBps?: FeeBps;
217
223
  additionalInfo: T;
218
224
  contractDetails: {
219
225
  address: ContractAddr;
@@ -1,5 +1,6 @@
1
1
  export * from './token-market-data';
2
2
  export * from './pricer';
3
+ export * from './pricer-avnu-api';
3
4
  export * from './pragma';
4
5
  export * from './zkLend';
5
6
  export * from './pricer-from-api';
@@ -0,0 +1,114 @@
1
+ import axios from "axios";
2
+ import { TokenInfo } from "@/interfaces/common";
3
+ import { IConfig } from "@/interfaces/common";
4
+ import { PricerBase } from "./pricerBase";
5
+ import { PriceInfo } from "./pricer";
6
+ import { logger } from "@/utils/logger";
7
+ import { ContractAddr } from "@/dataTypes";
8
+
9
+ const AVNU_TOKENS_API = "https://starknet.impulse.avnu.fi/v3/tokens";
10
+
11
+ interface AvnuTokenApiEntry {
12
+ address: string;
13
+ symbol: string;
14
+ starknet?: {
15
+ usd?: number;
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Polls Avnu impulse tokens API and keeps USD prices in memory for configured tokens.
21
+ * Price timestamp is set when each poll request completes.
22
+ */
23
+ export class PricerAvnuApi extends PricerBase {
24
+ protected prices: { [key: string]: PriceInfo } = {};
25
+
26
+ readonly refreshInterval = 15_000;
27
+ readonly staleTime = 5 * 60 * 1000;
28
+
29
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
30
+ private loading = false;
31
+
32
+ constructor(config: IConfig, tokens: TokenInfo[]) {
33
+ super(config, tokens);
34
+ }
35
+
36
+ start() {
37
+ this._loadPrices();
38
+ this.pollTimer = setInterval(() => {
39
+ this._loadPrices();
40
+ }, this.refreshInterval);
41
+ }
42
+
43
+ stop() {
44
+ if (this.pollTimer) {
45
+ clearInterval(this.pollTimer);
46
+ this.pollTimer = null;
47
+ }
48
+ }
49
+
50
+ isStale(timestamp: Date) {
51
+ return Date.now() - timestamp.getTime() > this.staleTime;
52
+ }
53
+
54
+ hasPrice(tokenSymbol: string) {
55
+ const info = this.prices[tokenSymbol];
56
+ return !!info && !this.isStale(info.timestamp);
57
+ }
58
+
59
+ async getPrice(tokenSymbol: string): Promise<PriceInfo> {
60
+ const info = this.prices[tokenSymbol];
61
+ if (!info) {
62
+ throw new Error(`AvnuApi: price of ${tokenSymbol} not found`);
63
+ }
64
+ if (this.isStale(info.timestamp)) {
65
+ throw new Error(`AvnuApi: price of ${tokenSymbol} is stale`);
66
+ }
67
+ return info;
68
+ }
69
+
70
+ protected async _loadPrices() {
71
+ if (this.loading) {
72
+ return;
73
+ }
74
+ this.loading = true;
75
+ const timestamp = new Date();
76
+ try {
77
+ const result = await axios.get<AvnuTokenApiEntry[]>(AVNU_TOKENS_API);
78
+ const priceByAddress = new Map<string, number>();
79
+ for (const entry of result.data) {
80
+ const usd = entry.starknet?.usd;
81
+ if (usd != null && usd > 0) {
82
+ priceByAddress.set(ContractAddr.standardise(entry.address), usd);
83
+ }
84
+ }
85
+
86
+ for (const token of this.tokens) {
87
+ if (token.symbol === "USDT" || token.symbol === "USDC") {
88
+ this.prices[token.symbol] = { price: 1, timestamp };
89
+ continue;
90
+ }
91
+
92
+ const targetToken = token.priceProxySymbol
93
+ ? this.tokens.find((t) => t.symbol === token.priceProxySymbol)
94
+ : token;
95
+ if (!targetToken) {
96
+ continue;
97
+ }
98
+
99
+ const addr = targetToken.address.address;
100
+ const price = priceByAddress.get(addr);
101
+ if (price != null) {
102
+ this.prices[token.symbol] = { price, timestamp };
103
+ logger.verbose(
104
+ `AvnuApi: ${token.symbol} -> $${price}`,
105
+ );
106
+ }
107
+ }
108
+ } catch (error: any) {
109
+ logger.warn(`AvnuApi: failed to fetch tokens: ${error?.message ?? error}`);
110
+ } finally {
111
+ this.loading = false;
112
+ }
113
+ }
114
+ }
@@ -7,12 +7,23 @@ import { PricerBase } from "./pricerBase";
7
7
  import { logger } from "@/utils/logger";
8
8
  import { AvnuWrapper } from "./avnu";
9
9
  import { BlockIdentifier } from "starknet";
10
+ import { PricerAvnuApi } from "./pricer-avnu-api";
10
11
 
11
12
  export interface PriceInfo {
12
13
  price: number,
13
14
  timestamp: Date
14
15
  }
15
16
 
17
+ type PriceMethod = 'AvnuApi' | 'Coinbase' | 'Coinmarketcap' | 'Ekubo' | 'Avnu';
18
+
19
+ const PRICE_METHOD_PRIORITY: PriceMethod[] = [
20
+ 'AvnuApi',
21
+ 'Coinbase',
22
+ 'Coinmarketcap',
23
+ 'Ekubo',
24
+ 'Avnu',
25
+ ];
26
+
16
27
  export class Pricer extends PricerBase {
17
28
  protected prices: {
18
29
  [key: string]: PriceInfo
@@ -21,9 +32,11 @@ export class Pricer extends PricerBase {
21
32
  refreshInterval = 30000;
22
33
  staleTime = 60000;
23
34
 
35
+ protected readonly avnuApiPricer: PricerAvnuApi;
36
+
24
37
  // code populates this map during runtime to determine which method to use for a given token
25
38
  // The method set will be the first one to try after first attempt
26
- protected methodToUse: {[tokenSymbol: string]: 'Ekubo' | 'Coinbase' | 'Coinmarketcap' | 'Avnu'} = {};
39
+ protected methodToUse: {[tokenSymbol: string]: PriceMethod} = {};
27
40
 
28
41
  /**
29
42
  * TOKENA and TOKENB are the two token names to get price of TokenA in terms of TokenB
@@ -36,6 +49,7 @@ export class Pricer extends PricerBase {
36
49
  super(config, tokens);
37
50
  this.refreshInterval = refreshInterval;
38
51
  this.staleTime = staleTime;
52
+ this.avnuApiPricer = new PricerAvnuApi(config, tokens);
39
53
  }
40
54
 
41
55
  isReady() {
@@ -69,6 +83,7 @@ export class Pricer extends PricerBase {
69
83
  }
70
84
 
71
85
  start() {
86
+ this.avnuApiPricer.start();
72
87
  this._loadPrices();
73
88
  setInterval(() => {
74
89
  this._loadPrices();
@@ -96,6 +111,9 @@ export class Pricer extends PricerBase {
96
111
  let retry = 0;
97
112
  while (retry < MAX_RETRIES) {
98
113
  try {
114
+ if (token.dontPrice) {
115
+ return;
116
+ }
99
117
  if (token.symbol === 'USDT' || token.symbol === 'USDC') {
100
118
  this.prices[token.symbol] = {
101
119
  price: 1,
@@ -145,56 +163,56 @@ export class Pricer extends PricerBase {
145
163
  }
146
164
  }
147
165
 
148
- async _getPrice(token: TokenInfo, defaultMethod = 'all'): Promise<number> {
149
- const methodToUse: string = this.methodToUse[token.symbol] || defaultMethod; // default start with coinbase
150
- logger.verbose(`Fetching price of ${token.symbol} using ${methodToUse}`);
151
- switch (methodToUse) {
166
+ async _getPrice(token: TokenInfo): Promise<number> {
167
+ const pinned = this.methodToUse[token.symbol];
168
+ if (pinned) {
169
+ logger.verbose(`Fetching price of ${token.symbol} using pinned ${pinned}`);
170
+ try {
171
+ return await this._tryPriceMethod(token, pinned);
172
+ } catch (error: any) {
173
+ console.warn(`${pinned}: pinned price failed [${token.symbol}]: `, error.message);
174
+ delete this.methodToUse[token.symbol];
175
+ }
176
+ }
177
+
178
+ for (const method of PRICE_METHOD_PRIORITY) {
179
+ logger.verbose(`Fetching price of ${token.symbol} using ${method}`);
180
+ try {
181
+ const result = await this._tryPriceMethod(token, method);
182
+ this.methodToUse[token.symbol] = method;
183
+ return result;
184
+ } catch (error: any) {
185
+ console.warn(`${method}: price err [${token.symbol}]: `, error.message);
186
+ }
187
+ }
188
+
189
+ throw new FatalError(`Price not found for ${token.symbol}`);
190
+ }
191
+
192
+ protected async _tryPriceMethod(token: TokenInfo, method: PriceMethod): Promise<number> {
193
+ switch (method) {
194
+ case 'AvnuApi':
195
+ return await this._getPriceAvnuApi(token);
152
196
  case 'Coinbase':
153
- // try {
154
- // const result = await this._getPriceCoinbase(token);
155
- // this.methodToUse[token.symbol] = 'Coinbase';
156
- // return result;
157
- // } catch (error: any) {
158
- // console.warn(`Coinbase: price err: message [${token.symbol}]: `, error.message);
159
- // // do nothing, try next
160
- // }
197
+ return await this._getPriceCoinbase(token);
161
198
  case 'Coinmarketcap':
162
- try {
163
- const result = await this._getPriceCoinMarketCap(token);
164
- this.methodToUse[token.symbol] = 'Coinmarketcap';
165
- return result;
166
- } catch (error: any) {
167
- console.warn(`CoinMarketCap: price err [${token.symbol}]: `, Object.keys(error));
168
- console.warn(`CoinMarketCap: price err [${token.symbol}]: `, error.message);
169
- }
199
+ return await this._getPriceCoinMarketCap(token);
170
200
  case 'Ekubo':
171
- try {
172
- const result = await this._getPriceEkubo(token, new Web3Number(token.priceCheckAmount ? token.priceCheckAmount : 1, token.decimals));
173
- this.methodToUse[token.symbol] = 'Ekubo';
174
- return result;
175
- } catch (error: any) {
176
- console.warn(`Ekubo: price err [${token.symbol}]: `, error.message);
177
- console.warn(`Ekubo: price err [${token.symbol}]: `, Object.keys(error));
178
- // do nothing, try next
179
- }
201
+ return await this._getPriceEkubo(
202
+ token,
203
+ new Web3Number(token.priceCheckAmount ? token.priceCheckAmount : 1, token.decimals),
204
+ );
180
205
  case 'Avnu':
181
- try {
182
- const result = await this._getAvnuPrice(token, new Web3Number(token.priceCheckAmount ? token.priceCheckAmount : 1, token.decimals));
183
- this.methodToUse[token.symbol] = 'Avnu';
184
- return result;
185
- } catch (error: any) {
186
- console.warn(`Avnu: price err [${token.symbol}]: `, error.message);
187
- console.warn(`Avnu: price err [${token.symbol}]: `, Object.keys(error));
188
- }
189
- }
190
-
191
- // if methodToUse is the default one, pass Coinbase to try all from start
192
- if (defaultMethod == 'all') {
193
- // try again with coinbase
194
- return await this._getPrice(token, 'Coinbase');
206
+ return await this._getAvnuPrice(
207
+ token,
208
+ new Web3Number(token.priceCheckAmount ? token.priceCheckAmount : 1, token.decimals),
209
+ );
195
210
  }
211
+ }
196
212
 
197
- throw new FatalError(`Price not found for ${token.symbol}`);
213
+ async _getPriceAvnuApi(token: TokenInfo): Promise<number> {
214
+ const priceInfo = await this.avnuApiPricer.getPrice(token.symbol);
215
+ return priceInfo.price;
198
216
  }
199
217
 
200
218
  async _getPriceCoinbase(token: TokenInfo) {
@@ -18,6 +18,7 @@ export class PricerRedis extends Pricer {
18
18
  await this.initRedis(redisUrl);
19
19
 
20
20
  logger.info(`Starting Pricer with Redis`);
21
+ this.avnuApiPricer.start();
21
22
  this._loadPrices(this._setRedisPrices.bind(this));
22
23
  setInterval(() => {
23
24
  this._loadPrices(this._setRedisPrices.bind(this));
@@ -2452,6 +2452,9 @@ const xSTRKSTRK: IStrategyMetadata<CLVaultStrategySettings> = {
2452
2452
  apyMethodology:
2453
2453
  "APY based on 30-day historical performance, including fees and rewards.",
2454
2454
  realizedApyMethodology: "The realizedAPY is based on past 14 days performance by the vault",
2455
+ feeBps: {
2456
+ performanceFeeBps: 1000,
2457
+ },
2455
2458
  additionalInfo: {
2456
2459
  newBounds: {
2457
2460
  lower: -1,