@strkfarm/sdk 2.0.0-dev.5 → 2.0.0-dev.51

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 (80) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +118889 -92229
  4. package/dist/index.browser.mjs +13381 -11153
  5. package/dist/index.d.ts +2284 -1938
  6. package/dist/index.js +13794 -11360
  7. package/dist/index.mjs +14253 -11843
  8. package/package.json +59 -60
  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 +280 -233
  19. package/src/index.browser.ts +2 -1
  20. package/src/interfaces/common.tsx +229 -6
  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 +99 -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 +2 -1
  28. package/src/modules/pragma.ts +23 -8
  29. package/src/modules/pricer-avnu-api.ts +114 -0
  30. package/src/modules/pricer-from-api.ts +159 -15
  31. package/src/modules/pricer-lst.ts +1 -1
  32. package/src/modules/pricer-quote-utils.ts +54 -0
  33. package/src/modules/pricer.ts +157 -54
  34. package/src/modules/pricerBase.ts +2 -1
  35. package/src/modules/zkLend.ts +3 -2
  36. package/src/node/deployer.ts +36 -1
  37. package/src/node/pricer-redis.ts +3 -1
  38. package/src/strategies/base-strategy.ts +168 -16
  39. package/src/strategies/constants.ts +8 -3
  40. package/src/strategies/ekubo-cl-vault.tsx +1048 -355
  41. package/src/strategies/factory.ts +199 -0
  42. package/src/strategies/index.ts +5 -3
  43. package/src/strategies/registry.ts +262 -0
  44. package/src/strategies/sensei.ts +354 -10
  45. package/src/strategies/svk-strategy.ts +292 -31
  46. package/src/strategies/token-boosted-xstrk-carry-strategy.tsx +1261 -0
  47. package/src/strategies/types.ts +4 -0
  48. package/src/strategies/universal-adapters/adapter-utils.ts +4 -1
  49. package/src/strategies/universal-adapters/avnu-adapter.ts +196 -272
  50. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  51. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  52. package/src/strategies/universal-adapters/index.ts +10 -8
  53. package/src/strategies/universal-adapters/svk-troves-adapter.ts +511 -0
  54. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  55. package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
  56. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +525 -0
  57. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +866 -860
  58. package/src/strategies/universal-adapters/vesu-position-common.ts +258 -0
  59. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  60. package/src/strategies/universal-lst-muliplier-strategy.tsx +895 -416
  61. package/src/strategies/universal-strategy.tsx +1332 -1173
  62. package/src/strategies/vesu-rebalance.tsx +254 -153
  63. package/src/strategies/yoloVault.ts +1096 -0
  64. package/src/utils/cacheClass.ts +11 -2
  65. package/src/utils/health-factor-math.ts +33 -1
  66. package/src/utils/index.ts +3 -1
  67. package/src/utils/logger.browser.ts +22 -4
  68. package/src/utils/logger.node.ts +259 -24
  69. package/src/utils/starknet-call-parser.ts +1036 -0
  70. package/src/utils/strategy-utils.ts +61 -0
  71. package/src/modules/ExtendedWrapperSDk/index.ts +0 -62
  72. package/src/modules/ExtendedWrapperSDk/types.ts +0 -311
  73. package/src/modules/ExtendedWrapperSDk/wrapper.ts +0 -395
  74. package/src/strategies/universal-adapters/extended-adapter.ts +0 -662
  75. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
  76. package/src/strategies/vesu-extended-strategy/services/operationService.ts +0 -34
  77. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +0 -77
  78. package/src/strategies/vesu-extended-strategy/utils/constants.ts +0 -49
  79. package/src/strategies/vesu-extended-strategy/utils/helper.ts +0 -372
  80. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +0 -1140
