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

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