@strkfarm/sdk 2.0.0-dev.4 → 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.
Files changed (78) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +116250 -90801
  4. package/dist/index.browser.mjs +13050 -10957
  5. package/dist/index.d.ts +2232 -1933
  6. package/dist/index.js +13380 -11084
  7. package/dist/index.mjs +13280 -11007
  8. package/package.json +6 -7
  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 +93 -36
  19. package/src/index.browser.ts +2 -1
  20. package/src/interfaces/common.tsx +218 -5
  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 +79 -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 +156 -15
  31. package/src/modules/pricer-lst.ts +1 -1
  32. package/src/modules/pricer.ts +94 -40
  33. package/src/modules/pricerBase.ts +2 -1
  34. package/src/node/deployer.ts +36 -1
  35. package/src/node/pricer-redis.ts +3 -1
  36. package/src/strategies/base-strategy.ts +168 -16
  37. package/src/strategies/constants.ts +8 -3
  38. package/src/strategies/ekubo-cl-vault.tsx +1047 -351
  39. package/src/strategies/factory.ts +199 -0
  40. package/src/strategies/index.ts +5 -3
  41. package/src/strategies/registry.ts +262 -0
  42. package/src/strategies/sensei.ts +353 -9
  43. package/src/strategies/svk-strategy.ts +283 -31
  44. package/src/strategies/token-boosted-xstrk-carry-strategy.tsx +1262 -0
  45. package/src/strategies/types.ts +4 -0
  46. package/src/strategies/universal-adapters/adapter-utils.ts +4 -1
  47. package/src/strategies/universal-adapters/avnu-adapter.ts +196 -272
  48. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  49. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  50. package/src/strategies/universal-adapters/index.ts +10 -8
  51. package/src/strategies/universal-adapters/svk-troves-adapter.ts +511 -0
  52. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  53. package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
  54. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +525 -0
  55. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1098 -712
  56. package/src/strategies/universal-adapters/vesu-position-common.ts +258 -0
  57. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  58. package/src/strategies/universal-lst-muliplier-strategy.tsx +631 -414
  59. package/src/strategies/universal-strategy.tsx +1331 -1173
  60. package/src/strategies/vesu-rebalance.tsx +252 -152
  61. package/src/strategies/yoloVault.ts +1087 -0
  62. package/src/utils/cacheClass.ts +11 -2
  63. package/src/utils/health-factor-math.ts +33 -1
  64. package/src/utils/index.ts +3 -1
  65. package/src/utils/logger.browser.ts +22 -4
  66. package/src/utils/logger.node.ts +259 -24
  67. package/src/utils/starknet-call-parser.ts +1036 -0
  68. package/src/utils/strategy-utils.ts +61 -0
  69. package/src/modules/ExtendedWrapperSDk/index.ts +0 -62
  70. package/src/modules/ExtendedWrapperSDk/types.ts +0 -311
  71. package/src/modules/ExtendedWrapperSDk/wrapper.ts +0 -395
  72. package/src/strategies/universal-adapters/extended-adapter.ts +0 -661
  73. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
  74. package/src/strategies/vesu-extended-strategy/services/operationService.ts +0 -34
  75. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +0 -77
  76. package/src/strategies/vesu-extended-strategy/utils/constants.ts +0 -49
  77. package/src/strategies/vesu-extended-strategy/utils/helper.ts +0 -372
  78. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +0 -1140
