@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
@@ -0,0 +1,28 @@
1
+ import { ApolloClient, InMemoryCache } from '@apollo/client';
2
+ import { IConfig } from '@/interfaces';
3
+
4
+ /**
5
+ * Creates an Apollo Client instance configured for the appropriate environment
6
+ * @param config - The application config containing network and stage information
7
+ * @returns Configured Apollo Client instance
8
+ */
9
+ export function createApolloClient(config: IConfig) {
10
+ // Determine the URI based on the environment
11
+ const uri = config.stage === 'production'
12
+ ? 'https://api.troves.fi/'
13
+ : 'http://localhost:4000';
14
+
15
+ return new ApolloClient({
16
+ uri,
17
+ cache: new InMemoryCache(),
18
+ });
19
+ }
20
+
21
+ // Default client for backward compatibility
22
+ const apolloClient = new ApolloClient({
23
+ uri: 'https://api.troves.fi/',
24
+ cache: new InMemoryCache(),
25
+ });
26
+
27
+ export default apolloClient;
28
+
@@ -37,7 +37,7 @@ export class AvnuWrapper {
37
37
  excludeSources = ['Haiko(Solvers)']
38
38
  ): Promise<Quote> {
39
39
  const MAX_RETRY = 5;
40
- logger.verbose(`${AvnuWrapper.name}: getQuotes => Getting quotes for ${fromToken} -> ${toToken}, amount: ${amountWei}, taker: ${taker}, retry: ${retry}`);
40
+ // logger.verbose(`${AvnuWrapper.name}: getQuotes => Getting quotes for ${fromToken} -> ${toToken}, amount: ${amountWei}, taker: ${taker}, retry: ${retry}`);
41
41
  const params: any = {
42
42
  sellTokenAddress: fromToken,
43
43
  buyTokenAddress: toToken,
@@ -100,9 +100,9 @@ export class AvnuWrapper {
100
100
  // swapInfo as expected by the strategy
101
101
  // fallback, max 1% slippage
102
102
  const _minAmount = minAmount || (quote.buyAmount * 95n / 100n).toString();
103
- logger.verbose(`${AvnuWrapper.name}: getSwapInfo => sellToken: ${quote.sellTokenAddress}, sellAmount: ${quote.sellAmount}`);
104
- logger.verbose(`${AvnuWrapper.name}: getSwapInfo => buyToken: ${quote.buyTokenAddress}`);
105
- logger.verbose(`${AvnuWrapper.name}: getSwapInfo => buyAmount: ${quote.buyAmount}, minAmount: ${_minAmount}`);
103
+ // logger.verbose(`${AvnuWrapper.name}: getSwapInfo => sellToken: ${quote.sellTokenAddress}, sellAmount: ${quote.sellAmount}`);
104
+ // logger.verbose(`${AvnuWrapper.name}: getSwapInfo => buyToken: ${quote.buyTokenAddress}`);
105
+ // logger.verbose(`${AvnuWrapper.name}: getSwapInfo => buyAmount: ${quote.buyAmount}, minAmount: ${_minAmount}`);
106
106
  const swapInfo: SwapInfo = {
107
107
  token_from_address: quote.sellTokenAddress,
108
108
  token_from_amount: uint256.bnToUint256(quote.sellAmount),
@@ -0,0 +1,79 @@
1
+ import { Contract, RpcProvider, BlockIdentifier } from "starknet";
2
+ import EkuboPricerAbi from '@/data/ekubo-price-fethcer.abi.json';
3
+ import { PricerBase } from "./pricerBase";
4
+ import { IConfig, TokenInfo } from "@/interfaces";
5
+ import { PriceInfo } from "./pricer";
6
+
7
+ export class EkuboPricer extends PricerBase {
8
+ EKUBO_PRICE_FETCHER_ADDRESS = '0x04946fb4ad5237d97bbb1256eba2080c4fe1de156da6a7f83e3b4823bb6d7da1';
9
+ readonly contract: Contract;
10
+ private readonly USDC_ADDRESS = '0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8';
11
+ private readonly USDC_DECIMALS = 6;
12
+
13
+ constructor(config: IConfig, tokens: TokenInfo[]) {
14
+ super(config, tokens);
15
+ this.contract = new Contract({
16
+ abi: EkuboPricerAbi,
17
+ address: this.EKUBO_PRICE_FETCHER_ADDRESS,
18
+ providerOrAccount: config.provider as RpcProvider
19
+ });
20
+ }
21
+
22
+ private div2Power128(num: bigint): number {
23
+ return Number((num * BigInt(1e18)) / BigInt(2 ** 128)) / 1e18;
24
+ }
25
+
26
+ async getPrice(tokenAddr: string, blockIdentifier: BlockIdentifier = 'latest'): Promise<PriceInfo> {
27
+ if (!tokenAddr) {
28
+ throw new Error(`EkuboPricer:getPrice - no token`);
29
+ }
30
+
31
+ // get_prices arguments in order:
32
+ // - quote_token: USDC address (quote token for price calculation)
33
+ // - base_tokens: array containing the base token address/addresses
34
+ // - period: time period in seconds for TWAP (3600 = 1 hour)
35
+ // - min_token: minimum token amount threshold (min liquidity) in 6 Decimals = 1000000)
36
+ const result: any = await this.contract.call(
37
+ 'get_prices',
38
+ [this.USDC_ADDRESS, [tokenAddr], 3600, 1000000],
39
+ { blockIdentifier }
40
+ );
41
+
42
+ if (!result || result.length === 0) {
43
+ throw new Error(`EkuboPricer: No price result returned for ${tokenAddr}`);
44
+ }
45
+
46
+ const priceResult = result[0];
47
+
48
+ if (!priceResult?.variant?.Price) {
49
+ const variant = priceResult?.variant ? Object.keys(priceResult.variant)[0] : 'Unknown';
50
+ throw new Error(`EkuboPricer: Price fetch failed with variant: ${variant}`);
51
+ }
52
+
53
+ const rawPrice = typeof priceResult.variant.Price === 'string'
54
+ ? BigInt(priceResult.variant.Price)
55
+ : priceResult.variant.Price;
56
+
57
+ // Get token info to determine decimals from configured tokens
58
+ const tokenInfo = this.tokens.find(t =>
59
+ t.address.address.toLowerCase() === tokenAddr.toLowerCase()
60
+ );
61
+
62
+ if (!tokenInfo) {
63
+ throw new Error(`Token ${tokenAddr} not found in global tokens`);
64
+ }
65
+
66
+ // Convert from x128 format
67
+ const priceAfterX128 = this.div2Power128(rawPrice);
68
+
69
+ // Adjust for token decimals
70
+ const decimalAdjustment = 10 ** (tokenInfo.decimals - this.USDC_DECIMALS);
71
+ const price = priceAfterX128 * decimalAdjustment;
72
+
73
+ return {
74
+ price,
75
+ timestamp: new Date()
76
+ };
77
+ }
78
+ }
79
+
@@ -41,36 +41,55 @@ export class EkuboQuoter {
41
41
  this.tokenMarketData = new TokenMarketData(pricer, config);
42
42
  }
43
43
 
44
- /**
45
- *
46
- * @param fromToken
47
- * @param toToken
48
- * @param amount Can be negative too, which would mean to get exact amount out
49
- * @returns
50
- */
51
- async getQuote(fromToken: string, toToken: string, amount: Web3Number, retry = 0): Promise<EkuboQuote> {
52
- // let _fromToken = amount.gt(0) ? fromToken : toToken;
53
- // let _toToken = amount.gt(0) ? toToken : fromToken;
54
-
44
+ private async _callQuoterApi(fromToken: string, toToken: string, amount: Web3Number, retry = 0): Promise<EkuboQuote> {
55
45
  try {
56
- const url = this.ENDPOINT.replace("{{AMOUNT}}", amount.toFixed(0)).replace("{{TOKEN_FROM_ADDRESS}}", fromToken).replace("{{TOKEN_TO_ADDRESS}}", toToken);
57
- console.log("url", url);
46
+ const url = this.ENDPOINT.replace("{{AMOUNT}}", amount.toWei()).replace("{{TOKEN_FROM_ADDRESS}}", fromToken).replace("{{TOKEN_TO_ADDRESS}}", toToken);
47
+ logger.verbose(`EkuboQuoter::_callQuoterApi url: ${url}`);
58
48
  const quote = await axios.get(url);
59
- return quote.data as EkuboQuote;
49
+ return quote.data as EkuboQuote;
60
50
  } catch (error: any) {
61
- logger.error(`${error.message} dassf ${error.data}`);
51
+ logger.error(`EkuboQuoter::_callQuoterApi error: ${error.message}`);
62
52
  if (retry < 3) {
63
53
  await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 5000));
64
- return await this.getQuote(fromToken, toToken, amount, retry + 1);
54
+ return await this._callQuoterApi(fromToken, toToken, amount, retry + 1);
65
55
  }
66
56
  throw error;
67
57
  }
68
58
  }
69
59
 
60
+ /**
61
+ * Given exactly `inputAmount` of `fromToken`, how much `toToken` do I receive?
62
+ * @param fromToken - address of the token being sold
63
+ * @param toToken - address of the token being bought
64
+ * @param inputAmount - must be positive (the amount of fromToken to sell)
65
+ * @returns EkuboQuote where `total_calculated` is the output amount (positive)
66
+ */
67
+ async getQuoteExactInput(fromToken: string, toToken: string, inputAmount: Web3Number): Promise<EkuboQuote> {
68
+ if (inputAmount.isNegative() || inputAmount.isZero()) {
69
+ throw new Error(`EkuboQuoter::getQuoteExactInput inputAmount must be positive, got ${inputAmount.toFixed()}`);
70
+ }
71
+ return this._callQuoterApi(fromToken, toToken, inputAmount);
72
+ }
73
+
74
+ /**
75
+ * To receive exactly `outputAmount` of `toToken`, how much `fromToken` must I provide?
76
+ * @param fromToken - address of the token being sold
77
+ * @param toToken - address of the token being bought
78
+ * @param outputAmount - must be positive (the desired amount of toToken to receive)
79
+ * @returns EkuboQuote where `total_calculated` is the required input amount (negative per Ekubo convention)
80
+ */
81
+ async getQuoteExactOutput(fromToken: string, toToken: string, outputAmount: Web3Number): Promise<EkuboQuote> {
82
+ if (outputAmount.isNegative() || outputAmount.isZero()) {
83
+ throw new Error(`EkuboQuoter::getQuoteExactOutput outputAmount must be positive, got ${outputAmount.toFixed()}`);
84
+ }
85
+ const negatedAmount = new Web3Number(outputAmount.multipliedBy(-1).toFixed(Math.min(outputAmount.decimals, 15)), outputAmount.decimals);
86
+ return this._callQuoterApi(toToken, fromToken, negatedAmount);
87
+ }
88
+
70
89
  async getDexPrice(baseToken: TokenInfo, quoteToken: TokenInfo, amount: Web3Number) {
71
90
  const lstTokenInfo = baseToken;
72
91
  const lstUnderlyingTokenInfo = quoteToken;
73
- const quote = await this.getQuote(
92
+ const quote = await this.getQuoteExactInput(
74
93
  lstTokenInfo.address.address,
75
94
  lstUnderlyingTokenInfo.address.address,
76
95
  amount
@@ -95,26 +114,23 @@ export class EkuboQuoter {
95
114
  logger.verbose(`${EkuboQuoter.name}:: LST true Exchange Rate: ${exchangeRate}`);
96
115
  return exchangeRate;
97
116
  }
98
- // debt collateral
117
+ // debt collateral
99
118
  async getSwapLimitAmount(fromToken: TokenInfo, toToken: TokenInfo, amount: Web3Number, max_slippage: number = 0.002): Promise<Web3Number> {
100
119
  const isExactAmountIn = amount.greaterThanOrEqualTo(0);
101
120
  logger.verbose(`${EkuboQuoter.name}::getSwapLimitAmount isExactAmountIn: ${isExactAmountIn}, fromToken: ${fromToken.symbol}, toToken: ${toToken.symbol}, amount: ${amount}`);
102
121
  const isYieldToken = this.tokenMarketData.isAPYSupported(toToken);
103
- console.log("isYieldToken", isYieldToken);
104
-
122
+
105
123
  // if LST, get true exchange rate else use dex price
106
124
  // wbtc
107
- const baseToken = isExactAmountIn ? toToken : fromToken; // fromToken -> wbtc,
108
- const quoteToken = isExactAmountIn ? fromToken : toToken; // toToken -> usdc,
125
+ const baseToken = isExactAmountIn ? toToken : fromToken; // fromToken -> wbtc,
126
+ const quoteToken = isExactAmountIn ? fromToken : toToken; // toToken -> usdc,
109
127
  // need dex price of from token in toToken
110
128
  // from baseToken to underlying token
111
129
  // for withdraw, usdc to btc with amount negative
112
130
  const dexPrice = await this.getDexPrice(baseToken, quoteToken, amount);
113
131
  const trueExchangeRate = isYieldToken ? await this.tokenMarketData.getTruePrice(baseToken) : dexPrice;
114
- console.log("trueExchangeRate", trueExchangeRate);
115
132
  if (isExactAmountIn) {
116
133
  let minLSTReceived = amount.dividedBy(dexPrice).multipliedBy(1 - max_slippage); // used for increase
117
- console.log("minLSTReceived", minLSTReceived);
118
134
  const minLSTReceivedAsPerTruePrice = amount.dividedBy(trueExchangeRate); // execution output to be <= True LST price
119
135
  if (minLSTReceived < minLSTReceivedAsPerTruePrice) {
120
136
  minLSTReceived = minLSTReceivedAsPerTruePrice; // the execution shouldn't be bad than True price logi
@@ -122,26 +138,26 @@ export class EkuboQuoter {
122
138
  logger.verbose(`${EkuboQuoter.name}::getModifyLeverCall minLSTReceivedAsPerTruePrice: ${minLSTReceivedAsPerTruePrice}, minLSTReceived: ${minLSTReceived}`);
123
139
  return minLSTReceived;
124
140
  }
125
-
141
+
126
142
  let maxUsedCollateral = amount.abs().dividedBy(dexPrice).multipliedBy(1 + max_slippage); // +ve for exact amount out, used for decrease
127
143
  const maxUsedCollateralInLST = amount.abs().dividedBy(trueExchangeRate).multipliedBy(1.005); // 0.5% slippage, worst case based on true price
128
144
  logger.verbose(`${EkuboQuoter.name}::getModifyLeverCall maxUsedCollateralInLST: ${maxUsedCollateralInLST}, maxUsedCollateral: ${maxUsedCollateral}`);
129
145
  if (maxUsedCollateralInLST > maxUsedCollateral) {
130
146
  maxUsedCollateral = maxUsedCollateralInLST;
131
147
  }
132
-
148
+
133
149
  return maxUsedCollateral;
134
150
  }
135
151
 
136
152
  /**
137
153
  * Formats Ekubo response for Vesu multiple use
138
- * @param quote
139
- * @param fromTokenInfo
140
- * @returns
154
+ * @param quote
155
+ * @param fromTokenInfo
156
+ * @returns
141
157
  */
142
158
  getVesuMultiplyQuote(quote: EkuboQuote, fromTokenInfo: TokenInfo, toTokenInfo: TokenInfo): Swap[] {
143
159
  return quote.splits.map(split => {
144
-
160
+
145
161
  const isNegativeAmount = BigInt(split.amount_specified) <= 0n;
146
162
  const token = isNegativeAmount ? toTokenInfo : fromTokenInfo;
147
163
  return {
@@ -58,6 +58,23 @@ export class ERC20 {
58
58
  return transferCall;
59
59
  }
60
60
 
61
+ transferFrom(
62
+ token: string | ContractAddr,
63
+ from: string | ContractAddr,
64
+ to: string | ContractAddr,
65
+ amount: Web3Number
66
+ ) {
67
+ const contract = this.contract(token);
68
+ const amountUint256 = uint256.bnToUint256(amount.toWei());
69
+ const transferFromCall = contract.populate("transferFrom", [
70
+ from.toString(),
71
+ to.toString(),
72
+ amountUint256.low.toString(),
73
+ amountUint256.high.toString(),
74
+ ]);
75
+ return transferFromCall;
76
+ }
77
+
61
78
  approve(
62
79
  token: string | ContractAddr,
63
80
  spender: string | ContractAddr,
@@ -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,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
  }
@@ -1,21 +1,64 @@
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);
10
35
  }
11
36
 
12
- async getPrice(tokenSymbol: string): Promise<PriceInfo> {
37
+ async getPrice(tokenSymbol: string, blockNumber?: BlockIdentifier): Promise<PriceInfo> {
38
+ const tokenInfo = this.tokens.find(t => t.symbol === tokenSymbol);
39
+ if (!tokenInfo) {
40
+ throw new Error(`Token ${tokenSymbol} not found in configured tokens`);
41
+ }
42
+ const symbol = tokenInfo.priceProxySymbol || tokenInfo.symbol;
43
+ // If blockNumber is provided, fetch historical price from GraphQL
44
+ if (blockNumber !== undefined) {
45
+ try {
46
+ return await this.getHistoricalPrice(symbol, blockNumber);
47
+ } catch (e: any) {
48
+ logger.error(`Historical price fetch failed for ${tokenSymbol}: ${e.message}`);
49
+ throw new Error('Server Error in fetching historical price')
50
+ // Fall back to current price if historical price is not available
51
+ }
52
+ }
53
+
54
+ // Otherwise, fetch current price
13
55
  try {
14
- return await this.getPriceFromMyAPI(tokenSymbol);
56
+ return await this.getPriceFromMyAPI(symbol);
15
57
  } catch (e: any) {
16
- logger.warn('getPriceFromMyAPI error', e);
58
+ logger.warn(`API price fetch failed for ${tokenSymbol}: ${e.message}, trying coinbase`);
17
59
  }
18
- logger.info('getPrice coinbase', tokenSymbol);
60
+
61
+ logger.info(`Using Coinbase price for ${tokenSymbol}`);
19
62
  let retry = 0;
20
63
  const MAX_RETRIES = 5;
21
64
  for (retry = 1; retry < MAX_RETRIES + 1; retry++) {
@@ -41,22 +84,120 @@ export class PricerFromApi extends PricerBase {
41
84
  }
42
85
 
43
86
  async getPriceFromMyAPI(tokenSymbol: string) {
44
- logger.verbose(`getPrice from api: ${tokenSymbol}`);
45
87
  const endpoint = 'https://proxy.api.troves.fi'
46
88
  const url = `${endpoint}/api/price/${tokenSymbol}`;
47
89
  const priceInfoRes = await fetch(url);
48
90
  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
91
  const price = Number(priceInfo.price);
57
92
  return {
58
93
  price,
59
94
  timestamp: new Date(priceInfo.timestamp)
60
95
  }
61
- }
62
- }
96
+ }
97
+
98
+ /**
99
+ * Fetches historical price for a token at a specific block number
100
+ * @param tokenSymbol - The token symbol to get price for
101
+ * @param blockNumber - The block number to query
102
+ * @returns PriceInfo with price at the closest block <= blockNumber
103
+ */
104
+ async getHistoricalPrice(tokenSymbol: string, blockNumber: BlockIdentifier): Promise<PriceInfo> {
105
+ const tokenInfo = this.tokens.find(t => t.symbol === tokenSymbol);
106
+ if (!tokenInfo) {
107
+ throw new Error(`Token ${tokenSymbol} not found in configured tokens`);
108
+ }
109
+
110
+ // If blockNumber is not a number, fall back to current price
111
+ if (typeof blockNumber !== 'number') {
112
+ logger.info(`Non-numeric blockIdentifier '${blockNumber}' provided, fetching current price for ${tokenSymbol}`);
113
+ return await this.getPriceFromMyAPI(tokenSymbol);
114
+ }
115
+
116
+ const standardizedAddress = ContractAddr.from(tokenInfo.address.address).address;
117
+ const isPragmaSupported = this.PRAGMA_SUPPORTED_TOKENS.includes(standardizedAddress);
118
+
119
+ // Step 1: Try GraphQL first for ALL tokens
120
+ let data;
121
+ try {
122
+ const result = await this.apolloClient.query({
123
+ query: gql`
124
+ query GetHistoricalPrice($asset: String!, $blockNumber: Int!) {
125
+ findFirstPrices(
126
+ where: {
127
+ asset: { equals: $asset },
128
+ block_number: { lte: $blockNumber }
129
+ }
130
+ orderBy: { block_number: desc }
131
+ ) {
132
+ price
133
+ timestamp
134
+ block_number
135
+ }
136
+ }
137
+ `,
138
+ variables: {
139
+ asset: standardizedAddress,
140
+ blockNumber: blockNumber,
141
+ },
142
+ });
143
+ data = result.data;
144
+ } catch (graphqlError: any) {
145
+ logger.error(`GraphQL query failed for ${tokenSymbol}: ${graphqlError.message}`);
146
+ }
147
+
148
+ // Step 2: If GraphQL has data, check block difference and use it if acceptable
149
+ if (data?.findFirstPrices) {
150
+ const priceData = data.findFirstPrices;
151
+ const blockDifference = blockNumber - priceData.block_number;
152
+ const MAX_BLOCK_DIFFERENCE = 6800; // ~6 hours worth of blocks
153
+
154
+ if (blockDifference <= MAX_BLOCK_DIFFERENCE) {
155
+ logger.info(`Using GraphQL price for ${tokenSymbol} at block ${priceData.block_number}`);
156
+ return {
157
+ price: Number(priceData.price),
158
+ timestamp: new Date(priceData.timestamp * 1000),
159
+ };
160
+ }
161
+
162
+ logger.info(
163
+ `Block difference ${blockDifference} exceeds limit for ${tokenSymbol}. ` +
164
+ `Will try fallback sources at block ${blockNumber}`
165
+ );
166
+ } else {
167
+ logger.info(`No GraphQL data for ${tokenSymbol} at block ${blockNumber}, trying fallback sources`);
168
+ }
169
+
170
+ // Step 3: Fallback to Pragma (for supported tokens) or Ekubo (for non-supported tokens)
171
+ if (isPragmaSupported) {
172
+ logger.info(`Attempting Pragma RPC fetch for ${tokenSymbol} at block ${blockNumber}`);
173
+ try {
174
+ const priceInfo = await this.pragma.getPrice(tokenInfo.address.address, blockNumber);
175
+ logger.info(`Using Pragma price for ${tokenSymbol}: ${priceInfo.price}`);
176
+ return {
177
+ price: priceInfo.price,
178
+ timestamp: priceInfo.timestamp,
179
+ };
180
+ } catch (error: any) {
181
+ logger.error(`Pragma RPC failed for ${tokenSymbol} at block ${blockNumber}: ${error.message}`);
182
+ throw new Error(`Server Error at Pragma token ${tokenSymbol} at block ${blockNumber}`);
183
+ }
184
+ } else {
185
+ // For non-Pragma tokens, use Ekubo pricer
186
+ logger.info(`Attempting Ekubo price fetch for ${tokenSymbol} at block ${blockNumber}`);
187
+ try {
188
+ const priceInfo = await this.ekuboPricer.getPrice(
189
+ tokenInfo.address.address,
190
+ blockNumber
191
+ );
192
+ logger.info(`Using Ekubo price for ${tokenSymbol}: ${priceInfo.price}`);
193
+ return {
194
+ price: priceInfo.price,
195
+ timestamp: priceInfo.timestamp,
196
+ };
197
+ } catch (error: any) {
198
+ logger.error(`Ekubo RPC failed for ${tokenSymbol} at block ${blockNumber}: ${error.message}`);
199
+ throw new Error(`Server Error at Ekubo token ${tokenSymbol} at block ${blockNumber}`);
200
+ }
201
+ }
202
+ }
203
+ }
@@ -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;