@@ -29,28 +29,36 @@ export class Harvests {
29
29
 
30
30
  async getUnHarvestedRewards(addr: ContractAddr) {
31
31
  const rewards = await this.getHarvests(addr);
32
+ logger.verbose(`${Harvests.name}: getUnHarvestedRewards => rewards length: ${rewards.length}`);
32
33
  if (rewards.length == 0) return [];
33
34
 
34
35
  const unClaimed: HarvestInfo[] = [];
35
36
 
36
37
  // use the latest one
37
- const reward = rewards.sort((a, b) => b.endDate.getTime() - a.endDate.getTime())[0];
38
-
39
- const cls = await this.config.provider.getClassAt(reward.rewardsContract.address);
40
- const contract = new Contract({abi: cls.abi, address: reward.rewardsContract.address, providerOrAccount: this.config.provider});
41
- const isClaimed = await contract.call('is_claimed', [reward.claim.id]);
42
- logger.verbose(`${Harvests.name}: isClaimed: ${isClaimed}`);
43
- if (isClaimed) {
44
- return unClaimed;
45
- }
46
- // rewards contract must have enough balance to claim
47
- const bal = await (new ERC20(this.config)).balanceOf(reward.token, reward.rewardsContract.address, 18);
48
- if (bal.lessThan(reward.claim.amount)) {
49
- logger.verbose(`${Harvests.name}: balance: ${bal.toString()}, amount: ${reward.claim.amount.toString()}`);
50
- return unClaimed;
38
+ const sortedRewards = rewards.sort((a, b) => b.endDate.getTime() - a.endDate.getTime());
39
+ if (sortedRewards.length == 0) {
40
+ logger.verbose(`${Harvests.name}: no rewards found`);
41
+ return [];
51
42
  }
52
43
 
53
- unClaimed.unshift(reward); // to ensure older harvest is first
44
+ const cls = await this.config.provider.getClassAt(sortedRewards[0].rewardsContract.address);
45
+
46
+ for (const reward of sortedRewards) {
47
+ const contract = new Contract({abi: cls.abi, address: reward.rewardsContract.address, providerOrAccount: this.config.provider});
48
+ const isClaimed = await contract.call('is_claimed', [reward.claim.id]);
49
+ logger.verbose(`${Harvests.name}: isClaimed: ${isClaimed}, claim id: ${reward.claim.id}, address: ${reward.rewardsContract.address}`);
50
+ if (isClaimed) {
51
+ continue;
52
+ }
53
+ // rewards contract must have enough balance to claim
54
+ const bal = await (new ERC20(this.config)).balanceOf(reward.token, reward.rewardsContract.address, 18);
55
+ if (bal.lessThan(reward.claim.amount)) {
56
+ logger.verbose(`${Harvests.name}: balance: ${bal.toString()}, amount: ${reward.claim.amount.toString()}`);
57
+ continue;
58
+ }
59
+
60
+ unClaimed.push(reward); // to ensure older harvest is first
61
+ }
54
62
  return unClaimed;
55
63
  }
56
64
  }
@@ -59,26 +67,28 @@ const STRK = '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'
59
67
 
60
68
  export class EkuboHarvests extends Harvests {
61
69
  async getHarvests(addr: ContractAddr) {
62
- const EKUBO_API = `https://starknet-mainnet-api.ekubo.org/airdrops/${addr.address}?token=${STRK}`
70
+ logger.verbose(`${EkuboHarvests.name}: getHarvests => addr: ${addr.address}`);
71
+ const EKUBO_API = `https://prod-api.ekubo.org/claims/${addr.address}`
63
72
  const resultEkubo = await fetch(EKUBO_API);
64
- const items = (await resultEkubo.json());
65
-
73
+ const data = (await resultEkubo.json());
74
+ const claims = data.claims || [];
75
+ logger.verbose(`${EkuboHarvests.name}: getHarvests => claims length: ${claims.length}`);
66
76
  const rewards: HarvestInfo[] = [];
67
- for (let i=0; i<items.length; ++i) {
68
- const info = items[i];
69
- assert(info.token == STRK, 'expected strk token only')
77
+ for (let i=0; i<claims.length; ++i) {
78
+ const claimData = claims[i];
79
+ assert(claimData.key.token == STRK, 'expected strk token only')
70
80
  rewards.push({
71
- rewardsContract: ContractAddr.from(info.contract_address),
81
+ rewardsContract: ContractAddr.from(claimData.dropAddress),
72
82
  token: ContractAddr.from(STRK),
73
- startDate: new Date(info.start_date),
74
- endDate: new Date(info.end_date),
83
+ startDate: new Date(0),
84
+ endDate: new Date(0),
75
85
  claim: {
76
- id: info.claim.id,
77
- amount: Web3Number.fromWei(info.claim.amount, 18),
78
- claimee: ContractAddr.from(info.claim.claimee)
86
+ id: claimData.claim.index,
87
+ amount: Web3Number.fromWei(claimData.claim.amount, 18),
88
+ claimee: ContractAddr.from(claimData.claim.account)
79
89
  },
80
- actualReward: Web3Number.fromWei(info.claim.amount, 18),
81
- proof: info.proof
90
+ actualReward: Web3Number.fromWei(claimData.claim.amount, 18),
91
+ proof: claimData.proof
82
92
  });
83
93
  }
84
94
  return rewards.sort((a, b) => b.endDate.getTime() - a.endDate.getTime());
@@ -101,6 +111,10 @@ export class VesuHarvests extends Harvests {
101
111
  logger.verbose(`${VesuHarvests.name}: claimed_amount: ${claimed_amount.toString()}`);
102
112
 
103
113
  const data = _data.data['defiSpring'];
114
+ if (!data) {
115
+ logger.verbose(`${VesuHarvests.name}: no defiSpring data found`);
116
+ return [];
117
+ }
104
118
 
105
119
  // get the actual reward
106
120
  const actualReward = Web3Number.fromWei(data.amount, 18).minus(claimed_amount);
@@ -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';
@@ -10,4 +11,4 @@ export * from './pricer-lst';
10
11
  export * from './lst-apr';
11
12
  export * from './pricerBase';
12
13
  export * from './midas';
13
- export * from './ExtendedWrapperSDk';
14
+ export * from './ekubo-pricer';
@@ -1,22 +1,37 @@
1
- import { Contract, RpcProvider } from "starknet";
1
+ import { Contract, RpcProvider, BlockIdentifier } from "starknet";
2
2
  import PragmaAbi from '@/data/pragma.abi.json';
3
3
  import { logger } from "@/utils/logger";
4
+ import { PricerBase } from "./pricerBase";
5
+ import { IConfig, TokenInfo } from "@/interfaces";
6
+ import { PriceInfo } from "./pricer";
4
7
 
5
- export class Pragma {
8
+ export class Pragma extends PricerBase {
6
9
  contractAddr = '0x023fb3afbff2c0e3399f896dcf7400acf1a161941cfb386e34a123f228c62832';
7
10
  readonly contract: Contract;
8
11
 
9
- constructor(provider: RpcProvider) {
10
- this.contract = new Contract({abi: PragmaAbi, address: this.contractAddr, providerOrAccount: provider});
12
+ constructor(config: IConfig, tokens: TokenInfo[]) {
13
+ super(config, tokens);
14
+ this.contract = new Contract({
15
+ abi: PragmaAbi,
16
+ address: this.contractAddr,
17
+ providerOrAccount: config.provider as RpcProvider
18
+ });
11
19
  }
12
20
 
13
- async getPrice(tokenAddr: string) {
21
+ async getPrice(tokenAddr: string, blockIdentifier: BlockIdentifier = 'latest'): Promise<PriceInfo> {
14
22
  if (!tokenAddr) {
15
23
  throw new Error(`Pragma:getPrice - no token`)
16
24
  }
17
- const result: any = await this.contract.call('get_price', [tokenAddr]);
25
+ const result: any = await this.contract.call(
26
+ 'get_price',
27
+ [tokenAddr],
28
+ { blockIdentifier }
29
+ );
18
30
  const price = Number(result.price) / 10**8;
19
- logger.verbose(`Pragma:${tokenAddr}: ${price}`);
20
- return price;
31
+ logger.verbose(`Pragma:${tokenAddr}: ${price} at block ${blockIdentifier}`);
32
+ return {
33
+ price,
34
+ timestamp: new Date()
35
+ };
21
36
  }
22
37
  }
@@ -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
+ }
@@ -1,21 +1,67 @@
1
- import { PriceInfo } from "./pricer";
1
+ import { PriceInfo } from "./pricer";
2
2
  import axios from "axios";
3
3
  import { IConfig, TokenInfo } from "@/interfaces";
4
4
  import { PricerBase } from "./pricerBase";
5
5
  import { logger } from "@/utils/logger";
6
+ import { createApolloClient } from "./apollo-client-config";
7
+ import { gql } from "@apollo/client";
8
+ import { ContractAddr } from "@/dataTypes";
9
+ import { Pragma } from "./pragma";
10
+ import { EkuboPricer } from "./ekubo-pricer";
11
+ import { BlockIdentifier } from "starknet";
6
12
 
7
13
  export class PricerFromApi extends PricerBase {
14
+ private apolloClient: ReturnType<typeof createApolloClient>;
15
+ private pragma: Pragma;
16
+ private ekuboPricer: EkuboPricer;
17
+
18
+ // Tokens supported by Pragma and GraphQL for historical prices (standardized addresses)
19
+ // Note: xSTRK is not supported by Pragma RPC, so it will use Ekubo pricer instead
20
+ private readonly PRAGMA_SUPPORTED_TOKENS = [
21
+ ContractAddr.from('0x3fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac').address, // WBTC
22
+ ContractAddr.from('0x53c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8').address, // USDC
23
+ ContractAddr.from('0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d').address, // STRK
24
+ // xSTRK removed - not supported by Pragma RPC, will use Ekubo pricer
25
+ // ContractAddr.from('0x28d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a').address, // xSTRK
26
+ ContractAddr.from('0x68f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8').address, // USDT
27
+ ContractAddr.from('0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7').address, // ETH
28
+ ];
29
+
8
30
  constructor(config: IConfig, tokens: TokenInfo[]) {
9
31
  super(config, tokens);
32
+ this.apolloClient = createApolloClient(config);
33
+ this.pragma = new Pragma(config, tokens);
34
+ this.ekuboPricer = new EkuboPricer(config, tokens, async (symbol) => {
35
+ const priceInfo = await this.getPriceFromMyAPI(symbol);
36
+ return priceInfo.price;
37
+ });
10
38
  }
11
39
 
12
- async getPrice(tokenSymbol: string): Promise<PriceInfo> {
40
+ async getPrice(tokenSymbol: string, blockNumber?: BlockIdentifier): Promise<PriceInfo> {
41
+ const tokenInfo = this.tokens.find(t => t.symbol === tokenSymbol);
42
+ if (!tokenInfo) {
43
+ throw new Error(`Token ${tokenSymbol} not found in configured tokens`);
44
+ }
45
+ const symbol = tokenInfo.priceProxySymbol || tokenInfo.symbol;
46
+ // If blockNumber is provided, fetch historical price from GraphQL
47
+ if (blockNumber !== undefined) {
48
+ try {
49
+ return await this.getHistoricalPrice(symbol, blockNumber);
50
+ } catch (e: any) {
51
+ logger.error(`Historical price fetch failed for ${tokenSymbol}: ${e.message}`);
52
+ throw new Error('Server Error in fetching historical price')
53
+ // Fall back to current price if historical price is not available
54
+ }
55
+ }
56
+
57
+ // Otherwise, fetch current price
13
58
  try {
14
- return await this.getPriceFromMyAPI(tokenSymbol);
59
+ return await this.getPriceFromMyAPI(symbol);
15
60
  } catch (e: any) {
16
- logger.warn('getPriceFromMyAPI error', e);
61
+ logger.warn(`API price fetch failed for ${tokenSymbol}: ${e.message}, trying coinbase`);
17
62
  }
18
- logger.info('getPrice coinbase', tokenSymbol);
63
+
64
+ logger.info(`Using Coinbase price for ${tokenSymbol}`);
19
65
  let retry = 0;
20
66
  const MAX_RETRIES = 5;
21
67
  for (retry = 1; retry < MAX_RETRIES + 1; retry++) {
@@ -41,22 +87,120 @@ export class PricerFromApi extends PricerBase {
41
87
  }
42
88
 
43
89
  async getPriceFromMyAPI(tokenSymbol: string) {
44
- logger.verbose(`getPrice from api: ${tokenSymbol}`);
45
90
  const endpoint = 'https://proxy.api.troves.fi'
46
91
  const url = `${endpoint}/api/price/${tokenSymbol}`;
47
92
  const priceInfoRes = await fetch(url);
48
93
  const priceInfo = await priceInfoRes.json();
49
- const now = new Date();
50
- const priceTime = new Date(priceInfo.timestamp);
51
- // if (now.getTime() - priceTime.getTime() > 900000) {
52
- // // 15 mins
53
- // logger.verbose(`Price is stale: ${tokenSymbol}, timestamp: ${priceInfo.timestamp}, price: ${priceInfo.price}`);
54
- // throw new Error('Price is stale');
55
- // }
56
94
  const price = Number(priceInfo.price);
57
95
  return {
58
96
  price,
59
97
  timestamp: new Date(priceInfo.timestamp)
60
98
  }
61
- }
62
- }
99
+ }
100
+
101
+ /**
102
+ * Fetches historical price for a token at a specific block number
103
+ * @param tokenSymbol - The token symbol to get price for
104
+ * @param blockNumber - The block number to query
105
+ * @returns PriceInfo with price at the closest block <= blockNumber
106
+ */
107
+ async getHistoricalPrice(tokenSymbol: string, blockNumber: BlockIdentifier): Promise<PriceInfo> {
108
+ const tokenInfo = this.tokens.find(t => t.symbol === tokenSymbol);
109
+ if (!tokenInfo) {
110
+ throw new Error(`Token ${tokenSymbol} not found in configured tokens`);
111
+ }
112
+
113
+ // If blockNumber is not a number, fall back to current price
114
+ if (typeof blockNumber !== 'number') {
115
+ logger.info(`Non-numeric blockIdentifier '${blockNumber}' provided, fetching current price for ${tokenSymbol}`);
116
+ return await this.getPriceFromMyAPI(tokenSymbol);
117
+ }
118
+
119
+ const standardizedAddress = ContractAddr.from(tokenInfo.address.address).address;
120
+ const isPragmaSupported = this.PRAGMA_SUPPORTED_TOKENS.includes(standardizedAddress);
121
+
122
+ // Step 1: Try GraphQL first for ALL tokens
123
+ let data;
124
+ try {
125
+ const result = await this.apolloClient.query({
126
+ query: gql`
127
+ query GetHistoricalPrice($asset: String!, $blockNumber: Int!) {
128
+ findFirstPrices(
129
+ where: {
130
+ asset: { equals: $asset },
131
+ block_number: { lte: $blockNumber }
132
+ }
133
+ orderBy: { block_number: desc }
134
+ ) {
135
+ price
136
+ timestamp
137
+ block_number
138
+ }
139
+ }
140
+ `,
141
+ variables: {
142
+ asset: standardizedAddress,
143
+ blockNumber: blockNumber,
144
+ },
145
+ });
146
+ data = result.data;
147
+ } catch (graphqlError: any) {
148
+ logger.error(`GraphQL query failed for ${tokenSymbol}: ${graphqlError.message}`);
149
+ }
150
+
151
+ // Step 2: If GraphQL has data, check block difference and use it if acceptable
152
+ if (data?.findFirstPrices) {
153
+ const priceData = data.findFirstPrices;
154
+ const blockDifference = blockNumber - priceData.block_number;
155
+ const MAX_BLOCK_DIFFERENCE = 6800; // ~6 hours worth of blocks
156
+
157
+ if (blockDifference <= MAX_BLOCK_DIFFERENCE) {
158
+ logger.info(`Using GraphQL price for ${tokenSymbol} at block ${priceData.block_number}`);
159
+ return {
160
+ price: Number(priceData.price),
161
+ timestamp: new Date(priceData.timestamp * 1000),
162
+ };
163
+ }
164
+
165
+ logger.info(
166
+ `Block difference ${blockDifference} exceeds limit for ${tokenSymbol}. ` +
167
+ `Will try fallback sources at block ${blockNumber}`
168
+ );
169
+ } else {
170
+ logger.info(`No GraphQL data for ${tokenSymbol} at block ${blockNumber}, trying fallback sources`);
171
+ }
172
+
173
+ // Step 3: Fallback to Pragma (for supported tokens) or Ekubo (for non-supported tokens)
174
+ if (isPragmaSupported) {
175
+ logger.info(`Attempting Pragma RPC fetch for ${tokenSymbol} at block ${blockNumber}`);
176
+ try {
177
+ const priceInfo = await this.pragma.getPrice(tokenInfo.address.address, blockNumber);
178
+ logger.info(`Using Pragma price for ${tokenSymbol}: ${priceInfo.price}`);
179
+ return {
180
+ price: priceInfo.price,
181
+ timestamp: priceInfo.timestamp,
182
+ };
183
+ } catch (error: any) {
184
+ logger.error(`Pragma RPC failed for ${tokenSymbol} at block ${blockNumber}: ${error.message}`);
185
+ throw new Error(`Server Error at Pragma token ${tokenSymbol} at block ${blockNumber}`);
186
+ }
187
+ } else {
188
+ // For non-Pragma tokens, use Ekubo pricer
189
+ logger.info(`Attempting Ekubo price fetch for ${tokenSymbol} at block ${blockNumber}`);
190
+ try {
191
+ const priceInfo = await this.ekuboPricer.getPrice(
192
+ tokenInfo.address.address,
193
+ blockNumber
194
+ );
195
+ logger.info(`Using Ekubo price for ${tokenSymbol}: ${priceInfo.price}`);
196
+ return {
197
+ price: priceInfo.price,
198
+ timestamp: priceInfo.timestamp,
199
+ };
200
+ } catch (error: any) {
201
+ logger.error(`Ekubo RPC failed for ${tokenSymbol} at block ${blockNumber}: ${error.message}`);
202
+ throw new Error(`Server Error at Ekubo token ${tokenSymbol} at block ${blockNumber}`);
203
+ }
204
+ }
205
+ }
206
+ }
@@ -6,7 +6,7 @@ import axios from "axios";
6
6
 
7
7
  export class PricerLST extends Pricer {
8
8
  private tokenMaps: {lst: TokenInfo, underlying: TokenInfo}[];
9
- protected EKUBO_API = 'https://quoter-mainnet-api.ekubo.org/{{AMOUNT}}/{{TOKEN_ADDRESS}}/{{UNDERLYING_ADDRESS}}'; // e.g. xSTRK/STRK
9
+ protected EKUBO_API = 'https://prod-api-quoter.ekubo.org/23448594291968334/{{AMOUNT}}/{{TOKEN_ADDRESS}}/{{UNDERLYING_ADDRESS}}'; // e.g. xSTRK/STRK
10
10
 
11
11
  constructor(config: IConfig, tokenMaps: {lst: TokenInfo, underlying: TokenInfo}[]) {
12
12
  const refreshInterval = 5000;
@@ -0,0 +1,54 @@
1
+ import { TokenInfo } from "@/interfaces/common";
2
+
3
+ export const PRICER_USDC_ADDRESS =
4
+ "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb";
5
+ export const PRICER_USDC_DECIMALS = 6;
6
+
7
+ export interface QuoteTokenConfig {
8
+ address: string;
9
+ decimals: number;
10
+ symbol: string;
11
+ isUsdQuote: boolean;
12
+ }
13
+
14
+ export function resolveQuoteTokenConfig(token: TokenInfo, tokens: TokenInfo[]): QuoteTokenConfig {
15
+ if (token.intermediateQuoteTokenSymbol) {
16
+ const intermediateToken = tokens.find(
17
+ (t) => t.symbol === token.intermediateQuoteTokenSymbol,
18
+ );
19
+ if (!intermediateToken) {
20
+ throw new Error(
21
+ `Intermediate quote token ${token.intermediateQuoteTokenSymbol} not found for ${token.symbol}`,
22
+ );
23
+ }
24
+
25
+ return {
26
+ address: intermediateToken.address.toString(),
27
+ decimals: intermediateToken.decimals,
28
+ symbol: intermediateToken.symbol,
29
+ isUsdQuote: false,
30
+ };
31
+ }
32
+
33
+ return {
34
+ address: PRICER_USDC_ADDRESS,
35
+ decimals: PRICER_USDC_DECIMALS,
36
+ symbol: "USDC",
37
+ isUsdQuote: true,
38
+ };
39
+ }
40
+
41
+ export async function convertQuoteAmountToUsd(
42
+ tokenSymbol: string,
43
+ quoteAmount: number,
44
+ quoteConfig: QuoteTokenConfig,
45
+ resolveUsdPrice: (symbol: string) => Promise<number>,
46
+ ): Promise<number> {
47
+ if (quoteConfig.isUsdQuote) {
48
+ const usdcPrice = await resolveUsdPrice("USDC");
49
+ return quoteAmount * usdcPrice;
50
+ }
51
+
52
+ const intermediateUsdPrice = await resolveUsdPrice(quoteConfig.symbol);
53
+ return quoteAmount * intermediateUsdPrice;
54
+ }