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

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 (79) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +119400 -92821
  4. package/dist/index.browser.mjs +13293 -11146
  5. package/dist/index.d.ts +2281 -1938
  6. package/dist/index.js +13532 -11179
  7. package/dist/index.mjs +14165 -11836
  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 +279 -233
  19. package/src/index.browser.ts +2 -1
  20. package/src/interfaces/common.tsx +228 -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 +80 -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 +107 -41
  33. package/src/modules/pricerBase.ts +2 -1
  34. package/src/modules/zkLend.ts +3 -2
  35. package/src/node/deployer.ts +36 -1
  36. package/src/node/pricer-redis.ts +3 -1
  37. package/src/strategies/base-strategy.ts +168 -16
  38. package/src/strategies/constants.ts +8 -3
  39. package/src/strategies/ekubo-cl-vault.tsx +1048 -355
  40. package/src/strategies/factory.ts +199 -0
  41. package/src/strategies/index.ts +5 -3
  42. package/src/strategies/registry.ts +262 -0
  43. package/src/strategies/sensei.ts +354 -10
  44. package/src/strategies/svk-strategy.ts +292 -31
  45. package/src/strategies/token-boosted-xstrk-carry-strategy.tsx +1261 -0
  46. package/src/strategies/types.ts +4 -0
  47. package/src/strategies/universal-adapters/adapter-utils.ts +4 -1
  48. package/src/strategies/universal-adapters/avnu-adapter.ts +196 -272
  49. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  50. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  51. package/src/strategies/universal-adapters/index.ts +10 -8
  52. package/src/strategies/universal-adapters/svk-troves-adapter.ts +511 -0
  53. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  54. package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
  55. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +525 -0
  56. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +866 -860
  57. package/src/strategies/universal-adapters/vesu-position-common.ts +258 -0
  58. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  59. package/src/strategies/universal-lst-muliplier-strategy.tsx +895 -416
  60. package/src/strategies/universal-strategy.tsx +1332 -1173
  61. package/src/strategies/vesu-rebalance.tsx +254 -153
  62. package/src/strategies/yoloVault.ts +1096 -0
  63. package/src/utils/cacheClass.ts +11 -2
  64. package/src/utils/health-factor-math.ts +33 -1
  65. package/src/utils/index.ts +3 -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/modules/ExtendedWrapperSDk/index.ts +0 -62
  71. package/src/modules/ExtendedWrapperSDk/types.ts +0 -311
  72. package/src/modules/ExtendedWrapperSDk/wrapper.ts +0 -395
  73. package/src/strategies/universal-adapters/extended-adapter.ts +0 -662
  74. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
  75. package/src/strategies/vesu-extended-strategy/services/operationService.ts +0 -34
  76. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +0 -77
  77. package/src/strategies/vesu-extended-strategy/utils/constants.ts +0 -49
  78. package/src/strategies/vesu-extended-strategy/utils/helper.ts +0 -372
  79. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +0 -1140
@@ -25,6 +25,14 @@ export interface RiskFactor {
25
25
  reason?: string; // optional reason for the risk factor
26
26
  }
27
27
 