@@ -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 {
@@ -55,6 +56,75 @@ export interface IProtocol {
55
56
  logo: string;
56
57
  }
57
58
 
59
+ export interface ICurator {
60
+ name: string;
61
+ logo: string;
62
+ }
63
+
64
+ export enum StrategyTag {
65
+ META_VAULT = "Meta Vaults",
66
+ LEVERED = "Maxx",
67
+ AUTOMATED_LP = "Ekubo",
68
+ BTC = "BTC"
69
+ }
70
+
71
+ export enum VaultType {
72
+ LOOPING = "Looping",
73
+ META_VAULT = "Meta Vault",
74
+ DELTA_NEUTRAL = "Delta Neutral",
75
+ AUTOMATED_LP = "Automated LP",
76
+ TVA = "Troves Value Averaging",
77
+ }
78
+
79
+ // Security metadata enums
80
+ export enum AuditStatus {
81
+ AUDITED = "Audited",
82
+ NOT_AUDITED = "Not Audited",
83
+ }
84
+
85
+ export enum SourceCodeType {
86
+ OPEN_SOURCE = "Open Source",
87
+ CLOSED_SOURCE = "Closed Source",
88
+ }
89
+
90
+ export enum AccessControlType {
91
+ MULTISIG_ACCOUNT = "Multisig Account",
92
+ STANDARD_ACCOUNT = "Standard Account",
93
+ ROLE_BASED_ACCESS = "Role Based Access",
94
+ }
95
+
96
+ export enum InstantWithdrawalVault {
97
+ YES = "Yes",
98
+ NO = "No",
99
+ }
100
+
101
+ // Security metadata interfaces
102
+ export interface SourceCodeInfo {
103
+ type: SourceCodeType;
104
+ contractLink: string;
105
+ }
106
+
107
+ export interface AccessControlInfo {
108
+ type: AccessControlType;
109
+ addresses: ContractAddr[];
110
+ timeLock?: string;
111
+ }
112
+
113
+ export interface SecurityMetadata {
114
+ auditStatus: AuditStatus;
115
+ sourceCode: SourceCodeInfo;
116
+ accessControl: AccessControlInfo;
117
+ }
118
+
119
+ export interface RedemptionInfo {
120
+ instantWithdrawalVault: InstantWithdrawalVault;
121
+ redemptionsInfo: {
122
+ title: string; // e.g. Upto $1M
123
+ description: string; // e.g. "1-2 hours"
124
+ }[],
125
+ alerts: StrategyAlert[];
126
+ }
127
+
58
128
  export enum FlowChartColors {
59
129
  Green = "purple",
60
130
  Blue = "#35484f",
@@ -66,26 +136,90 @@ export interface FAQ {
66
136
  answer: string | React.ReactNode;
67
137
  }
68
138
 
139
+ export enum StrategyLiveStatus {
140
+ ACTIVE = "Active",
141
+ NEW = "New",
142
+ COMING_SOON = "Coming Soon",
143
+ DEPRECATED = "Deprecated", // active but not recommended
144
+ RETIRED = "Retired", // not active anymore
145
+ HOT = "Hot & New 🔥"
146
+ }
147
+
148
+ export interface StrategyAlert {
149
+ type: "warning" | "info";
150
+ text: string | React.ReactNode;
151
+ tab: "all" | "deposit" | "withdraw";
152
+ }
153
+
154
+ export interface StrategySettings {
155
+ maxTVL?: Web3Number;
156
+ liveStatus?: StrategyLiveStatus;
157
+ isPaused?: boolean;
158
+ isInMaintenance?: boolean;
159
+ isAudited: boolean;
160
+ isInstantWithdrawal?: boolean;
161
+ hideHarvestInfo?: boolean;
162
+ is_promoted?: boolean;
163
+ isTransactionHistDisabled?: boolean;
164
+ quoteToken: TokenInfo;
165
+ hideNetEarnings?: boolean;
166
+ showWithdrawalWarningModal?: boolean;
167
+ alerts?: StrategyAlert[];
168
+ tags?: StrategyTag[];
169
+ }
170
+
171
+ export interface StrategyApyHistoryUIConfig {
172
+ // Defaults to true in UI if omitted.
173
+ showApyHistory?: boolean;
174
+ // Optional message shown when APY history is hidden.
175
+ noApyHistoryMessage?: string;
176
+ }
177
+
178
+ export interface FeeBps {
179
+ performanceFeeBps: number;
180
+ }
181
+
69
182
  /**
70
183
  * @property risk.riskFactor.factor - The risk factors that are considered for the strategy.
71
184
  * @property risk.riskFactor.factor - The value of the risk factor from 0 to 10, 0 being the lowest and 10 being the highest.
185
+ * @property security - Security-related metadata including audit status, source code information, and access control details.
186
+ * @property redemptionInfo - Redemption information including instant withdrawal availability and expected redemption times.
72
187
  */
73
188
  export interface IStrategyMetadata<T> {
189
+ id: string;
74
190
  name: string;
75
191
  description: string | React.ReactNode;
192
+ /**
193
+ * Optional UI sort priority. Higher shows earlier.
194
+ * Intended for pinning flagship parent vaults (e.g. BTC above STRK).
195
+ */
196
+ priority?: number;
197
+ /**
198
+ * Optional UI config for the variant intro popup (strategy page).
199
+ * Should be identical across strategies that share the same `parentId`.
200
+ */
201
+ variantIntro?: {
202
+ title: string;
203
+ description: string;
204
+ };
76
205
  address: ContractAddr;
77
206
  launchBlock: number;
78
207
  type: "ERC4626" | "ERC721" | "Other";
208
+ vaultType: {
209
+ type: VaultType;
210
+ description: string;
211
+ };
79
212
  depositTokens: TokenInfo[];
80
213
  protocols: IProtocol[];
81
214
  auditUrl?: string;
82
- maxTVL: Web3Number;
83
215
  risk: {
84
216
  riskFactor: RiskFactor[];
85
217
  netRisk: number;
86
218
  notARisks: RiskType[];
87
219
  };
88
220
  apyMethodology?: string;
221
+ realizedApyMethodology?: string;
222
+ feeBps?: FeeBps;
89
223
  additionalInfo: T;
90
224
  contractDetails: {
91
225
  address: ContractAddr;
@@ -96,8 +230,34 @@ export interface IStrategyMetadata<T> {
96
230
  points?: {multiplier: number, logo: string, toolTip?: string}[];
97
231
  docs?: string;
98
232
  investmentSteps: string[];
99
- curator?: { name: string, logo: string },
100
- isPreview?: boolean
233
+ curator?: ICurator,
234
+ isPreview?: boolean;
235
+ tags?: StrategyTag[];
236
+ security: SecurityMetadata;
237
+ redemptionInfo: RedemptionInfo;
238
+ usualTimeToEarnings: null | string; // e.g. "2 weeks" // some strats grow like step functions
239
+ usualTimeToEarningsDescription: null | string; // e.g. "LSTs price on DEX goes up roughly every 2 weeks"
240
+ discontinuationInfo?: {
241
+ date?: Date;
242
+ reason?: React.ReactNode | string;
243
+ info?: React.ReactNode | string;
244
+ };
245
+ settings?: StrategySettings;
246
+ apyHistoryUIConfig?: StrategyApyHistoryUIConfig;
247
+ // Legacy field for multi-step strategies (deprecated, use investmentFlows instead)
248
+ actions?: Array<{
249
+ name?: string;
250
+ pool?: {
251
+ protocol?: { name: string; logo: string };
252
+ pool?: { name: string; logos?: string[] };
253
+ apr?: number;
254
+ borrow?: { apr?: number };
255
+ };
256
+ amount?: string | number;
257
+ isDeposit?: boolean;
258
+ }>;
259
+ parentId?: string;
260
+ parentName?: string;
101
261
  }
102
262
 
103
263
  export interface IInvestmentFlow {
@@ -124,6 +284,23 @@ export function getMainnetConfig(
124
284
  };
125
285
  }
126
286
 
287
+ export const getStrategyTagDesciption = (tag: StrategyTag): string => {
288
+ switch (tag) {
289
+ case StrategyTag.META_VAULT:
290
+ return "A meta vault is a vault that auto allocates funds to multiple vaults based on optimal yield opportunities";
291
+ case StrategyTag.LEVERED:
292
+ return "Looping vaults on Endur LSTs with leveraged borrowing of STRK or BTC to increase yield (2-4x higher yield than simply staking)";
293
+ case StrategyTag.AUTOMATED_LP:
294
+ return "Automated LP vaults on Ekubo that rebalance position automatically, ensuring you earn fees efficiently";
295
+ case StrategyTag.BTC:
296
+ return "BTC linked vaults";
297
+ }
298
+ }
299
+
300
+ export const getAllStrategyTags = (): StrategyTag[] => {
301
+ return Object.values(StrategyTag);
302
+ }
303
+
127
304
  export const getRiskExplaination = (riskType: RiskType) => {
128
305
  switch (riskType) {
129
306
  case RiskType.MARKET_RISK:
@@ -196,7 +373,7 @@ export function highlightTextWithLinks(
196
373
  {parts.map((part, i) => {
197
374
  const match = highlights.find(m => m.highlight.toLowerCase() === part.toLowerCase());
198
375
  return match ? (
199
- <a key={i} href={match.link} target="_blank" style={{ color: 'var(--chakra-colors-white)', background: 'var(--chakra-colors-highlight)' }}>
376
+ <a key={i} href={match.link} target="_blank" style={{ color: 'white', background: 'rgba(255, 255, 255, 0.04)' }}>
200
377
  {part}
201
378
  </a>
202
379
  ) : (
@@ -215,6 +392,30 @@ export interface VaultPosition {
215
392
  protocol: IProtocol
216
393
  }
217
394
 
395
+ export interface AmountInfo {
396
+ amount: Web3Number;
397
+ usdValue: number;
398
+ tokenInfo: TokenInfo;
399
+ }
400
+
401
+ export interface AmountsInfo {
402
+ usdValue: number;
403
+ amounts: AmountInfo[];
404
+ }
405
+
406
+ /**
407
+ * Strategy capabilities interface
408
+ * Describes what optional methods a strategy instance supports
409
+ */
410
+ export interface StrategyCapabilities {
411
+ hasMatchInputAmounts: boolean;
412
+ hasNetAPY: boolean;
413
+ hasGetInvestmentFlows: boolean;
414
+ hasGetPendingRewards: boolean;
415
+ hasHarvest: boolean;
416
+ hasRebalance: boolean;
417
+ }
418
+
218
419
  const VesuProtocol: IProtocol = {
219
420
  name: "Vesu",
220
421
  logo: "https://static-assets-8zct.onrender.com/integrations/vesu/logo.png"
@@ -249,6 +450,12 @@ const VaultProtocol: IProtocol = {
249
450
  name: "Vault",
250
451
  logo: ""
251
452
  }
453
+
454
+ const TrovesProtocol: IProtocol = {
455
+ name: "Troves",
456
+ logo: "https://app.troves.fi/favicon.ico"
457
+ };
458
+
252
459
  export const Protocols = {
253
460
  NONE: NoneProtocol,
254
461
  VESU: VesuProtocol,
@@ -256,5 +463,11 @@ export const Protocols = {
256
463
  EXTENDED: ExtendedProtocol,
257
464
  EKUBO: EkuboProtocol,
258
465
  AVNU: AvnuProtocol,
259
- VAULT: VaultProtocol
466
+ VAULT: VaultProtocol,
467
+ TROVES: TrovesProtocol
468
+ }
469
+
470
+ export const UnwrapLabsCurator: ICurator = {
471
+ name: "Unwrap Labs",
472
+ logo: "https://assets.troves.fi/integrations/unwraplabs/white.png"
260
473
  }
@@ -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
+
@@ -4,7 +4,7 @@ import { Call, Uint256 } from "starknet";
4
4
  import { AvnuOptions, fetchBuildExecuteTransaction, fetchQuotes, Quote } from "@avnu/avnu-sdk";
5
5
  import { assert } from "../utils";
6
6
  import { logger } from "@/utils/logger";
7
- import { ContractAddr } from "@/dataTypes";
7
+ import { ContractAddr, Web3Number } from "@/dataTypes";
8
8
 
9
9
  export interface Route {
10
10
  token_from: string,
@@ -15,12 +15,12 @@ export interface Route {
15
15
  }
16
16
 
17
17
  export interface SwapInfo {
18
- token_from_address: string,
19
- token_from_amount: Uint256,
20
- token_to_address: string,
21
- token_to_amount: Uint256,
22
- token_to_min_amount: Uint256,
23
- beneficiary: string,
18
+ token_from_address: string,
19
+ token_from_amount: Uint256,
20
+ token_to_address: string,
21
+ token_to_amount: Uint256,
22
+ token_to_min_amount: Uint256,
23
+ beneficiary: string,
24
24
  integrator_fee_amount_bps: number,
25
25
  integrator_fee_recipient: string,
26
26
  routes: Route[]
@@ -59,7 +59,7 @@ export class AvnuWrapper {
59
59
  }
60
60
  throw new Error('no quotes found')
61
61
  }
62
-
62
+
63
63
  return filteredQuotes[0];
64
64
  }
65
65
 
@@ -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),
@@ -140,12 +140,21 @@ export class AvnuWrapper {
140
140
  async getSwapCallData(
141
141
  quote: Pick<Quote, 'quoteId' | 'buyTokenAddress' | 'buyAmount' | 'sellTokenAddress' | 'sellAmount'>,
142
142
  taker: string,
143
+ minAmount?: Web3Number,
143
144
  ) {
144
145
  const calldata = await fetchBuildExecuteTransaction(quote.quoteId, taker, undefined, false);
145
146
  const result = calldata.calls.map((call: Call) => {
146
147
  const data = call.calldata as string[];
147
148
  return data.map(x => BigInt(x));
148
149
  });
150
+
151
+ // override with given min amount out
152
+ if (minAmount) {
153
+ logger.verbose(`AvnuWrapper: minAmount passed ${minAmount.toString()}`)
154
+ const u256 = uint256.bnToUint256(minAmount.toWei());
155
+ result[0][6] = BigInt(u256.low);
156
+ result[0][7] = BigInt(u256.high);
157
+ }
149
158
  return result;
150
159
  }
151
- }
160
+ }
@@ -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,57 @@ 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
+ // console.log('quote', quote.data);
50
+ // console.log('')
51
+ return quote.data as EkuboQuote;
60
52
  } catch (error: any) {
61
- logger.error(`${error.message} dassf ${error.data}`);
53
+ logger.error(`EkuboQuoter::_callQuoterApi error: ${error.message}`);
62
54
  if (retry < 3) {
63
55
  await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 5000));
64
- return await this.getQuote(fromToken, toToken, amount, retry + 1);
56
+ return await this._callQuoterApi(fromToken, toToken, amount, retry + 1);
65
57
  }
66
58
  throw error;
67
59
  }
68
60
  }
69
61
 
62
+ /**
63
+ * Given exactly `inputAmount` of `fromToken`, how much `toToken` do I receive?
64
+ * @param fromToken - address of the token being sold
65
+ * @param toToken - address of the token being bought
66
+ * @param inputAmount - must be positive (the amount of fromToken to sell)
67
+ * @returns EkuboQuote where `total_calculated` is the output amount (positive)
68
+ */
69
+ async getQuoteExactInput(fromToken: string, toToken: string, inputAmount: Web3Number): Promise<EkuboQuote> {
70
+ if (inputAmount.isNegative() || inputAmount.isZero()) {
71
+ throw new Error(`EkuboQuoter::getQuoteExactInput inputAmount must be positive, got ${inputAmount.toFixed()}`);
72
+ }
73
+ return this._callQuoterApi(fromToken, toToken, inputAmount);
74
+ }
75
+
76
+ /**
77
+ * To receive exactly `outputAmount` of `toToken`, how much `fromToken` must I provide?
78
+ * @param fromToken - address of the token being sold
79
+ * @param toToken - address of the token being bought
80
+ * @param outputAmount - must be positive (the desired amount of toToken to receive)
81
+ * @returns EkuboQuote where `total_calculated` is the required input amount (negative per Ekubo convention)
82
+ */
83
+ async getQuoteExactOutput(fromToken: string, toToken: string, outputAmount: Web3Number): Promise<EkuboQuote> {
84
+ if (outputAmount.isNegative() || outputAmount.isZero()) {
85
+ throw new Error(`EkuboQuoter::getQuoteExactOutput outputAmount must be positive, got ${outputAmount.toFixed()}`);
86
+ }
87
+ const negatedAmount = new Web3Number(outputAmount.multipliedBy(-1).toFixed(Math.min(outputAmount.decimals, 15)), outputAmount.decimals);
88
+ return this._callQuoterApi(toToken, fromToken, negatedAmount);
89
+ }
90
+
70
91
  async getDexPrice(baseToken: TokenInfo, quoteToken: TokenInfo, amount: Web3Number) {
71
92
  const lstTokenInfo = baseToken;
72
93
  const lstUnderlyingTokenInfo = quoteToken;
73
- const quote = await this.getQuote(
94
+ const quote = await this.getQuoteExactInput(
74
95
  lstTokenInfo.address.address,
75
96
  lstUnderlyingTokenInfo.address.address,
76
97
  amount
@@ -95,26 +116,23 @@ export class EkuboQuoter {
95
116
  logger.verbose(`${EkuboQuoter.name}:: LST true Exchange Rate: ${exchangeRate}`);
96
117
  return exchangeRate;
97
118
  }
98
- // debt collateral
119
+ // debt collateral
99
120
  async getSwapLimitAmount(fromToken: TokenInfo, toToken: TokenInfo, amount: Web3Number, max_slippage: number = 0.002): Promise<Web3Number> {
100
121
  const isExactAmountIn = amount.greaterThanOrEqualTo(0);
101
122
  logger.verbose(`${EkuboQuoter.name}::getSwapLimitAmount isExactAmountIn: ${isExactAmountIn}, fromToken: ${fromToken.symbol}, toToken: ${toToken.symbol}, amount: ${amount}`);
102
123
  const isYieldToken = this.tokenMarketData.isAPYSupported(toToken);
103
- console.log("isYieldToken", isYieldToken);
104
-
124
+
105
125
  // if LST, get true exchange rate else use dex price
106
126
  // wbtc
107
- const baseToken = isExactAmountIn ? toToken : fromToken; // fromToken -> wbtc,
108
- const quoteToken = isExactAmountIn ? fromToken : toToken; // toToken -> usdc,
127
+ const baseToken = isExactAmountIn ? toToken : fromToken; // fromToken -> wbtc,
128
+ const quoteToken = isExactAmountIn ? fromToken : toToken; // toToken -> usdc,
109
129
  // need dex price of from token in toToken
110
130
  // from baseToken to underlying token
111
131
  // for withdraw, usdc to btc with amount negative
112
132
  const dexPrice = await this.getDexPrice(baseToken, quoteToken, amount);
113
133
  const trueExchangeRate = isYieldToken ? await this.tokenMarketData.getTruePrice(baseToken) : dexPrice;
114
- console.log("trueExchangeRate", trueExchangeRate);
115
134
  if (isExactAmountIn) {
116
135
  let minLSTReceived = amount.dividedBy(dexPrice).multipliedBy(1 - max_slippage); // used for increase
117
- console.log("minLSTReceived", minLSTReceived);
118
136
  const minLSTReceivedAsPerTruePrice = amount.dividedBy(trueExchangeRate); // execution output to be <= True LST price
119
137
  if (minLSTReceived < minLSTReceivedAsPerTruePrice) {
120
138
  minLSTReceived = minLSTReceivedAsPerTruePrice; // the execution shouldn't be bad than True price logi
@@ -122,26 +140,26 @@ export class EkuboQuoter {
122
140
  logger.verbose(`${EkuboQuoter.name}::getModifyLeverCall minLSTReceivedAsPerTruePrice: ${minLSTReceivedAsPerTruePrice}, minLSTReceived: ${minLSTReceived}`);
123
141
  return minLSTReceived;
124
142
  }
125
-
143
+
126
144
  let maxUsedCollateral = amount.abs().dividedBy(dexPrice).multipliedBy(1 + max_slippage); // +ve for exact amount out, used for decrease
127
145
  const maxUsedCollateralInLST = amount.abs().dividedBy(trueExchangeRate).multipliedBy(1.005); // 0.5% slippage, worst case based on true price
128
146
  logger.verbose(`${EkuboQuoter.name}::getModifyLeverCall maxUsedCollateralInLST: ${maxUsedCollateralInLST}, maxUsedCollateral: ${maxUsedCollateral}`);
129
147
  if (maxUsedCollateralInLST > maxUsedCollateral) {
130
148
  maxUsedCollateral = maxUsedCollateralInLST;
131
149
  }
132
-
150
+
133
151
  return maxUsedCollateral;
134
152
  }
135
153
 
136
154
  /**
137
155
  * Formats Ekubo response for Vesu multiple use
138
- * @param quote
139
- * @param fromTokenInfo
140
- * @returns
156
+ * @param quote
157
+ * @param fromTokenInfo
158
+ * @returns
141
159
  */
142
160
  getVesuMultiplyQuote(quote: EkuboQuote, fromTokenInfo: TokenInfo, toTokenInfo: TokenInfo): Swap[] {
143
161
  return quote.splits.map(split => {
144
-
162
+
145
163
  const isNegativeAmount = BigInt(split.amount_specified) <= 0n;
146
164
  const token = isNegativeAmount ? toTokenInfo : fromTokenInfo;
147
165
  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,