28
+ export type PriceMethod = 'AvnuApi' | 'Coinbase' | 'Coinmarketcap' | 'Ekubo' | 'Avnu';
29
+ export enum TokenIndexingType {
30
+ PEGGED = "pegged",
31
+ INDEXER = "indexer",
32
+ LST_SCRIPT = "lstScript",
33
+ IGNORE = "ignore",
34
+ }
35
+
28
36
  export interface TokenInfo {
29
37
  name: string;
30
38
  symbol: string;
@@ -35,6 +43,9 @@ export interface TokenInfo {
35
43
  displayDecimals: number;
36
44
  priceProxySymbol?: string; // for tokens like illiquid tokens, we use a proxy symbol to get the price
37
45
  priceCheckAmount?: number; // for tokens like BTC, doing 1BTC price check may not be ideal, esp on illiquid netwrks like sn
46
+ priceMethod?: PriceMethod; // preferred price source; tried first, then falls back to other methods
47
+ dontPrice?: boolean; // a flag that skips pricer to check these tokens.
48
+ indexingType: TokenIndexingType;
38
49
  }
39
50
 
40
51
  export enum Network {
@@ -55,6 +66,75 @@ export interface IProtocol {
55
66
  logo: string;
56
67
  }
57
68
 
69
+ export interface ICurator {
70
+ name: string;
71
+ logo: string;
72
+ }
73
+
74
+ export enum StrategyTag {
75
+ META_VAULT = "Meta Vaults",
76
+ LEVERED = "Maxx",
77
+ AUTOMATED_LP = "Ekubo",
78
+ BTC = "BTC"
79
+ }
80
+
81
+ export enum VaultType {
82
+ LOOPING = "Looping",
83
+ META_VAULT = "Meta Vault",
84
+ DELTA_NEUTRAL = "Delta Neutral",
85
+ AUTOMATED_LP = "Automated LP",
86
+ TVA = "Troves Value Averaging",
87
+ }
88
+
89
+ // Security metadata enums
90
+ export enum AuditStatus {
91
+ AUDITED = "Audited",
92
+ NOT_AUDITED = "Not Audited",
93
+ }
94
+
95
+ export enum SourceCodeType {
96
+ OPEN_SOURCE = "Open Source",
97
+ CLOSED_SOURCE = "Closed Source",
98
+ }
99
+
100
+ export enum AccessControlType {
101
+ MULTISIG_ACCOUNT = "Multisig Account",
102
+ STANDARD_ACCOUNT = "Standard Account",
103
+ ROLE_BASED_ACCESS = "Role Based Access",
104
+ }
105
+
106
+ export enum InstantWithdrawalVault {
107
+ YES = "Yes",
108
+ NO = "No",
109
+ }
110
+
111
+ // Security metadata interfaces
112
+ export interface SourceCodeInfo {
113
+ type: SourceCodeType;
114
+ contractLink: string;
115
+ }
116
+
117
+ export interface AccessControlInfo {
118
+ type: AccessControlType;
119
+ addresses: ContractAddr[];
120
+ timeLock?: string;
121
+ }
122
+
123
+ export interface SecurityMetadata {
124
+ auditStatus: AuditStatus;
125
+ sourceCode: SourceCodeInfo;
126
+ accessControl: AccessControlInfo;
127
+ }
128
+
129
+ export interface RedemptionInfo {
130
+ instantWithdrawalVault: InstantWithdrawalVault;
131
+ redemptionsInfo: {
132
+ title: string; // e.g. Upto $1M
133
+ description: string; // e.g. "1-2 hours"
134
+ }[],
135
+ alerts: StrategyAlert[];
136
+ }
137
+
58
138
  export enum FlowChartColors {
59
139
  Green = "purple",
60
140
  Blue = "#35484f",
@@ -66,26 +146,89 @@ export interface FAQ {
66
146
  answer: string | React.ReactNode;
67
147
  }
68
148
 
149
+ export enum StrategyLiveStatus {
150
+ ACTIVE = "Active",
151
+ NEW = "New",
152
+ COMING_SOON = "Coming Soon",
153
+ DEPRECATED = "Deprecated", // active but not recommended
154
+ RETIRED = "Retired", // not active anymore
155
+ HOT = "Hot & New 🔥"
156
+ }
157
+
158
+ export interface StrategyAlert {
159
+ type: "warning" | "info";
160
+ text: string | React.ReactNode;
161
+ tab: "all" | "deposit" | "withdraw";
162
+ }
163
+
164
+ export interface StrategySettings {
165
+ liveStatus?: StrategyLiveStatus;
166
+ isPaused?: boolean;
167
+ isInMaintenance?: boolean;
168
+ isAudited: boolean;
169
+ isInstantWithdrawal?: boolean;
170
+ hideHarvestInfo?: boolean;
171
+ is_promoted?: boolean;
172
+ isTransactionHistDisabled?: boolean;
173
+ quoteToken: TokenInfo;
174
+ hideNetEarnings?: boolean;
175
+ showWithdrawalWarningModal?: boolean;
176
+ alerts?: StrategyAlert[];
177
+ tags?: StrategyTag[];
178
+ }
179
+
180
+ export interface StrategyApyHistoryUIConfig {
181
+ // Defaults to true in UI if omitted.
182
+ showApyHistory?: boolean;
183
+ // Optional message shown when APY history is hidden.
184
+ noApyHistoryMessage?: string;
185
+ }
186
+
187
+ export interface FeeBps {
188
+ performanceFeeBps: number;
189
+ }
190
+
69
191
  /**
70
192
  * @property risk.riskFactor.factor - The risk factors that are considered for the strategy.
71
193
  * @property risk.riskFactor.factor - The value of the risk factor from 0 to 10, 0 being the lowest and 10 being the highest.
194
+ * @property security - Security-related metadata including audit status, source code information, and access control details.
195
+ * @property redemptionInfo - Redemption information including instant withdrawal availability and expected redemption times.
72
196
  */
73
197
  export interface IStrategyMetadata<T> {
198
+ id: string;
74
199
  name: string;
75
200
  description: string | React.ReactNode;
201
+ /**
202
+ * Optional UI sort priority. Higher shows earlier.
203
+ * Intended for pinning flagship parent vaults (e.g. BTC above STRK).
204
+ */
205
+ priority?: number;
206
+ /**
207
+ * Optional UI config for the variant intro popup (strategy page).
208
+ * Should be identical across strategies that share the same `parentId`.
209
+ */
210
+ variantIntro?: {
211
+ title: string;
212
+ description: string;
213
+ };
76
214
  address: ContractAddr;
77
215
  launchBlock: number;
78
216
  type: "ERC4626" | "ERC721" | "Other";
217
+ vaultType: {
218
+ type: VaultType;
219
+ description: string;
220
+ };
79
221
  depositTokens: TokenInfo[];
80
222
  protocols: IProtocol[];
81
223
  auditUrl?: string;
82
- maxTVL: Web3Number;
83
224
  risk: {
84
225
  riskFactor: RiskFactor[];
85
226
  netRisk: number;
86
227
  notARisks: RiskType[];
87
228
  };
88
229
  apyMethodology?: string;
230
+ realizedApyMethodology?: string;
231
+ feeBps?: FeeBps;
89
232
  additionalInfo: T;
90
233
  contractDetails: {
91
234
  address: ContractAddr;
@@ -96,8 +239,34 @@ export interface IStrategyMetadata<T> {
96
239
  points?: {multiplier: number, logo: string, toolTip?: string}[];
97
240
  docs?: string;
98
241
  investmentSteps: string[];
99
- curator?: { name: string, logo: string },
100
- isPreview?: boolean
242
+ curator?: ICurator,
243
+ isPreview?: boolean;
244
+ tags?: StrategyTag[];
245
+ security: SecurityMetadata;
246
+ redemptionInfo: RedemptionInfo;
247
+ usualTimeToEarnings: null | string; // e.g. "2 weeks" // some strats grow like step functions
248
+ usualTimeToEarningsDescription: null | string; // e.g. "LSTs price on DEX goes up roughly every 2 weeks"
249
+ discontinuationInfo?: {
250
+ date?: Date;
251
+ reason?: React.ReactNode | string;
252
+ info?: React.ReactNode | string;
253
+ };
254
+ settings?: StrategySettings;
255
+ apyHistoryUIConfig?: StrategyApyHistoryUIConfig;
256
+ // Legacy field for multi-step strategies (deprecated, use investmentFlows instead)
257
+ actions?: Array<{
258
+ name?: string;
259
+ pool?: {
260
+ protocol?: { name: string; logo: string };
261
+ pool?: { name: string; logos?: string[] };
262
+ apr?: number;
263
+ borrow?: { apr?: number };
264
+ };
265
+ amount?: string | number;
266
+ isDeposit?: boolean;
267
+ }>;
268
+ parentId?: string;
269
+ parentName?: string;
101
270
  }
102
271
 
103
272
  export interface IInvestmentFlow {
@@ -124,6 +293,23 @@ export function getMainnetConfig(
124
293
  };
125
294
  }
126
295
 
296
+ export const getStrategyTagDesciption = (tag: StrategyTag): string => {
297
+ switch (tag) {
298
+ case StrategyTag.META_VAULT:
299
+ return "A meta vault is a vault that auto allocates funds to multiple vaults based on optimal yield opportunities";
300
+ case StrategyTag.LEVERED:
301
+ return "Looping vaults on Endur LSTs with leveraged borrowing of STRK or BTC to increase yield (2-4x higher yield than simply staking)";
302
+ case StrategyTag.AUTOMATED_LP:
303
+ return "Automated LP vaults on Ekubo that rebalance position automatically, ensuring you earn fees efficiently";
304
+ case StrategyTag.BTC:
305
+ return "BTC linked vaults";
306
+ }
307
+ }
308
+
309
+ export const getAllStrategyTags = (): StrategyTag[] => {
310
+ return Object.values(StrategyTag);
311
+ }
312
+
127
313
  export const getRiskExplaination = (riskType: RiskType) => {
128
314
  switch (riskType) {
129
315
  case RiskType.MARKET_RISK:
@@ -196,7 +382,7 @@ export function highlightTextWithLinks(
196
382
  {parts.map((part, i) => {
197
383
  const match = highlights.find(m => m.highlight.toLowerCase() === part.toLowerCase());
198
384
  return match ? (
199
- <a key={i} href={match.link} target="_blank" style={{ color: 'var(--chakra-colors-white)', background: 'var(--chakra-colors-highlight)' }}>
385
+ <a key={i} href={match.link} target="_blank" style={{ color: 'white', background: 'rgba(255, 255, 255, 0.04)' }}>
200
386
  {part}
201
387
  </a>
202
388
  ) : (
@@ -215,6 +401,30 @@ export interface VaultPosition {
215
401
  protocol: IProtocol
216
402
  }
217
403
 
404
+ export interface AmountInfo {
405
+ amount: Web3Number;
406
+ usdValue: number;
407
+ tokenInfo: TokenInfo;
408
+ }
409
+
410
+ export interface AmountsInfo {
411
+ usdValue: number;
412
+ amounts: AmountInfo[];
413
+ }
414
+
415
+ /**
416
+ * Strategy capabilities interface
417
+ * Describes what optional methods a strategy instance supports
418
+ */
419
+ export interface StrategyCapabilities {
420
+ hasMatchInputAmounts: boolean;
421
+ hasNetAPY: boolean;
422
+ hasGetInvestmentFlows: boolean;
423
+ hasGetPendingRewards: boolean;
424
+ hasHarvest: boolean;
425
+ hasRebalance: boolean;
426
+ }
427
+
218
428
  const VesuProtocol: IProtocol = {
219
429
  name: "Vesu",
220
430
  logo: "https://static-assets-8zct.onrender.com/integrations/vesu/logo.png"
@@ -249,6 +459,12 @@ const VaultProtocol: IProtocol = {
249
459
  name: "Vault",
250
460
  logo: ""
251
461
  }
462
+
463
+ const TrovesProtocol: IProtocol = {
464
+ name: "Troves",
465
+ logo: "https://app.troves.fi/favicon.ico"
466
+ };
467
+
252
468
  export const Protocols = {
253
469
  NONE: NoneProtocol,
254
470
  VESU: VesuProtocol,
@@ -256,5 +472,11 @@ export const Protocols = {
256
472
  EXTENDED: ExtendedProtocol,
257
473
  EKUBO: EkuboProtocol,
258
474
  AVNU: AvnuProtocol,
259
- VAULT: VaultProtocol
260
- }
475
+ VAULT: VaultProtocol,
476
+ TROVES: TrovesProtocol
477
+ }
478
+
479
+ export const UnwrapLabsCurator: ICurator = {
480
+ name: "Unwrap Labs",
481
+ logo: "https://assets.troves.fi/integrations/unwraplabs/white.png"
482
+ }
@@ -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,80 @@
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
+ // Updating to new USDC_ADDRESS
11
+ private readonly USDC_ADDRESS = '0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb';
12
+ private readonly USDC_DECIMALS = 6;
13
+
14
+ constructor(config: IConfig, tokens: TokenInfo[]) {
15
+ super(config, tokens);
16
+ this.contract = new Contract({
17
+ abi: EkuboPricerAbi,
18
+ address: this.EKUBO_PRICE_FETCHER_ADDRESS,
19
+ providerOrAccount: config.provider as RpcProvider
20
+ });
21
+ }
22
+
23
+ private div2Power128(num: bigint): number {
24
+ return Number((num * BigInt(1e18)) / BigInt(2 ** 128)) / 1e18;
25
+ }
26
+
27
+ async getPrice(tokenAddr: string, blockIdentifier: BlockIdentifier = 'latest'): Promise<PriceInfo> {
28
+ if (!tokenAddr) {
29
+ throw new Error(`EkuboPricer:getPrice - no token`);
30
+ }
31
+
32
+ // get_prices arguments in order:
33
+ // - quote_token: USDC address (quote token for price calculation)
34
+ // - base_tokens: array containing the base token address/addresses
35
+ // - period: time period in seconds for TWAP (3600 = 1 hour)
36
+ // - min_token: minimum token amount threshold (min liquidity) in 6 Decimals = 1000000)
37
+ const result: any = await this.contract.call(
38
+ 'get_prices',
39
+ [this.USDC_ADDRESS, [tokenAddr], 3600, 1000000],
40
+ { blockIdentifier }
41
+ );
42
+
43
+ if (!result || result.length === 0) {
44
+ throw new Error(`EkuboPricer: No price result returned for ${tokenAddr}`);
45
+ }
46
+
47
+ const priceResult = result[0];
48
+
49
+ if (!priceResult?.variant?.Price) {
50
+ const variant = priceResult?.variant ? Object.keys(priceResult.variant)[0] : 'Unknown';
51
+ throw new Error(`EkuboPricer: Price fetch failed with variant: ${variant}`);
52
+ }
53
+
54
+ const rawPrice = typeof priceResult.variant.Price === 'string'
55
+ ? BigInt(priceResult.variant.Price)
56
+ : priceResult.variant.Price;
57
+
58
+ // Get token info to determine decimals from configured tokens
59
+ const tokenInfo = this.tokens.find(t =>
60
+ t.address.address.toLowerCase() === tokenAddr.toLowerCase()
61
+ );
62
+
63
+ if (!tokenInfo) {
64
+ throw new Error(`Token ${tokenAddr} not found in global tokens`);
65
+ }
66
+
67
+ // Convert from x128 format
68
+ const priceAfterX128 = this.div2Power128(rawPrice);
69
+
70
+ // Adjust for token decimals
71
+ const decimalAdjustment = 10 ** (tokenInfo.decimals - this.USDC_DECIMALS);
72
+ const price = priceAfterX128 * decimalAdjustment;
73
+
74
+ return {
75
+ price,
76
+ timestamp: new Date()
77
+ };
78
+ }
79
+ }
80
+
@@ -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,