@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,2941 @@
1
+ import { ContractAddr, Web3Number } from "@/dataTypes";
2
+ import { IConfig, TokenInfo } from "@/interfaces";
3
+ import { PricerBase } from "@/modules/pricerBase";
4
+ import { ERC20 } from "@/modules";
5
+ import { assert, logger } from "@/utils";
6
+ import { ExtendedAdapter } from "../../universal-adapters/extended-adapter";
7
+ import { VesuMultiplyAdapter } from "../../universal-adapters/vesu-multiply-adapter";
8
+ import { AssetOperationType, AssetOperationStatus } from "@/modules/ExtendedWrapperSDk";
9
+ import { USDC_TOKEN_DECIMALS } from "../utils/constants";
10
+ import {
11
+ calculateDeltaDebtAmount,
12
+ calculateExtendedLevergae,
13
+ calculateVesuLeverage,
14
+ } from "../utils/helper";
15
+
16
+ // ─── State types ───────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Snapshot of a single Vesu pool's position: collateral, debt, and their prices.
20
+ */
21
+ export interface VesuPoolState {
22
+ poolId: ContractAddr;
23
+ collateralToken: TokenInfo;
24
+ debtToken: TokenInfo;
25
+ collateralAmount: Web3Number;
26
+ collateralUsdValue: number;
27
+ debtAmount: Web3Number;
28
+ debtUsdValue: number;
29
+ collateralPrice: number;
30
+ debtPrice: number;
31
+ }
32
+
33
+ /**
34
+ * Snapshot of a single Extended exchange open position.
35
+ */
36
+ export interface ExtendedPositionState {
37
+ instrument: string;
38
+ side: string;
39
+ size: Web3Number;
40
+ valueUsd: Web3Number;
41
+ leverage: string;
42
+ }
43
+
44
+ /**
45
+ * Snapshot of the Extended exchange account-level balance.
46
+ */
47
+ export interface ExtendedBalanceState {
48
+ equity: Web3Number;
49
+ availableForTrade: Web3Number;
50
+ availableForWithdrawal: Web3Number;
51
+ unrealisedPnl: Web3Number;
52
+ balance: Web3Number;
53
+ /**
54
+ * Funds in transit to/from Extended.
55
+ * Positive = deposit in transit (funds left wallet, not yet credited on Extended).
56
+ * Negative = withdrawal in transit (funds left Extended, not yet received in wallet).
57
+ */
58
+ pendingDeposit: Web3Number;
59
+ }
60
+
61
+ /**
62
+ * Generic token balance with USD valuation.
63
+ */
64
+ export interface TokenBalance {
65
+ token: TokenInfo;
66
+ amount: Web3Number;
67
+ usdValue: number;
68
+ }
69
+
70
+ // ─── Solve result types ────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Per-position exposure change on the Extended exchange.
74
+ * Positive delta = increase position size, negative = reduce.
75
+ */
76
+ export interface ExtendedPositionDelta {
77
+ instrument: string;
78
+ delta: Web3Number;
79
+ }
80
+
81
+ /**
82
+ * Per-pool position change on Vesu.
83
+ * debtDelta: positive = borrow more, negative = repay.
84
+ * collateralDelta: positive = add collateral, negative = remove.
85
+ */
86
+ export interface VesuPoolDelta {
87
+ poolId: ContractAddr;
88
+ collateralToken: TokenInfo;
89
+ debtToken: TokenInfo;
90
+ debtDelta: Web3Number;
91
+ collateralDelta: Web3Number;
92
+ collateralPrice: number;
93
+ debtPrice: number;
94
+ }
95
+
96
+ // ─── ExecutionRoute types ───────────────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Enumerates all possible fund-routing paths used during execution.
100
+ */
101
+ export enum RouteType {
102
+ /** P1: Deposit USDC.e from operator wallet directly to Extended exchange */
103
+ WALLET_TO_EXTENDED = 'WALLET_TO_EXTENDED',
104
+ /** P2: USDC from vault allocator → swap to USDC.e → deposit to Extended */
105
+ VA_TO_EXTENDED = 'VA_TO_EXTENDED',
106
+ /** Withdraw from Extended exchange → operator wallet */
107
+ EXTENDED_TO_WALLET = 'EXTENDED_TO_WALLET',
108
+ /** Swap USDC → BTC to deposit to Vesu */
109
+ AVNU_DEPOSIT_SWAP = 'AVNU_DEPOSIT_SWAP',
110
+ /** Increase leverage on Vesu i.e. deposit on vesu, borrow, in one go to create lever */
111
+ VESU_MULTIPLY_INCREASE_LEVER = 'VESU_MULTIPLY_INCREASE_LEVER',
112
+ /** Decrease leverage on Vesu i.e. withdraw from vesu, repay, in one go to reduce leverage */
113
+ VESU_MULTIPLY_DECREASE_LEVER = 'VESU_MULTIPLY_DECREASE_LEVER',
114
+ /** Swap BTC → USDC to withdraw from Vesu */
115
+ AVNU_WITHDRAW_SWAP = 'AVNU_WITHDRAW_SWAP',
116
+ /** Borrow additional USDC from Vesu (when wallet + VA insufficient for Extended) */
117
+ VESU_BORROW = 'VESU_BORROW',
118
+ /** Repay USDC debt to Vesu (debtDelta < 0) */
119
+ VESU_REPAY = 'VESU_REPAY',
120
+ /** Transfer USDC.e from operator wallet to vault allocator */
121
+ WALLET_TO_VA = 'WALLET_TO_VA',
122
+ /** Realize PnL on Extended exchange */
123
+ REALISE_PNL = 'REALISE_PNL',
124
+ /** Increase leverage on Extended exchange i.e. deposit on extended, in one go to create lever */
125
+ EXTENDED_INCREASE_LEVER = 'EXTENDED_INCREASE_LEVER',
126
+ /** Decrease leverage on Extended exchange i.e. withdraw from extended, in one go to reduce leverage */
127
+ EXTENDED_DECREASE_LEVER = 'EXTENDED_DECREASE_LEVER',
128
+
129
+ /** Increase leverage on Extended exchange to max leverage (e.g. 4x from 3x) */
130
+ CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE = 'CRISIS_INCREASE_EXTENDER_MAX_LEVERAGE',
131
+ /** Undo max leverage on Extended exchange to reduce leverage (e.g. 4x to 3x) */
132
+ CRISIS_UNDO_EXTENDED_MAX_LEVERAGE = 'CRISIS_UNDO_EXTENDED_MAX_LEVERAGE',
133
+
134
+ /** Borrow beyyond target HF (e.g. 1.2 from 1.4) */
135
+ CRISIS_BORROW_BEYOND_TARGET_HF = 'CRISIS_BORROW_BEYOND_TARGET_HF',
136
+
137
+ /** Bring liquidity from vault allocator to vault contract (for user withdrawals) */
138
+ BRING_LIQUIDITY = 'BRING_LIQUIDITY',
139
+
140
+ // often a bridge tx is involved. when the solve cycle runs again
141
+ // bridges funds shall be handled again
142
+ RETURN_TO_WAIT = 'RETURN_TO_WAIT',
143
+ }
144
+
145
+ // ─── Per-route-type metadata ─────────────────────────────────────────────────
146
+
147
+ /** Common fields shared by every route variant */
148
+ interface RouteBase {
149
+ /** Execution order — lower values execute first */
150
+ priority: number;
151
+ }
152
+
153
+ /** Simple fund-transfer routes: WALLET_TO_EXTENDED, VA_TO_EXTENDED, EXTENDED_TO_WALLET, WALLET_TO_VA */
154
+ export interface TransferRoute extends RouteBase {
155
+ type:
156
+ | RouteType.WALLET_TO_EXTENDED
157
+ | RouteType.VA_TO_EXTENDED
158
+ | RouteType.EXTENDED_TO_WALLET
159
+ | RouteType.WALLET_TO_VA;
160
+ /** Amount to transfer */
161
+ amount: Web3Number;
162
+ }
163
+
164
+ /** AVNU swap routes: AVNU_DEPOSIT_SWAP (USDC→BTC), AVNU_WITHDRAW_SWAP (BTC→USDC) */
165
+ export interface SwapRoute extends RouteBase {
166
+ type: RouteType.AVNU_DEPOSIT_SWAP | RouteType.AVNU_WITHDRAW_SWAP;
167
+ /** Source token symbol */
168
+ fromToken: string;
169
+ /** Source amount */
170
+ fromAmount: Web3Number;
171
+ /** Destination token symbol */
172
+ toToken: string;
173
+ /** Exact output amount (if known; otherwise undefined means best-effort) */
174
+ toAmount?: Web3Number;
175
+ }
176
+
177
+ /** Vesu multiply lever routes: deposit+borrow or withdraw+repay in one go */
178
+ export interface VesuMultiplyRoute extends RouteBase {
179
+ type: RouteType.VESU_MULTIPLY_INCREASE_LEVER | RouteType.VESU_MULTIPLY_DECREASE_LEVER;
180
+ /** Pool to interact with */
181
+ poolId: ContractAddr;
182
+ /** Collateral token info */
183
+ collateralToken: TokenInfo;
184
+ /** Collateral amount delta (positive = deposit, negative = withdraw) */
185
+ marginAmount: Web3Number; // in collateral token units
186
+ swappedCollateralAmount: Web3Number; // debt swapped, in collateral token units
187
+ /** Debt token info */
188
+ debtToken: TokenInfo;
189
+ /** Debt amount delta (positive = borrow, negative = repay) */
190
+ debtAmount: Web3Number;
191
+ }
192
+
193
+ /** Vesu single-side borrow / repay routes */
194
+ export interface VesuDebtRoute extends RouteBase {
195
+ type: RouteType.VESU_BORROW | RouteType.VESU_REPAY;
196
+ /** Pool to interact with */
197
+ poolId: ContractAddr;
198
+ /** Amount to borrow (positive) or repay (negative) */
199
+ amount: Web3Number;
200
+ /** Collateral token info */
201
+ collateralToken: TokenInfo;
202
+ /** Debt token info */
203
+ debtToken: TokenInfo;
204
+ }
205
+
206
+ /** Realise PnL on Extended exchange */
207
+ export interface RealisePnlRoute extends RouteBase {
208
+ type: RouteType.REALISE_PNL;
209
+ /** Amount of PnL to realise (the shortfall beyond available-to-withdraw) */
210
+ amount: Web3Number;
211
+ /** Extended instrument name (e.g. "BTC-USD") */
212
+ instrument: string;
213
+ }
214
+
215
+ /** Increase / decrease leverage on Extended exchange */
216
+ export interface ExtendedLeverRoute extends RouteBase {
217
+ type: RouteType.EXTENDED_INCREASE_LEVER | RouteType.EXTENDED_DECREASE_LEVER;
218
+ /** Change in exposure denominated in the exposure token */
219
+ amount: Web3Number;
220
+ /** Extended instrument name (e.g. "BTC-USD") */
221
+ instrument: string;
222
+ }
223
+
224
+ /** Crisis: temporarily increase / undo max leverage on Extended — no args for now */
225
+ export interface CrisisExtendedLeverRoute extends RouteBase {
226
+ type: RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE | RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE;
227
+ }
228
+
229
+ /** Crisis: borrow beyond the normal target HF */
230
+ export interface CrisisBorrowRoute extends RouteBase {
231
+ type: RouteType.CRISIS_BORROW_BEYOND_TARGET_HF;
232
+ /** Pool to borrow from */
233
+ poolId: ContractAddr;
234
+ /** Token being borrowed */
235
+ token: string;
236
+ /** Amount to borrow */
237
+ amount: Web3Number;
238
+ }
239
+
240
+ /** Bring liquidity from vault allocator to vault contract for user withdrawals */
241
+ export interface BringLiquidityRoute extends RouteBase {
242
+ type: RouteType.BRING_LIQUIDITY;
243
+ /** Amount to bring */
244
+ amount: Web3Number;
245
+ }
246
+
247
+ /** Control routes: RETURN_TO_WAIT (stop execution, resume in next cycle) */
248
+ export interface WaitRoute extends RouteBase {
249
+ type: RouteType.RETURN_TO_WAIT;
250
+ }
251
+
252
+ /**
253
+ * A single step in the execution plan produced by the solver.
254
+ * Discriminated union keyed on `type` — each variant carries only
255
+ * the metadata that specific route needs.
256
+ */
257
+ export type ExecutionRoute =
258
+ | TransferRoute
259
+ | SwapRoute
260
+ | VesuMultiplyRoute
261
+ | VesuDebtRoute
262
+ | RealisePnlRoute
263
+ | ExtendedLeverRoute
264
+ | CrisisExtendedLeverRoute
265
+ | CrisisBorrowRoute
266
+ | BringLiquidityRoute
267
+ | WaitRoute;
268
+
269
+ // ─── Case classification types ──────────────────────────────────────────────────
270
+
271
+ /**
272
+ * Broad category of a detected case. Multiple categories may apply simultaneously.
273
+ */
274
+ export enum CaseCategory {
275
+ LTV_REBALANCE = 'LTV_REBALANCE',
276
+ NEW_DEPOSITS = 'NEW_DEPOSITS',
277
+ WITHDRAWAL = 'WITHDRAWAL',
278
+ EXPOSURE_IMBALANCE = 'EXPOSURE_IMBALANCE',
279
+ MARGIN_CRISIS = 'MARGIN_CRISIS',
280
+ }
281
+
282
+ /**
283
+ * Specific case IDs corresponding to the situations defined in cases.json.
284
+ */
285
+ export enum CaseId {
286
+ // LTV Rebalance
287
+ LTV_VESU_LOW_TO_EXTENDED = 'LTV_VESU_LOW_TO_EXTENDED',
288
+ LTV_EXTENDED_PROFITABLE_AVAILABLE = 'LTV_EXTENDED_PROFITABLE_AVAILABLE',
289
+ LTV_EXTENDED_PROFITABLE_REALIZE = 'LTV_EXTENDED_PROFITABLE_REALIZE',
290
+ LTV_VESU_HIGH_USE_VA_OR_WALLET = 'LTV_VESU_HIGH_USE_VA_OR_WALLET',
291
+ LTV_EXTENDED_HIGH_USE_VA_OR_WALLET = 'LTV_EXTENDED_HIGH_USE_VA_OR_WALLET',
292
+
293
+ // New Deposits / Excess Funds
294
+ DEPOSIT_FRESH_VAULT = 'DEPOSIT_FRESH_VAULT',
295
+ DEPOSIT_EXTENDED_AVAILABLE = 'DEPOSIT_EXTENDED_AVAILABLE',
296
+ DEPOSIT_VESU_BORROW_CAPACITY = 'DEPOSIT_VESU_BORROW_CAPACITY',
297
+ DEPOSIT_COMBINATION = 'DEPOSIT_COMBINATION',
298
+ // Withdrawal
299
+ WITHDRAWAL_SIMPLE = 'WITHDRAWAL_SIMPLE',
300
+ // Margin Crisis
301
+ MARGIN_CRISIS_EXTENDED = 'MARGIN_CRISIS_EXTENDED',
302
+ MARGIN_CRISIS_VESU = 'MARGIN_CRISIS_VESU',
303
+ // Exposure Imbalance
304
+ IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS = 'IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS',
305
+ IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS = 'IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS',
306
+ IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS = 'IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS',
307
+ IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS = 'IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS',
308
+ }
309
+
310
+ /**
311
+ * A detected case describing the current system state and the
312
+ * high-level steps required to resolve it.
313
+ */
314
+ export interface SolveCase {
315
+ id: CaseId;
316
+ category: CaseCategory;
317
+ title: string;
318
+ description: string;
319
+ /** High-level steps describing what needs to happen for this case */
320
+ steps: string[];
321
+ }
322
+
323
+ /** Minimum USD amount to consider a case significant */
324
+ const CASE_THRESHOLD_USD = 5;
325
+ const CASE_MIN_BRIDING_USD = 10;
326
+ /** Decimal places for rounding collateral / exposure deltas in token terms (e.g. BTC) */
327
+ const COLLATERAL_PRECISION = 4;
328
+
329
+ /** Safely create a Web3Number from a float, avoiding >15 significant digit errors */
330
+ function safeUsdcWeb3Number(value: number): Web3Number {
331
+ return new Web3Number(value.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
332
+ }
333
+
334
+ /**
335
+ * A single detected case with its metadata, amounts, and the execution
336
+ * routes that belong to this case.
337
+ */
338
+ export interface SolveCaseEntry {
339
+ case: SolveCase;
340
+ additionalArgs: { amount?: Web3Number };
341
+ routes: ExecutionRoute[];
342
+ }
343
+
344
+ /**
345
+ * Maps each CaseId to the RouteTypes that are relevant for resolving it.
346
+ * Used to filter the global route list into per-case route subsets.
347
+ */
348
+ export const CASE_ROUTE_TYPES: Record<CaseId, RouteType[]> = {
349
+ // LTV Rebalance — Vesu side
350
+ [CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET]: [RouteType.WALLET_TO_VA, RouteType.VESU_REPAY], // use wallet to va if wallet has and va doesnt have enough
351
+ [CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE]: [RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
352
+ [CaseId.LTV_EXTENDED_PROFITABLE_REALIZE]: [RouteType.REALISE_PNL, RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
353
+ // LTV Rebalance — Extended side
354
+ [CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET]: [RouteType.VA_TO_EXTENDED, RouteType.WALLET_TO_EXTENDED],
355
+ [CaseId.LTV_VESU_LOW_TO_EXTENDED]: [RouteType.VESU_BORROW, RouteType.VA_TO_EXTENDED],
356
+
357
+ // New Deposits
358
+ // @dev, when handling routes, after VA_TO_EXTENDED and/or WALLET_TO_EXTENDED, return. bcz, funds take time to reach extended. Anyways, in next cycle, these funds will be computed to increase lever
359
+ // Sequence: fund-movement transfers first (WALLET_TO_EXTENDED, VA_TO_EXTENDED, WALLET_TO_VA, EXTENDED_TO_WALLET),
360
+ // then RETURN_TO_WAIT, then optional second WALLET_TO_VA + RETURN_TO_WAIT, then lever routes.
361
+ [CaseId.DEPOSIT_FRESH_VAULT]: [RouteType.WALLET_TO_EXTENDED, RouteType.VA_TO_EXTENDED, RouteType.WALLET_TO_VA, RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.AVNU_DEPOSIT_SWAP, RouteType.VESU_MULTIPLY_INCREASE_LEVER, RouteType.EXTENDED_INCREASE_LEVER],
362
+ [CaseId.DEPOSIT_EXTENDED_AVAILABLE]: [RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.AVNU_DEPOSIT_SWAP, RouteType.VESU_MULTIPLY_INCREASE_LEVER, RouteType.EXTENDED_INCREASE_LEVER],
363
+ [CaseId.DEPOSIT_VESU_BORROW_CAPACITY]: [RouteType.VESU_BORROW, RouteType.VA_TO_EXTENDED, RouteType.RETURN_TO_WAIT, RouteType.AVNU_DEPOSIT_SWAP, RouteType.VESU_MULTIPLY_INCREASE_LEVER, RouteType.EXTENDED_INCREASE_LEVER],
364
+ [CaseId.DEPOSIT_COMBINATION]: [], // exroutes to be computed on the fly based on above sub routes and where deposit is available
365
+ // Withdrawal
366
+ // Sequence: transfer out first (EXTENDED_TO_WALLET, RETURN_TO_WAIT, WALLET_TO_VA), then unwind lever, then BRING_LIQUIDITY
367
+ [CaseId.WITHDRAWAL_SIMPLE]: [RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.AVNU_WITHDRAW_SWAP, RouteType.EXTENDED_DECREASE_LEVER, RouteType.BRING_LIQUIDITY],
368
+
369
+ // Margin Crisis
370
+ // amounts must be chosen such that, when decresing lever, the LTV reached should be adequate, not high not low.
371
+ [CaseId.MARGIN_CRISIS_EXTENDED]: [RouteType.CRISIS_BORROW_BEYOND_TARGET_HF, RouteType.VA_TO_EXTENDED, RouteType.RETURN_TO_WAIT, RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.EXTENDED_DECREASE_LEVER, RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE],
372
+ [CaseId.MARGIN_CRISIS_VESU]: [RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE, RouteType.EXTENDED_TO_WALLET, RouteType.WALLET_TO_VA, RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.EXTENDED_DECREASE_LEVER, RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE],
373
+
374
+ // Exposure Imbalance
375
+ // try to use available funds in VA/Wallet to increase lever in vesu. if not possible, reduce lever on extended for remaining
376
+ [CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS]: [RouteType.AVNU_DEPOSIT_SWAP, RouteType.VESU_MULTIPLY_INCREASE_LEVER, RouteType.EXTENDED_DECREASE_LEVER],
377
+
378
+ // no available funds in VA/Wallet to increase lever in vesu. reduce lever on extended for remaining
379
+ [CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS]: [RouteType.EXTENDED_DECREASE_LEVER],
380
+
381
+ // try to use available funds in VA/Wallet to increase lever in extended. if not possible, reduce lever on vesu for remaining
382
+ [CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS]: [RouteType.VA_TO_EXTENDED, RouteType.WALLET_TO_EXTENDED, RouteType.RETURN_TO_WAIT, RouteType.EXTENDED_INCREASE_LEVER, RouteType.RETURN_TO_WAIT, RouteType.VESU_MULTIPLY_DECREASE_LEVER],
383
+
384
+ // no available funds in VA/Wallet to increase lever in extended. reduce lever on vesu for remaining
385
+ [CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS]: [RouteType.VESU_MULTIPLY_DECREASE_LEVER],
386
+ };
387
+
388
+ /** Minimum exposure imbalance fraction to flag an imbalance case */
389
+ const IMBALANCE_THRESHOLD_FRACTION = 0.0002; // 0.02%
390
+
391
+ /**
392
+ * Static case definitions derived from cases.json.
393
+ * Each entry maps a CaseId to its metadata and descriptive steps.
394
+ */
395
+ const CASE_DEFINITIONS: Record<CaseId, SolveCase> = {
396
+ [CaseId.LTV_VESU_LOW_TO_EXTENDED]: {
397
+ id: CaseId.LTV_VESU_LOW_TO_EXTENDED,
398
+ category: CaseCategory.LTV_REBALANCE,
399
+ title: 'LTV Rebalance: Vesu Low LTV → Extended (No BTC Imbalance)',
400
+ description: 'Exposures are balanced on both sides, but Vesu has low LTV (can borrow more). Extended needs additional margin.',
401
+ steps: [
402
+ 'Borrow additional USDC from Vesu (up to target LTV)',
403
+ 'Vault → Extended: Deposit borrowed USDC as margin',
404
+ ],
405
+ },
406
+ [CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE]: {
407
+ id: CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE,
408
+ category: CaseCategory.LTV_REBALANCE,
409
+ title: 'LTV Rebalance: Extended Profitable → Vesu (Funds Available to Withdraw)',
410
+ description: 'Exposures balanced. Extended is profitable with sufficient available-to-withdraw funds. Vesu needs more collateral.',
411
+ steps: [
412
+ 'Withdraw USDC from Extended to Wallet',
413
+ 'Wallet → Vault: Transfer USDC',
414
+ 'Vault → Vesu: Deposit USDC to repay',
415
+ ],
416
+ },
417
+ [CaseId.LTV_EXTENDED_PROFITABLE_REALIZE]: {
418
+ id: CaseId.LTV_EXTENDED_PROFITABLE_REALIZE,
419
+ category: CaseCategory.LTV_REBALANCE,
420
+ title: 'LTV Rebalance: Extended Profitable → Vesu (Need to Realize PnL)',
421
+ description: 'Exposures balanced. Extended is profitable but available-to-withdraw is insufficient. Unrealized PnL needs to be realized.',
422
+ steps: [
423
+ 'Partially close short BTC position on Extended (realize PnL)',
424
+ 'Immediately reopen short BTC position on Extended (maintain exposure)',
425
+ 'Withdraw realized USDC from Extended to Wallet',
426
+ 'Wallet → Vault: Transfer USDC',
427
+ 'Vault → Vesu: Deposit USDC as collateral',
428
+ ],
429
+ },
430
+ [CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET]: {
431
+ id: CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET,
432
+ category: CaseCategory.LTV_REBALANCE,
433
+ title: 'LTV Rebalance: Vesu High LTV (Use VA or Wallet)',
434
+ description: 'Vesu has high LTV (can repay debt). funds available in VA and/or Wallet can be used to repay debt.',
435
+ steps: [
436
+ 'Wallet → Vesu -> Vesu: Repay using USDC',
437
+ ],
438
+ },
439
+ [CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET]: {
440
+ id: CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET,
441
+ category: CaseCategory.LTV_REBALANCE,
442
+ title: 'LTV Rebalance: Extended High LTV (Use VA or Wallet)',
443
+ description: 'Extended has high LTV (can borrow more). funds available in VA and/or Wallet can be used to add margin',
444
+ steps: [
445
+ 'VA or Wallet → Extended -> Extended: Borrow using USDC',
446
+ ],
447
+ },
448
+ [CaseId.DEPOSIT_FRESH_VAULT]: {
449
+ id: CaseId.DEPOSIT_FRESH_VAULT,
450
+ category: CaseCategory.NEW_DEPOSITS,
451
+ title: 'New Deposits/Excess Funds: Fresh USDC in Vault',
452
+ description: 'Fresh USDC deposits received in Vault contract.',
453
+ steps: [
454
+ 'Unused funds in Vault or in Wallet',
455
+ 'Calculate allocation amounts for Extended and Vesu',
456
+ 'Vault and/or Wallet → Extended: Deposit allocated USDC',
457
+ 'Once extended receives the funds, open short BTC position',
458
+ 'Vault and/or Wallet → Vesu: Deposit allocated USDC, borrow USDC, open long BTC position',
459
+ ],
460
+ },
461
+ [CaseId.DEPOSIT_EXTENDED_AVAILABLE]: {
462
+ id: CaseId.DEPOSIT_EXTENDED_AVAILABLE,
463
+ category: CaseCategory.NEW_DEPOSITS,
464
+ title: 'New Deposits/Excess Funds: Available to Withdraw from Extended',
465
+ description: 'Extended has excess available-to-withdraw funds that can be deployed without LTV rebalancing or BTC imbalance concerns.',
466
+ steps: [
467
+ 'Calculate position increase amounts for both protocols',
468
+ 'Withdraw portion of funds from Extended to Wallet (if needed for Vesu). If part of the required funds are in unrealised PnL, convert to realised PnL.',
469
+ 'Once wallet receives the funds, use available funds on Extended to increase short BTC position',
470
+ 'Wallet → Vault → Vesu: Deposit USDC as collateral',
471
+ 'Borrow USDC from Vesu, open additional long BTC position',
472
+ ],
473
+ },
474
+ [CaseId.DEPOSIT_VESU_BORROW_CAPACITY]: {
475
+ id: CaseId.DEPOSIT_VESU_BORROW_CAPACITY,
476
+ category: CaseCategory.NEW_DEPOSITS,
477
+ title: 'New Deposits/Excess Funds: Available Borrowing Capacity on Vesu',
478
+ description: 'Vesu has low LTV with excess borrowing capacity that can be deployed for position expansion.',
479
+ steps: [
480
+ 'Borrow additional USDC from Vesu (within safe LTV limits)',
481
+ 'Calculate allocation for position expansion',
482
+ 'Deposit portion of funds on Extended',
483
+ 'Once extended receives the funds, open short BTC position',
484
+ 'Use portion to open additional long BTC position on Vesu',
485
+ ],
486
+ },
487
+ [CaseId.DEPOSIT_COMBINATION]: {
488
+ id: CaseId.DEPOSIT_COMBINATION,
489
+ category: CaseCategory.NEW_DEPOSITS,
490
+ title: 'New Deposits/Excess Funds: Combination case',
491
+ description: 'Combination of new deposits/excess funds on vault/extended/vesu.',
492
+ steps: [
493
+ 'Optimally distribute funds from multiple sources and create new positions. Avoid unnecessary routes.',
494
+ ],
495
+ },
496
+ [CaseId.WITHDRAWAL_SIMPLE]: {
497
+ id: CaseId.WITHDRAWAL_SIMPLE,
498
+ category: CaseCategory.WITHDRAWAL,
499
+ title: 'User Withdrawal: Simple Position Reduction',
500
+ description: 'User requests withdrawal with sufficient available funds to close positions proportionally.',
501
+ steps: [
502
+ 'First check if any available funds can be used to settle the withdrawal (vault, available to withdraw or borrowing from vesu)',
503
+ 'If yes, do necessary transfers such that funds are available in vault',
504
+ 'If not or if more required, calculate position sizes to close on both sides',
505
+ 'Close long BTC position on Vesu (partially or fully)',
506
+ 'Repay borrowed USDC on Vesu',
507
+ 'Withdraw collateral from Vesu to Vault',
508
+ 'Close short BTC position on Extended',
509
+ 'Withdraw margin from Extended to Wallet',
510
+ 'Wallet → Vault: Transfer USDC',
511
+ 'Vault → User: Process withdrawal',
512
+ ],
513
+ },
514
+ [CaseId.MARGIN_CRISIS_EXTENDED]: {
515
+ id: CaseId.MARGIN_CRISIS_EXTENDED,
516
+ category: CaseCategory.MARGIN_CRISIS,
517
+ title: 'Margin Crisis: LTV rebalance requires more funds than available, margin required on Extended',
518
+ description: 'LTV rebalance requires more funds than available on vault/extended/vesu. Extended needs margin urgently.',
519
+ steps: [
520
+ 'Calculate required position reduction in vesu and extended',
521
+ 'Borrow more on vesu such that HF can go max up to 1.2',
522
+ 'Use the borrowed funds to settle margin requirements on Extended (vault to extended)',
523
+ 'Once funds reach on extended, close portion of long BTC position on Vesu',
524
+ 'Convert BTC to USDC',
525
+ 'Repay portion of borrowed USDC on Vesu such that HF can go back to target',
526
+ 'Close corresponding short BTC position on Extended (match reduced exposure)',
527
+ ],
528
+ },
529
+ [CaseId.MARGIN_CRISIS_VESU]: {
530
+ id: CaseId.MARGIN_CRISIS_VESU,
531
+ category: CaseCategory.MARGIN_CRISIS,
532
+ title: 'Margin Crisis: LTV rebalance requires more funds than available, margin required on Vesu',
533
+ description: 'LTV rebalance requires more funds than available on vault/extended/vesu. Vesu needs margin urgently.',
534
+ steps: [
535
+ 'Calculate required position reduction in extended',
536
+ 'Update leverage to 4x and try to use available to withdraw funds on extended',
537
+ 'Wait until funds reach on wallet',
538
+ 'Close portion of short BTC position on Extended',
539
+ 'Move wallet to vault and use the USDC to repay on vesu',
540
+ 'Reduce long position to match the short position on extended',
541
+ ],
542
+ },
543
+ [CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS]: {
544
+ id: CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS,
545
+ category: CaseCategory.EXPOSURE_IMBALANCE,
546
+ title: 'BTC Imbalance: Extended Has Excess Short (No funds to match)',
547
+ description: 'Extended has more short BTC exposure than Vesu long. No funds available to increase Vesu long.',
548
+ steps: [
549
+ 'Reduce short BTC position on Extended to match the long position on Vesu',
550
+ ],
551
+ },
552
+ [CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS]: {
553
+ id: CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS,
554
+ category: CaseCategory.EXPOSURE_IMBALANCE,
555
+ title: 'BTC Imbalance: Extended Has Excess Short (Vault/Wallet Has Funds)',
556
+ description: 'Extended has more short BTC exposure than Vesu long. Use available funds to increase long on Vesu.',
557
+ steps: [
558
+ 'Use available funds to increase long on Vesu as much as possible',
559
+ 'If still not matched, reduce short BTC position on Extended to match the long position on Vesu',
560
+ ],
561
+ },
562
+ [CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS]: {
563
+ id: CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS,
564
+ category: CaseCategory.EXPOSURE_IMBALANCE,
565
+ title: 'BTC Imbalance: Vesu Has Excess Long (No funds to match)',
566
+ description: 'Vesu has more long BTC exposure than Extended short. Extended does not have enough available to trade funds.',
567
+ steps: [
568
+ 'Reduce long BTC position on Vesu to match the short position on Extended',
569
+ ],
570
+ },
571
+ [CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS]: {
572
+ id: CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS,
573
+ category: CaseCategory.EXPOSURE_IMBALANCE,
574
+ title: 'BTC Imbalance: Vesu Has Excess Long (Extended has funds)',
575
+ description: 'Vesu has more long BTC exposure than Extended short. Extended has available to trade funds to match.',
576
+ steps: [
577
+ 'Use available funds to increase short on Extended as much as possible',
578
+ 'If still not matched, reduce long BTC position on Vesu to match the short position on Extended',
579
+ ],
580
+ },
581
+ };
582
+
583
+ /**
584
+ * Complete output from the solver describing all needed state changes.
585
+ *
586
+ * - cases: detected cases describing the system state and needed actions.
587
+ * - extendedDeposit: positive = deposit USDC into Extended, negative = withdraw.
588
+ * - extendedPositionDeltas: per-instrument exposure changes (with instrument id).
589
+ * - vesuDeltas: per-pool debt & collateral changes (with pool id and token pair).
590
+ * - vesuAllocationUsd: USDC directed to Vesu side (negative = unwind from Vesu).
591
+ * - extendedAllocationUsd: USDC directed to Extended side (negative = withdraw from Extended).
592
+ * - bringLiquidityAmount: amount VA should send via bringLiquidity to the vault
593
+ * (= withdrawAmount input; 0 during investment cycles).
594
+ */
595
+ export interface SolveResult {
596
+ /** Detected cases describing the current situation, amounts, and per-case routes */
597
+ cases: SolveCaseEntry[];
598
+ extendedDeposit: Web3Number;
599
+ extendedPositionDeltas: ExtendedPositionDelta[];
600
+ vesuDepositAmount: Web3Number;
601
+ vesuDeltas: VesuPoolDelta[];
602
+ vesuAllocationUsd: Web3Number;
603
+ extendedAllocationUsd: Web3Number;
604
+ bringLiquidityAmount: Web3Number;
605
+ /**
606
+ * Net pending deposit for Extended.
607
+ * Positive = deposit in transit (wallet → Extended), negative = withdrawal in transit.
608
+ * Used by ExecutionService to avoid double-sending transfers.
609
+ */
610
+ pendingDeposit: Web3Number;
611
+ }
612
+
613
+ // ─── Config ────────────────────────────────────────────────────────────────────
614
+
615
+ export interface StateManagerConfig {
616
+ pricer: PricerBase;
617
+ networkConfig: IConfig;
618
+ vesuAdapters: VesuMultiplyAdapter[];
619
+ extendedAdapter: ExtendedAdapter;
620
+ vaultAllocator: ContractAddr;
621
+ walletAddress: string;
622
+ assetToken: TokenInfo;
623
+ /** USDC.e token for wallet balance checks during route computation */
624
+ usdceToken: TokenInfo;
625
+ /** Collateral token (e.g. WBTC) for wallet balance checks */
626
+ collateralToken: TokenInfo;
627
+ limitBalanceBufferFactor: number;
628
+ }
629
+
630
+ // ─── ExecutionRoute helpers ──────────────────────────────────────────────────────────────
631
+
632
+ /**
633
+ * Returns a short human-readable summary of a route's key fields for logging.
634
+ */
635
+ export function routeSummary(r: ExecutionRoute): string {
636
+ switch (r.type) {
637
+ case RouteType.WALLET_TO_EXTENDED:
638
+ case RouteType.VA_TO_EXTENDED:
639
+ case RouteType.EXTENDED_TO_WALLET:
640
+ case RouteType.WALLET_TO_VA:
641
+ return `${r.amount.toNumber()}`;
642
+ case RouteType.AVNU_DEPOSIT_SWAP:
643
+ case RouteType.AVNU_WITHDRAW_SWAP:
644
+ return `${r.fromAmount.toNumber()} ${r.fromToken}→${r.toToken}`;
645
+ case RouteType.VESU_MULTIPLY_INCREASE_LEVER:
646
+ case RouteType.VESU_MULTIPLY_DECREASE_LEVER:
647
+ return `coll=${r.marginAmount.toNumber()} [${r.marginAmount.decimals}] debt=${r.debtAmount.toNumber()} [${r.debtAmount.decimals}] (swapped ${r.swappedCollateralAmount.toNumber()} [${r.swappedCollateralAmount.decimals}]) pool=${r.poolId.shortString()}`;
648
+ case RouteType.VESU_BORROW:
649
+ case RouteType.VESU_REPAY:
650
+ return `${r.amount.toNumber()} ${r.debtToken.symbol} pool=${r.poolId.shortString()} [Collateral: ${r.collateralToken.symbol}]`;
651
+ case RouteType.REALISE_PNL:
652
+ return `${r.amount.toNumber()} ${r.instrument}`;
653
+ case RouteType.EXTENDED_INCREASE_LEVER:
654
+ case RouteType.EXTENDED_DECREASE_LEVER:
655
+ return `${r.amount.toNumber()} ${r.instrument}`;
656
+ case RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE:
657
+ case RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE:
658
+ return '(no args)';
659
+ case RouteType.CRISIS_BORROW_BEYOND_TARGET_HF:
660
+ return `${r.amount.toNumber()} ${r.token} pool=${r.poolId.shortString()}`;
661
+ case RouteType.BRING_LIQUIDITY:
662
+ return `${r.amount.toNumber()}`;
663
+ case RouteType.RETURN_TO_WAIT:
664
+ return '(control)';
665
+ default:
666
+ return '';
667
+ }
668
+ }
669
+
670
+ // ─── Solve Budget ───────────────────────────────────────────────────────────────
671
+
672
+ /**
673
+ * Single source of truth for all mutable state during a solve() call.
674
+ *
675
+ * Holds both the refreshed on-chain/off-chain state snapshots AND the
676
+ * budget-tracking values consumed by sub-classifiers. Spend methods
677
+ * automatically keep `totalUnused` in sync. State-mutation methods
678
+ * (`applyVesuDelta`, `applyExtendedExposureDelta`, `applyExtendedBalanceChange`)
679
+ * update the underlying snapshots so that downstream classifiers always
680
+ * see the most up-to-date picture.
681
+ */
682
+ export class SolveBudget {
683
+ // ── Refreshed state (mutable during solve) ──────────────────────────
684
+ unusedBalance: TokenBalance[];
685
+ walletBalance: TokenBalance | null;
686
+ vaultBalance: TokenBalance | null;
687
+ extendedPositions: ExtendedPositionState[];
688
+ extendedBalance: ExtendedBalanceState | null;
689
+ vesuPoolStates: VesuPoolState[];
690
+ vesuPerPoolDebtDeltasToBorrow: Web3Number[];
691
+ shouldVesuRebalance: boolean[]; // should be same length as vesuPerPoolDebtDeltasToBorrow
692
+
693
+ // ── Budget tracking (populated by initBudget) ──────────────────────
694
+ private _vaUsd: number = 0;
695
+ private _walletUsd: number = 0;
696
+ private _extAvailWithdraw: number = 0;
697
+ private _extAvailUpnl: number = 0;
698
+ private _extAvailTrade: number = 0;
699
+ private _extPendingDeposit: number = 0;
700
+ private _vesuBorrowCapacity: number = 0;
701
+ private _totalUnused: number = 0;
702
+
703
+ constructor(state: {
704
+ limitBalanceBufferFactor: number;
705
+ unusedBalance: TokenBalance[];
706
+ walletBalance: TokenBalance | null;
707
+ vaultBalance: TokenBalance | null;
708
+ extendedPositions: ExtendedPositionState[];
709
+ extendedBalance: ExtendedBalanceState | null;
710
+ vesuPoolStates: VesuPoolState[];
711
+ }) {
712
+ const buffer = state.limitBalanceBufferFactor;
713
+ this.unusedBalance = state.unusedBalance.map((item) => {
714
+ return {
715
+ ...item,
716
+ amount: item.amount.multipliedBy(1 - buffer),
717
+ usdValue: item.usdValue * (1 - buffer),
718
+ };
719
+ });
720
+ this.walletBalance = state.walletBalance ? {
721
+ ...state.walletBalance,
722
+ amount: state.walletBalance.amount.multipliedBy(1 - buffer),
723
+ usdValue: state.walletBalance.usdValue * (1 - buffer),
724
+ } : null;
725
+ this.vaultBalance = state.vaultBalance ? {
726
+ ...state.vaultBalance,
727
+ amount: state.vaultBalance.amount.multipliedBy(1 - buffer),
728
+ usdValue: state.vaultBalance.usdValue * (1 - buffer),
729
+ } : null;
730
+ this.extendedPositions = state.extendedPositions;
731
+ this.extendedBalance = state.extendedBalance ? {
732
+ ...state.extendedBalance,
733
+ availableForTrade: state.extendedBalance.availableForTrade.multipliedBy(1 - buffer),
734
+ availableForWithdrawal: state.extendedBalance.availableForWithdrawal.multipliedBy(1 - buffer),
735
+ unrealisedPnl: state.extendedBalance.unrealisedPnl.multipliedBy(1 - buffer),
736
+ balance: state.extendedBalance.balance.multipliedBy(1 - buffer),
737
+ } : null;
738
+ this.vesuPoolStates = state.vesuPoolStates;
739
+ const vesuPerPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
740
+ assert(vesuPerPoolDebtDeltasToBorrow.length === this.vesuPoolStates.length, 'vesuPerPoolDebtDeltasToBorrow length must match vesuPoolStates length');
741
+ this.vesuPerPoolDebtDeltasToBorrow = vesuPerPoolDebtDeltasToBorrow.map((item) => item.deltaDebt);
742
+ this.shouldVesuRebalance = vesuPerPoolDebtDeltasToBorrow.map((item) => item.shouldRebalance);
743
+ }
744
+
745
+ /**
746
+ * Initialise budget-tracking values from the current state snapshot.
747
+ * Must be called after state is populated and debt deltas are computed.
748
+ *
749
+ * Accounts for pendingDeposit:
750
+ * - pendingDeposit > 0: funds in transit TO Extended → increase effective Extended available-for-trade
751
+ * - pendingDeposit < 0: funds in transit FROM Extended → increase effective wallet balance
752
+ */
753
+ initBudget(): void {
754
+ const debtDeltaNum = this.vesuPerPoolDebtDeltasToBorrow.reduce((a, b) => a + b.toNumber(), 0);
755
+ let totalUnusedUsd = this.unusedBalance.reduce((a, b) => a + b.usdValue, 0);
756
+ if (debtDeltaNum > 0) totalUnusedUsd += debtDeltaNum;
757
+ const extAvailTrade = this.extendedBalance?.availableForTrade?.toNumber() ?? 0;
758
+ if (extAvailTrade > 0) totalUnusedUsd += extAvailTrade;
759
+ if (debtDeltaNum > 0) totalUnusedUsd += debtDeltaNum;
760
+
761
+ this._vaUsd = this.vaultBalance?.usdValue ?? 0;
762
+ this._walletUsd = this.walletBalance?.usdValue ?? 0;
763
+ this._extAvailWithdraw = this.extendedBalance?.availableForWithdrawal?.toNumber() ?? 0;
764
+ this._extAvailUpnl = this.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
765
+ this._extAvailTrade = extAvailTrade;
766
+ this._extPendingDeposit = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
767
+ this._vesuBorrowCapacity = debtDeltaNum;
768
+ this._totalUnused = totalUnusedUsd;
769
+
770
+ // ! cannot assume like this. the assumpotion, raher is opposite. also, be
771
+ const pendingDeposit = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
772
+ if (pendingDeposit > 0) {
773
+ this._extAvailTrade += pendingDeposit;
774
+ this._totalUnused += pendingDeposit;
775
+ logger.debug(`SolveBudget::initBudget pendingDeposit=${pendingDeposit} -> increased extAvailTrade`);
776
+ } else if (pendingDeposit < 0) {
777
+ const inTransit = Math.abs(pendingDeposit);
778
+ this._walletUsd += inTransit;
779
+ this._totalUnused += inTransit;
780
+ logger.debug(`SolveBudget::initBudget pendingDeposit=${pendingDeposit} -> increased walletUsd by ${inTransit}`);
781
+ }
782
+ }
783
+
784
+ // ── Read-only getters ────────────────────────────────────────────────
785
+
786
+ get vaUsd(): number { return this._vaUsd; }
787
+ get walletUsd(): number { return this._walletUsd; }
788
+ get vaWalletUsd(): number { return this._vaUsd + this._walletUsd; }
789
+ get extAvailWithdraw(): number { return this._extAvailWithdraw; }
790
+ get extAvailUpnl(): number { return this._extAvailUpnl; }
791
+ get extAvailTrade(): number { return this._extAvailTrade; }
792
+ get vesuBorrowCapacity(): number { return this._vesuBorrowCapacity; }
793
+ get totalUnused(): number { return this._totalUnused; }
794
+ get extPendingDeposit(): number { return this._extPendingDeposit; }
795
+
796
+ // ── Spend methods (return amount consumed, auto-decrement totalUnused) ─
797
+
798
+ spendVA(desired: number): number {
799
+ const used = Math.min(this._vaUsd, Math.max(0, desired));
800
+ this._vaUsd -= used;
801
+ this._totalUnused -= used;
802
+ logger.debug(`SolveBudget::spendVA used=${used}, vaUsd=${this._vaUsd}, totalUnused=${this._totalUnused}`);
803
+ return used;
804
+ }
805
+
806
+ addToVA(amount: number): void {
807
+ assert(amount >= 0, 'SolveBudget::addToVA amount must be positive');
808
+ this._vaUsd += amount;
809
+ this._totalUnused += amount;
810
+ logger.debug(`SolveBudget::addToVA amount=${amount}, vaUsd=${this._vaUsd}, totalUnused=${this._totalUnused}`);
811
+ }
812
+
813
+ spendWallet(desired: number): number {
814
+ const used = Math.min(this._walletUsd, Math.max(0, desired));
815
+ this._walletUsd -= used;
816
+ this._totalUnused -= used;
817
+ logger.debug(`SolveBudget::spendWallet used=${used}, walletUsd=${this._walletUsd}, totalUnused=${this._totalUnused}`);
818
+ return used;
819
+ }
820
+
821
+ addToWallet(amount: number): void {
822
+ assert(amount >= 0, 'SolveBudget::addToWallet amount must be positive');
823
+ this._walletUsd += amount;
824
+ this._totalUnused += amount;
825
+ logger.debug(`SolveBudget::addToWallet amount=${amount}, walletUsd=${this._walletUsd}, totalUnused=${this._totalUnused}`);
826
+ }
827
+
828
+ spendVAWallet(desired: number): number {
829
+ let remaining = Math.max(0, desired);
830
+ const vaSpent = this.spendVA(remaining);
831
+ remaining -= vaSpent;
832
+ const walletSpent = this.spendWallet(remaining);
833
+ return vaSpent + walletSpent;
834
+ }
835
+
836
+ private _updateExtAvailWithdraw(desired: number, isSpend: boolean): number {
837
+ let amount = desired;
838
+ assert(amount > 0, 'SolveBudget::_updateExtAvailWithdraw amount must be positive');
839
+ if (isSpend) {
840
+ amount = Math.min(this._extAvailWithdraw, Math.max(0, desired));
841
+ amount = -amount; // invert sign
842
+ }
843
+ this._extAvailWithdraw += amount;
844
+ this._totalUnused += amount;
845
+ this._extAvailTrade += amount;
846
+
847
+ // update extended balances
848
+ if (this.extendedBalance) {
849
+ this.extendedBalance.availableForWithdrawal = safeUsdcWeb3Number(this.extendedBalance.availableForWithdrawal.toNumber() + amount);
850
+ this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() + amount);
851
+ this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + amount);
852
+ this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + amount);
853
+ }
854
+ logger.debug(`SolveBudget::updateExtAvailWithdraw amount=${amount}, extAvailWithdraw=${this._extAvailWithdraw}, totalUnused=${this._totalUnused}`);
855
+ return amount;
856
+ }
857
+
858
+ private _updateExtAvailUpnl(desired: number, isSpend: boolean): number {
859
+ let amount = desired;
860
+ assert(amount > 0, 'SolveBudget::_updateExtAvailUpnl amount must be positive');
861
+ if (isSpend) {
862
+ amount = Math.min(this._extAvailUpnl, Math.max(0, desired));
863
+ amount = -amount; // invert sign
864
+ }
865
+ this._extAvailUpnl += amount;
866
+ this._totalUnused += amount;
867
+
868
+ // update extended balances
869
+ if (this.extendedBalance) {
870
+ this.extendedBalance.unrealisedPnl = safeUsdcWeb3Number(this.extendedBalance.unrealisedPnl.toNumber() + amount);
871
+ this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + amount);
872
+ this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + amount);
873
+ this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() + amount);
874
+ }
875
+ logger.debug(`SolveBudget::updateExtAvailUpnl amount=${amount}, extAvailUpnl=${this._extAvailUpnl}, totalUnused=${this._totalUnused}`);
876
+ return amount;
877
+ }
878
+
879
+ spendExtAvailTrade(desired: number): number {
880
+ const used = this._updateExtAvailWithdraw(desired, true);
881
+ const usedUpnl = this._updateExtAvailUpnl(desired, true);
882
+ logger.debug(`SolveBudget::updateExtAvailTrade amount=${used + usedUpnl}, extAvailTrade=${this._extAvailTrade}, totalUnused=${this._totalUnused}`);
883
+ return used + usedUpnl;
884
+ }
885
+
886
+ spendExtAvailUpnl(desired: number): number {
887
+ return this._updateExtAvailUpnl(desired, true);
888
+ }
889
+
890
+ addToExtAvailTrade(amount: number): void {
891
+ this._updateExtAvailWithdraw(amount, false);
892
+ logger.debug(`SolveBudget::addToExtAvailTrade amount=${amount}, extAvailTrade=${this._extAvailTrade}, totalUnused=${this._totalUnused}`);
893
+ }
894
+
895
+ spendVesuBorrowCapacity(desired: number): { used: number, spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] } {
896
+ const used = Math.min(this._vesuBorrowCapacity, Math.max(0, desired));
897
+ this._vesuBorrowCapacity -= used;
898
+ // todo check this function is used correctly
899
+ this._totalUnused -= used; // we assume only borrowed till ideal LTV
900
+
901
+ let spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] = [];
902
+ // reduce the debt delta for the pool
903
+ for (let index = 0; index < this.vesuPerPoolDebtDeltasToBorrow.length; index++) {
904
+ const d = this.vesuPerPoolDebtDeltasToBorrow[index];
905
+ if (d.toNumber() <= CASE_THRESHOLD_USD) continue;
906
+ const borrowed = Math.min(d.toNumber(), used);
907
+ this.vesuPerPoolDebtDeltasToBorrow[index] = d.minus(safeUsdcWeb3Number(borrowed));
908
+ this.vesuPoolStates[index].debtAmount = safeUsdcWeb3Number(this.vesuPoolStates[index].debtAmount.toNumber() + borrowed);
909
+ this.vesuPoolStates[index].debtUsdValue = this.vesuPoolStates[index].debtAmount.toNumber() * this.vesuPoolStates[index].debtPrice;
910
+ spendsByPool.push({ poolId: this.vesuPoolStates[index].poolId, amount: safeUsdcWeb3Number(borrowed), collateralToken: this.vesuPoolStates[index].collateralToken, debtToken: this.vesuPoolStates[index].debtToken });
911
+ }
912
+
913
+ logger.debug(`SolveBudget::spendVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this._vesuBorrowCapacity}, totalUnused=${this._totalUnused}`);
914
+ return { used, spendsByPool };
915
+ }
916
+
917
+ repayVesuBorrowCapacity(desired: number): { used: number, spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] } {
918
+ assert(desired > 0, 'SolveBudget::repayVesuBorrowCapacity desired must be positive');
919
+ // const used = Math.min(this._vesuBorrowCapacity, Math.max(0, desired));
920
+ const used = desired;
921
+ // todo check this function is used correctly
922
+ this._vesuBorrowCapacity += used;
923
+ // wont increase total unused because we are repaying debt
924
+
925
+ const spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] = [];
926
+ for (let index = 0; index < this.vesuPerPoolDebtDeltasToBorrow.length; index++) {
927
+ const d = this.vesuPerPoolDebtDeltasToBorrow[index];
928
+ if (d.toNumber() >= -CASE_THRESHOLD_USD) continue; // positive means, dont repay
929
+ const repaid = Math.min(Math.abs(d.toNumber()), used);
930
+ this.vesuPerPoolDebtDeltasToBorrow[index] = d.plus(safeUsdcWeb3Number(repaid));
931
+ this.vesuPoolStates[index].debtAmount = safeUsdcWeb3Number(this.vesuPoolStates[index].debtAmount.toNumber() - repaid);
932
+ spendsByPool.push({ poolId: this.vesuPoolStates[index].poolId, amount: safeUsdcWeb3Number(-repaid), collateralToken: this.vesuPoolStates[index].collateralToken, debtToken: this.vesuPoolStates[index].debtToken });
933
+ }
934
+
935
+ logger.debug(`SolveBudget::repayVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this._vesuBorrowCapacity}, totalUnused=${this._totalUnused}`);
936
+ return { used, spendsByPool };
937
+ }
938
+
939
+ // ── State mutation ──────────────────────────────────────────────────
940
+
941
+ /** Update a Vesu pool's collateral and debt after a lever / borrow / repay route. */
942
+ applyVesuDelta(poolId: ContractAddr, collToken: TokenInfo, debtToken: TokenInfo, collDelta: Web3Number, debtDelta: Web3Number): void {
943
+ const pool = this.vesuPoolStates.find(
944
+ (p) => p.poolId.toString() === poolId.toString() && p.collateralToken.symbol === collToken.symbol && p.debtToken.symbol === debtToken.symbol,
945
+ );
946
+ if (!pool) throw new Error(`SolveBudget::applyVesuDelta pool not found: poolId=${poolId.toString()}, collToken=${collToken.symbol}, debtToken=${debtToken.symbol}`);
947
+ pool.collateralAmount = new Web3Number(
948
+ pool.collateralAmount.plus(collDelta).toFixed(pool.collateralToken.decimals),
949
+ pool.collateralToken.decimals,
950
+ );
951
+ pool.collateralUsdValue = pool.collateralAmount.toNumber() * pool.collateralPrice;
952
+ pool.debtAmount = new Web3Number(
953
+ pool.debtAmount.plus(debtDelta).toFixed(pool.debtToken.decimals),
954
+ pool.debtToken.decimals,
955
+ );
956
+ pool.debtUsdValue = pool.debtAmount.toNumber() * pool.debtPrice;
957
+
958
+ // recompute per pool deltas here
959
+ const vesuPerPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
960
+ this.vesuPerPoolDebtDeltasToBorrow = vesuPerPoolDebtDeltasToBorrow.map((item) => item.deltaDebt);
961
+ const sum = this.vesuPerPoolDebtDeltasToBorrow.reduce((a, b) => a.plus(b), new Web3Number(0, USDC_TOKEN_DECIMALS));
962
+ this._vesuBorrowCapacity = sum.toNumber();
963
+ this.shouldVesuRebalance = vesuPerPoolDebtDeltasToBorrow.map((item) => item.shouldRebalance);
964
+ }
965
+
966
+ /** Update an Extended position's size after a lever route. Creates the position if new. */
967
+ applyExtendedExposureDelta(instrument: string, sizeDelta: Web3Number): void {
968
+ const pos = this.extendedPositions.find((p) => p.instrument === instrument);
969
+ if (pos) {
970
+ pos.size = new Web3Number(pos.size.plus(sizeDelta).toFixed(8), 8);
971
+ } else if (sizeDelta.toNumber() !== 0) {
972
+ this.extendedPositions.push({
973
+ instrument,
974
+ side: 'SHORT',
975
+ size: sizeDelta,
976
+ valueUsd: new Web3Number(0, USDC_TOKEN_DECIMALS),
977
+ leverage: '0',
978
+ });
979
+ }
980
+ }
981
+
982
+
983
+ /**
984
+ * For each Vesu pool, computes the debt delta needed to bring the position
985
+ * back to the target health factor.
986
+ *
987
+ * Positive = can borrow more, negative = need to repay.
988
+ */
989
+ private _computeperPoolDebtDeltasToBorrow(): { deltaDebt: Web3Number, shouldRebalance: boolean }[] {
990
+ const output = this.vesuPoolStates.map((pool) =>
991
+ calculateDeltaDebtAmount(
992
+ pool.collateralAmount,
993
+ pool.debtAmount,
994
+ pool.debtPrice,
995
+ pool.collateralPrice,
996
+ ),
997
+ );
998
+
999
+ // assert al pools share same collateral and debt tokens
1000
+ const collateralToken = this.vesuPoolStates[0].collateralToken;
1001
+ const debtToken = this.vesuPoolStates[0].debtToken;
1002
+ for (const pool of this.vesuPoolStates) {
1003
+ if (pool.collateralToken.symbol !== collateralToken.symbol || pool.debtToken.symbol !== debtToken.symbol) {
1004
+ // not yet handled in code with multiple pool types. will be handled in future.
1005
+ throw new Error(`SolveBudget::_computeperPoolDebtDeltasToBorrow: All pools must share same collateral and debt tokens`);
1006
+ }
1007
+ }
1008
+ return output;
1009
+ }
1010
+ }
1011
+
1012
+ // ─── State Manager ─────────────────────────────────────────────────────────────
1013
+
1014
+ /**
1015
+ * Reads all on-chain / off-chain state for the Extended + SVK + Vesu strategy,
1016
+ * then solves for the optimal rebalancing deltas.
1017
+ *
1018
+ * Usage:
1019
+ * const manager = new ExtendedSVKVesuStateManager(config);
1020
+ * const result = await manager.solve();
1021
+ */
1022
+ export class ExtendedSVKVesuStateManager {
1023
+ private readonly _config: StateManagerConfig;
1024
+ private readonly _tag = "ExtendedSVKVesuStateManager";
1025
+
1026
+ /** Single mutable state holder — initialised by _refresh(), budget by initBudget(). */
1027
+ private _budget!: SolveBudget;
1028
+
1029
+ constructor(config: StateManagerConfig) {
1030
+ this._config = config;
1031
+ }
1032
+
1033
+ // ═══════════════════════════════════════════════════════════════════════════
1034
+ // Public API
1035
+ // ═══════════════════════════════════════════════════════════════════════════
1036
+
1037
+ /**
1038
+ * Main entry point. Refreshes all state, then computes and returns
1039
+ * the optimal deltas for rebalancing the strategy.
1040
+ *
1041
+ * @param withdrawAmount — amount (in vault asset, e.g. USDC) the user wants
1042
+ * to withdraw. When > 0 the solver shrinks protocol allocations so that the
1043
+ * vault allocator ends up with enough balance to execute bringLiquidity.
1044
+ * Pass 0 (default) for normal investment / rebalancing cycles.
1045
+ */
1046
+ async solve(
1047
+ withdrawAmount: Web3Number = new Web3Number(0, USDC_TOKEN_DECIMALS),
1048
+ ): Promise<SolveResult | null> {
1049
+ await this._refresh();
1050
+ if (Math.abs(this._budget.extPendingDeposit) > 0) {
1051
+ logger.warn(`${this._tag}::solve extPendingDeposit=${this._budget.extPendingDeposit}`);
1052
+ return null;
1053
+ }
1054
+ this._validateRefreshedState();
1055
+
1056
+ // // Step 1: For each Vesu pool, compute debt delta needed for LTV maintenance
1057
+ // const perPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
1058
+
1059
+ // // Step 2: Determine distributable capital after debt and withdrawal adjustments
1060
+ // const distributableAmount = this._computeDistributableAmount(
1061
+ // perPoolDebtDeltasToBorrow,
1062
+ // withdrawAmount,
1063
+ // );
1064
+
1065
+ // // Step 3: Split distributable capital between Vesu and Extended allocations
1066
+ // const { vesuAllocationUsd, extendedAllocationUsd } =
1067
+ // this._computeAllocationSplit(distributableAmount, perPoolDebtDeltasToBorrow);
1068
+
1069
+ // // Step 4: Convert Vesu allocation to per-pool collateral deltas
1070
+ // const vesuDeltas =
1071
+ // this._computePerPoolCollateralDeltas(vesuAllocationUsd, perPoolDebtDeltasToBorrow);
1072
+
1073
+ // // Step 5: Compute Extended position deltas for delta-neutral matching
1074
+ // const extendedPositionDeltas =
1075
+ // this._computeExtendedPositionDeltas(vesuDeltas);
1076
+ // console.log({ extendedPositionDeltas: extendedPositionDeltas.map((delta) => ({
1077
+ // instrument: delta.instrument,
1078
+ // delta: delta.delta.toNumber(),
1079
+ // })) });
1080
+
1081
+ // // Step 6: Compute net deposit/withdrawal change for Extended platform
1082
+ // const extendedDeposit =
1083
+ // this._computeExtendedDepositDelta(extendedAllocationUsd);
1084
+ // console.log({ extendedDeposit: extendedDeposit.toNumber() });
1085
+
1086
+ // // Step 7: compute vesu deposit amount (i.e. collateral required to open position may be)
1087
+ // const vesuDepositAmount =
1088
+ // this._computeVesuDepositAmount(vesuDeltas);
1089
+
1090
+ // // Step 7: Compute bringLiquidity amount (= withdrawAmount for the vault)
1091
+ // const bringLiquidityAmount =
1092
+ // this._computeBringLiquidityAmount(withdrawAmount);
1093
+ // console.log({ bringLiquidityAmount: bringLiquidityAmount.toNumber() });
1094
+
1095
+ // Step 8: Classify state into actionable cases — each case carries
1096
+ // its own routes with amounts and state info.
1097
+ const cases = this._classifyCases(withdrawAmount);
1098
+
1099
+ const result: SolveResult = {
1100
+ cases,
1101
+ // ignore these fields for now. only cases are relevant.
1102
+ extendedDeposit: safeUsdcWeb3Number(0),
1103
+ extendedPositionDeltas: [],
1104
+ vesuDepositAmount: safeUsdcWeb3Number(0),
1105
+ vesuDeltas: [],
1106
+ vesuAllocationUsd: safeUsdcWeb3Number(0),
1107
+ extendedAllocationUsd: safeUsdcWeb3Number(0),
1108
+ bringLiquidityAmount: safeUsdcWeb3Number(0),
1109
+ pendingDeposit: this._budget.extendedBalance?.pendingDeposit ?? new Web3Number(0, USDC_TOKEN_DECIMALS),
1110
+ };
1111
+
1112
+ this._logSolveResult(result);
1113
+ return result;
1114
+ }
1115
+
1116
+ // ═══════════════════════════════════════════════════════════════════════════
1117
+ // Private — state refresh
1118
+ // ═══════════════════════════════════════════════════════════════════════════
1119
+
1120
+ /**
1121
+ * Reads all on-chain and off-chain state in parallel and stores
1122
+ * results in private instance variables.
1123
+ */
1124
+ private async _refresh(): Promise<void> {
1125
+ logger.info(`${this._tag}::_refresh starting`);
1126
+
1127
+ const [
1128
+ vaultAllocatorBalance,
1129
+ walletBalance,
1130
+ vesuPoolStates,
1131
+ extendedBalance,
1132
+ extendedPositions,
1133
+ ] = await Promise.all([
1134
+ this._fetchVaultAllocatorBalance(),
1135
+ this._fetchWalletBalances(),
1136
+ this._fetchAllVesuPoolStates(),
1137
+ this._fetchExtendedBalance(),
1138
+ this._fetchExtendedPositions(),
1139
+ ]);
1140
+
1141
+ logger.verbose(`_refresh vaultAllocatorBalance: ${vaultAllocatorBalance.usdValue}, walletBalance: ${walletBalance.usdValue}`);
1142
+ const unusedBalance = this._computeUnusedBalances(
1143
+ vaultAllocatorBalance,
1144
+ walletBalance,
1145
+ );
1146
+
1147
+ this._budget = new SolveBudget({
1148
+ limitBalanceBufferFactor: this._config.limitBalanceBufferFactor,
1149
+ unusedBalance,
1150
+ walletBalance,
1151
+ vaultBalance: vaultAllocatorBalance,
1152
+ extendedPositions,
1153
+ extendedBalance,
1154
+ vesuPoolStates,
1155
+ });
1156
+
1157
+ const totalUnusedUsd = unusedBalance.reduce(
1158
+ (acc, b) => acc + b.usdValue,
1159
+ 0,
1160
+ );
1161
+ logger.info(
1162
+ `${this._tag}::_refresh completed — ` +
1163
+ `unusedBalances: ${unusedBalance.length} tokens, ` +
1164
+ `totalUnusedUsd: ${totalUnusedUsd.toFixed(2)}, ` +
1165
+ `extendedPositions: ${extendedPositions.length}, ` +
1166
+ `vesuPools: ${vesuPoolStates.length} - ` +
1167
+ `availableForTrade: ${extendedBalance?.availableForTrade.toNumber()} - ` +
1168
+ `availableForWithdrawal: ${extendedBalance?.availableForWithdrawal.toNumber()} - ` +
1169
+ `unrealisedPnl: ${extendedBalance?.unrealisedPnl.toNumber()} - ` +
1170
+ `extendedBalance::balance: ${extendedBalance?.balance.toNumber()} - ` +
1171
+ `extendedBalance::pendingDeposit: ${extendedBalance?.pendingDeposit.toNumber()} - ` +
1172
+ `extendedBalance::equity: ${extendedBalance?.equity.toNumber()}`,
1173
+ );
1174
+ }
1175
+
1176
+ // todo add communication check with python server of extended. if not working, throw error in solve function.
1177
+
1178
+ /**
1179
+ * Reads the asset-token balance sitting idle in the vault allocator contract.
1180
+ */
1181
+ private async _fetchVaultAllocatorBalance(): Promise<TokenBalance> {
1182
+ const { assetToken, vaultAllocator, networkConfig, pricer } = this._config;
1183
+ const balance = await new ERC20(networkConfig).balanceOf(
1184
+ assetToken.address,
1185
+ vaultAllocator,
1186
+ assetToken.decimals,
1187
+ );
1188
+ const price = await pricer.getPrice(assetToken.symbol);
1189
+ const usdValue =
1190
+ Number(balance.toFixed(assetToken.decimals)) * price.price;
1191
+
1192
+ return { token: assetToken, amount: balance, usdValue };
1193
+ }
1194
+
1195
+ /**
1196
+ * Merges the vault-allocator balance and wallet balances into a
1197
+ * deduplicated array of TokenBalance entries keyed by token address.
1198
+ *
1199
+ * e.g. VA has USDC, wallet has USDC + USDC.e → returns
1200
+ * [{ token: USDC, amount: VA+wallet, usdValue: … },
1201
+ * { token: USDC.e, amount: wallet, usdValue: … }]
1202
+ */
1203
+ private _computeUnusedBalances(
1204
+ vaultAllocatorBalance: TokenBalance,
1205
+ walletBalance: TokenBalance,
1206
+ ): TokenBalance[] {
1207
+ const balanceMap = new Map<string, TokenBalance>();
1208
+
1209
+ // Seed with vault-allocator balance
1210
+ balanceMap.set(vaultAllocatorBalance.token.address.toString(), {
1211
+ token: vaultAllocatorBalance.token,
1212
+ amount: vaultAllocatorBalance.amount,
1213
+ usdValue: vaultAllocatorBalance.usdValue,
1214
+ });
1215
+
1216
+ // Merge wallet balances by token address
1217
+ const key = walletBalance.token.address.toString();
1218
+ const existing = balanceMap.get(key);
1219
+ if (existing) {
1220
+ existing.amount = new Web3Number(
1221
+ existing.amount.plus(walletBalance.amount).toFixed(existing.token.decimals),
1222
+ existing.token.decimals,
1223
+ );
1224
+ existing.usdValue += walletBalance.usdValue;
1225
+ } else {
1226
+ balanceMap.set(key, {
1227
+ token: walletBalance.token,
1228
+ amount: walletBalance.amount,
1229
+ usdValue: walletBalance.usdValue,
1230
+ });
1231
+ }
1232
+
1233
+ return Array.from(balanceMap.values());
1234
+ }
1235
+
1236
+ /**
1237
+ * Reads the operator wallet's balances for the asset token (USDC.e) and
1238
+ * USDC.e (needed for route computation — P1 vs P2 decision for Extended deposits).
1239
+ */
1240
+ private async _fetchWalletBalances(): Promise<TokenBalance> {
1241
+ const {
1242
+ networkConfig,
1243
+ pricer,
1244
+ walletAddress,
1245
+ assetToken,
1246
+ usdceToken,
1247
+ } = this._config;
1248
+ const erc20 = new ERC20(networkConfig);
1249
+
1250
+ const [usdceBalance, usdcePrice] =
1251
+ await Promise.all([
1252
+ erc20.balanceOf(
1253
+ usdceToken.address,
1254
+ walletAddress,
1255
+ usdceToken.decimals,
1256
+ ),
1257
+ pricer.getPrice(usdceToken.priceProxySymbol || usdceToken.symbol),
1258
+ ]);
1259
+
1260
+ return {
1261
+ token: usdceToken,
1262
+ amount: usdceBalance,
1263
+ usdValue:
1264
+ Number(usdceBalance.toFixed(usdceToken.decimals)) * usdcePrice.price,
1265
+ };
1266
+ }
1267
+
1268
+ /**
1269
+ * Reads the Extended exchange account-level balance (equity, available for
1270
+ * trade/withdrawal, etc.). Returns null if the API call fails.
1271
+ */
1272
+ private async _fetchExtendedBalance(): Promise<ExtendedBalanceState | null> {
1273
+ const [holdings, pendingDeposit] = await Promise.all([
1274
+ this._config.extendedAdapter.getExtendedDepositAmount(),
1275
+ this._fetchPendingDeposit(),
1276
+ ]);
1277
+ if (!holdings) return null;
1278
+
1279
+ return {
1280
+ equity: new Web3Number(holdings.equity, USDC_TOKEN_DECIMALS),
1281
+ availableForTrade: new Web3Number(
1282
+ holdings.availableForTrade,
1283
+ USDC_TOKEN_DECIMALS,
1284
+ ),
1285
+ availableForWithdrawal: new Web3Number(
1286
+ holdings.availableForWithdrawal,
1287
+ USDC_TOKEN_DECIMALS,
1288
+ ),
1289
+ unrealisedPnl: new Web3Number(holdings.unrealisedPnl, USDC_TOKEN_DECIMALS),
1290
+ balance: new Web3Number(holdings.balance, USDC_TOKEN_DECIMALS),
1291
+ pendingDeposit,
1292
+ };
1293
+ }
1294
+
1295
+ /**
1296
+ * Computes the net pending deposit by subtracting pending withdrawals from
1297
+ * pending deposits. Uses the Extended exchange asset operations API.
1298
+ *
1299
+ * Positive = deposit in transit (wallet → Extended).
1300
+ * Negative = withdrawal in transit (Extended → wallet).
1301
+ */
1302
+ private async _fetchPendingDeposit(): Promise<Web3Number> {
1303
+ try {
1304
+ const client = this._config.extendedAdapter.client;
1305
+ const [pendingDeposits, pendingWithdrawals] = await Promise.all([
1306
+ client.getAssetOperations({
1307
+ operationsType: [AssetOperationType.DEPOSIT],
1308
+ operationsStatus: [AssetOperationStatus.IN_PROGRESS],
1309
+ }),
1310
+ client.getAssetOperations({
1311
+ operationsType: [AssetOperationType.WITHDRAWAL],
1312
+ operationsStatus: [AssetOperationStatus.IN_PROGRESS],
1313
+ }),
1314
+ ]);
1315
+
1316
+ const depositTotal = pendingDeposits.data.reduce(
1317
+ (sum, op) => sum + parseFloat(op.amount || '0'), 0,
1318
+ );
1319
+ const withdrawTotal = pendingWithdrawals.data.reduce(
1320
+ (sum, op) => sum + parseFloat(op.amount || '0'), 0,
1321
+ );
1322
+
1323
+ const net = depositTotal - withdrawTotal;
1324
+ logger.info(
1325
+ `${this._tag}::_fetchPendingDeposit deposits=${depositTotal}, withdrawals=${withdrawTotal}, net=${net}`,
1326
+ );
1327
+ return new Web3Number(net.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
1328
+ } catch (err) {
1329
+ logger.error(`${this._tag}::_fetchPendingDeposit error: ${err}`);
1330
+ return new Web3Number(0, USDC_TOKEN_DECIMALS);
1331
+ }
1332
+ }
1333
+
1334
+ /**
1335
+ * Reads all open positions on the Extended exchange. Each position
1336
+ * includes instrument, side, size, USD value, and leverage.
1337
+ */
1338
+ private async _fetchExtendedPositions(): Promise<ExtendedPositionState[]> {
1339
+ const positions =
1340
+ await this._config.extendedAdapter.getAllOpenPositions();
1341
+ if (!positions) return [];
1342
+
1343
+ return positions.map((position) => ({
1344
+ instrument: position.market,
1345
+ side: position.side,
1346
+ size: new Web3Number(position.size, USDC_TOKEN_DECIMALS), // though in different token terms, this wont matter from DEX perspective, hence 6decimals is ok
1347
+ valueUsd: new Web3Number(position.value, USDC_TOKEN_DECIMALS),
1348
+ leverage: position.leverage,
1349
+ }));
1350
+ }
1351
+
1352
+ /**
1353
+ * Reads a single Vesu pool's position data: collateral amount/price,
1354
+ * debt amount/price.
1355
+ */
1356
+ private async _fetchSingleVesuPoolState(
1357
+ adapter: VesuMultiplyAdapter,
1358
+ ): Promise<VesuPoolState> {
1359
+ const assetPrices = await adapter._vesuAdapter.getAssetPrices();
1360
+ return {
1361
+ poolId: adapter.config.poolId,
1362
+ collateralToken: adapter.config.collateral,
1363
+ debtToken: adapter.config.debt,
1364
+ collateralAmount: assetPrices.collateralTokenAmount,
1365
+ collateralUsdValue: assetPrices.collateralUSDAmount,
1366
+ debtAmount: assetPrices.debtTokenAmount,
1367
+ debtUsdValue: assetPrices.debtUSDAmount,
1368
+ collateralPrice: assetPrices.collateralPrice,
1369
+ debtPrice: assetPrices.debtPrice,
1370
+ };
1371
+ }
1372
+
1373
+ /**
1374
+ * Reads all Vesu pool states in parallel (one per configured adapter).
1375
+ */
1376
+ private async _fetchAllVesuPoolStates(): Promise<VesuPoolState[]> {
1377
+ return Promise.all(
1378
+ this._config.vesuAdapters.map((adapter) =>
1379
+ this._fetchSingleVesuPoolState(adapter),
1380
+ ),
1381
+ );
1382
+ }
1383
+
1384
+ // ═══════════════════════════════════════════════════════════════════════════
1385
+ // Private — validation
1386
+ // ═══════════════════════════════════════════════════════════════════════════
1387
+
1388
+ /**
1389
+ * Validates that all critical refreshed state is present and contains
1390
+ * finite, sensible values. Throws on invalid state.
1391
+ */
1392
+ private _validateRefreshedState(): void {
1393
+ if (this._budget.unusedBalance.length === 0) {
1394
+ throw new Error(
1395
+ `${this._tag}: unusedBalance is empty after refresh`,
1396
+ );
1397
+ }
1398
+ for (const balance of this._budget.unusedBalance) {
1399
+ this._validateTokenBalanceOrThrow(
1400
+ balance,
1401
+ `unusedBalance[${balance.token.symbol}]`,
1402
+ );
1403
+ }
1404
+ this._validateVesuPoolPricesOrThrow();
1405
+ this._validateExtendedBalanceOrThrow();
1406
+ }
1407
+
1408
+ private _validateTokenBalanceOrThrow(
1409
+ balance: TokenBalance | null,
1410
+ label: string,
1411
+ ): void {
1412
+ if (!balance) {
1413
+ throw new Error(`${this._tag}: ${label} is null after refresh`);
1414
+ }
1415
+ if (!Number.isFinite(balance.usdValue) || balance.usdValue < 0) {
1416
+ throw new Error(
1417
+ `${this._tag}: ${label} has invalid usdValue: ${balance.usdValue}`,
1418
+ );
1419
+ }
1420
+ }
1421
+
1422
+ private _validateVesuPoolPricesOrThrow(): void {
1423
+ for (const pool of this._budget.vesuPoolStates) {
1424
+ const poolLabel = pool.poolId.shortString();
1425
+ this._assertPositiveFinite(
1426
+ pool.collateralPrice,
1427
+ `Vesu pool ${poolLabel} collateralPrice`,
1428
+ );
1429
+ this._assertPositiveFinite(
1430
+ pool.debtPrice,
1431
+ `Vesu pool ${poolLabel} debtPrice`,
1432
+ );
1433
+ }
1434
+ }
1435
+
1436
+ private _validateExtendedBalanceOrThrow(): void {
1437
+ if (!this._budget.extendedBalance) return; // null is acceptable; treated as zero
1438
+
1439
+ const { equity, availableForTrade } = this._budget.extendedBalance;
1440
+ if (
1441
+ !Number.isFinite(equity.toNumber()) ||
1442
+ !Number.isFinite(availableForTrade.toNumber())
1443
+ ) {
1444
+ throw new Error(
1445
+ `${this._tag}: Extended balance contains non-finite values — ` +
1446
+ `equity: ${equity.toNumber()}, availableForTrade: ${availableForTrade.toNumber()}`,
1447
+ );
1448
+ }
1449
+ }
1450
+
1451
+ private _assertPositiveFinite(value: number, label: string): void {
1452
+ if (!Number.isFinite(value) || value <= 0) {
1453
+ throw new Error(
1454
+ `${this._tag}: ${label} is invalid (${value}). Expected finite positive number.`,
1455
+ );
1456
+ }
1457
+ }
1458
+
1459
+ // ═══════════════════════════════════════════════════════════════════════════
1460
+ // Private — solve step 1: per-pool debt deltas (LTV maintenance)
1461
+ // ═══════════════════════════════════════════════════════════════════════════
1462
+
1463
+ // ═══════════════════════════════════════════════════════════════════════════
1464
+ // Private — solve step 2: distributable amount
1465
+ // ═══════════════════════════════════════════════════════════════════════════
1466
+
1467
+ /**
1468
+ * Computes total distributable capital by combining idle vault balance
1469
+ * and Extended available balance, then adjusting for aggregate debt
1470
+ * delta and any pending withdrawal.
1471
+ *
1472
+ * When withdrawAmount > 0 the distributable pool shrinks, forcing the
1473
+ * allocation split to produce smaller (or negative) allocations — which
1474
+ * in turn causes protocol positions to unwind and free capital for the
1475
+ * vault allocator to execute bringLiquidity.
1476
+ */
1477
+ private _computeDistributableAmount(
1478
+ perPoolDebtDeltasToBorrow: Web3Number[],
1479
+ withdrawAmount: Web3Number,
1480
+ ): Web3Number {
1481
+ const totalInvestable = this._computeTotalInvestableAmount();
1482
+ const aggregateDebtAdjustment = this._sumDebtDeltas(perPoolDebtDeltasToBorrow);
1483
+ return new Web3Number(
1484
+ totalInvestable
1485
+ .plus(aggregateDebtAdjustment)
1486
+ .minus(withdrawAmount)
1487
+ .toFixed(USDC_TOKEN_DECIMALS),
1488
+ USDC_TOKEN_DECIMALS,
1489
+ );
1490
+ }
1491
+
1492
+ /**
1493
+ * Total investable = vault allocator balance + Extended available-for-trade,
1494
+ * both reduced by the configured buffer percentage.
1495
+ */
1496
+ private _computeTotalInvestableAmount(): Web3Number {
1497
+ const totalUnusedUsd = this._budget.unusedBalance.reduce(
1498
+ (acc, b) => acc + b.usdValue,
1499
+ 0,
1500
+ );
1501
+ logger.debug(
1502
+ `${this._tag}::_computeTotalInvestableAmount unusedBalances=` +
1503
+ `${JSON.stringify(this._budget.unusedBalance.map((b) => ({ token: b.token.symbol, amount: b.amount.toNumber(), usdValue: b.usdValue })))}`,
1504
+ );
1505
+ const extendedAvailable =
1506
+ this._budget.extendedBalance?.availableForTrade ??
1507
+ new Web3Number(0, USDC_TOKEN_DECIMALS);
1508
+ logger.verbose(`_computeTotalInvestableAmount totalUnusedUsd: ${totalUnusedUsd}, extendedAvailable: ${extendedAvailable.toNumber()}`);
1509
+ return new Web3Number(totalUnusedUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS)
1510
+ .plus(extendedAvailable)
1511
+ }
1512
+
1513
+ private _sumDebtDeltas(deltas: Web3Number[]): Web3Number {
1514
+ return deltas.reduce(
1515
+ (sum, delta) => sum.plus(delta),
1516
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
1517
+ );
1518
+ }
1519
+
1520
+ // ═══════════════════════════════════════════════════════════════════════════
1521
+ // Private — solve step 3: allocation split
1522
+ // ═══════════════════════════════════════════════════════════════════════════
1523
+
1524
+ /**
1525
+ * Splits distributable capital between Vesu and Extended using leverage
1526
+ * ratios and existing exposure to match delta-neutral targets.
1527
+ *
1528
+ * Formula (from existing strategy):
1529
+ * ExtendedAlloc = (vL * distributable + vesuExposure - extendedExposure) / (vL + eL)
1530
+ * VesuAlloc = distributable - ExtendedAlloc
1531
+ */
1532
+ private _computeAllocationSplit(distributableAmount: Web3Number, deltaVesuCollateral = 0, deltaExtendedCollateral = 0, isRecursive = false): {
1533
+ vesuAllocationUsd: Web3Number;
1534
+ extendedAllocationUsd: Web3Number;
1535
+ vesuPositionDelta: number;
1536
+ extendedPositionDelta: number;
1537
+ } {
1538
+ if (this._hasNoVesuAdapters()) {
1539
+ throw new Error(`${this._tag}: No Vesu adapters found`);
1540
+ }
1541
+
1542
+ const vesuLeverage = calculateVesuLeverage();
1543
+ const extendedLeverage = calculateExtendedLevergae();
1544
+ const denominator = vesuLeverage + extendedLeverage;
1545
+
1546
+ if (denominator === 0) {
1547
+ throw new Error(`${this._tag}: Denominator is zero`);
1548
+ }
1549
+
1550
+ // get long/short exposures on both sides
1551
+ const collateralPrice = this._budget.vesuPoolStates[0]?.collateralPrice ?? 0;
1552
+ const totalVesuExposureUsd = this._totalVesuCollateralUsd().plus(new Web3Number((deltaVesuCollateral * collateralPrice).toFixed(6), USDC_TOKEN_DECIMALS));
1553
+ const totalExtendedExposureUsd = this._totalExtendedExposureUsd().plus(new Web3Number((deltaExtendedCollateral * collateralPrice).toFixed(6), USDC_TOKEN_DECIMALS));
1554
+
1555
+ const numerator =
1556
+ vesuLeverage * distributableAmount.toNumber() +
1557
+ totalVesuExposureUsd.toNumber() -
1558
+ totalExtendedExposureUsd.toNumber();
1559
+
1560
+ const extendedAllocationUsd = new Web3Number(
1561
+ (numerator / denominator).toFixed(USDC_TOKEN_DECIMALS),
1562
+ USDC_TOKEN_DECIMALS,
1563
+ );
1564
+ let vesuAllocationUsd = new Web3Number(
1565
+ distributableAmount
1566
+ .minus(extendedAllocationUsd)
1567
+ .toFixed(USDC_TOKEN_DECIMALS),
1568
+ USDC_TOKEN_DECIMALS,
1569
+ );
1570
+
1571
+ // add debt repayments to vesu allocation (-1 to convert to repay amount)
1572
+ const perPoolDebtDeltasToBorrow = this._budget.vesuPerPoolDebtDeltasToBorrow;
1573
+ vesuAllocationUsd = vesuAllocationUsd.plus(this._sumDebtDeltas(perPoolDebtDeltasToBorrow).multipliedBy(-1));
1574
+
1575
+ let vesuPositionDelta = Number((new Web3Number((vesuAllocationUsd.toNumber() * (vesuLeverage) / collateralPrice).toFixed(6), 6)).toFixedRoundDown(COLLATERAL_PRECISION));
1576
+ let extendedPositionDelta = Number((new Web3Number((extendedAllocationUsd.toNumber() * (extendedLeverage) / collateralPrice).toFixed(6), 6)).toFixedRoundDown(COLLATERAL_PRECISION));
1577
+
1578
+ // for the first call, reconsider the newly created position delta to actually recompute the allocation split
1579
+ if (!isRecursive) {
1580
+ return this._computeAllocationSplit(distributableAmount, vesuPositionDelta, extendedPositionDelta, true);
1581
+ }
1582
+
1583
+ // subtract any deltas passed
1584
+ // throw new Error(`${this._tag}: Recursive allocation vesuPositionDelta: ${vesuPositionDelta}, extendedPositionDelta: ${extendedPositionDelta}, vesuAllocationUsd: ${vesuAllocationUsd.toNumber()}, extendedAllocationUsd: ${extendedAllocationUsd.toNumber()}`);
1585
+ return { vesuAllocationUsd, extendedAllocationUsd, vesuPositionDelta, extendedPositionDelta };
1586
+ }
1587
+
1588
+ private _hasNoVesuAdapters(): boolean {
1589
+ return this._config.vesuAdapters.length === 0;
1590
+ }
1591
+
1592
+ // ═══════════════════════════════════════════════════════════════════════════
1593
+ // Private — solve step 4: per-pool collateral deltas
1594
+ // ═══════════════════════════════════════════════════════════════════════════
1595
+
1596
+ /**
1597
+ * Distributes the total Vesu USD allocation across pools proportionally
1598
+ * by existing collateral value, then converts each share to collateral
1599
+ * token units.
1600
+ */
1601
+ private _computePerPoolCollateralDeltas(
1602
+ vesuAllocationUsd: Web3Number, perPoolDebtDeltasToBorrow: Web3Number[]
1603
+ ): VesuPoolDelta[] {
1604
+ const vesuLeverage = calculateVesuLeverage();
1605
+
1606
+ // remove any repayments from the vesu allocation
1607
+ const availableVesuCollateralAllocationUsd = vesuAllocationUsd.plus(this._sumDebtDeltas(perPoolDebtDeltasToBorrow));
1608
+ const postLeverageAllocationUsd = availableVesuCollateralAllocationUsd.multipliedBy(vesuLeverage);
1609
+ const totalCollateralExisting = this._totalVesuCollateral();
1610
+
1611
+ return this._budget.vesuPoolStates.map((pool, index) => {
1612
+ const _postLeverageAllocation = postLeverageAllocationUsd.dividedBy(pool.collateralPrice);
1613
+
1614
+ const postLeverageAllocation = new Web3Number(((_postLeverageAllocation.plus(totalCollateralExisting)).toFixed(COLLATERAL_PRECISION)), pool.collateralToken.decimals).minus(totalCollateralExisting);
1615
+ const _poolCollateralDelta = this._computePoolCollateralShare(
1616
+ pool,
1617
+ totalCollateralExisting,
1618
+ postLeverageAllocation,
1619
+ );
1620
+ const poolCollateralDelta = new Web3Number(
1621
+ _poolCollateralDelta.toFixed(COLLATERAL_PRECISION),
1622
+ pool.collateralToken.decimals,
1623
+ );
1624
+
1625
+ // the excess collateral should come from debt.
1626
+ const newDebt = postLeverageAllocationUsd.minus(availableVesuCollateralAllocationUsd).dividedBy(pool.debtPrice);
1627
+ return {
1628
+ poolId: pool.poolId,
1629
+ collateralToken: pool.collateralToken,
1630
+ debtToken: pool.debtToken,
1631
+ debtDelta: perPoolDebtDeltasToBorrow[index].plus(newDebt),
1632
+ collateralDelta: poolCollateralDelta,
1633
+ collateralPrice: pool.collateralPrice,
1634
+ debtPrice: pool.debtPrice,
1635
+ };
1636
+ });
1637
+ }
1638
+
1639
+ /**
1640
+ * Determines how much of the total Vesu allocation goes to a single pool.
1641
+ * Single-pool or zero-total cases get the entire allocation.
1642
+ * Multi-pool cases split proportionally by current collateral USD value.
1643
+ */
1644
+ private _computePoolCollateralShare(
1645
+ pool: VesuPoolState,
1646
+ totalCollateral: Web3Number,
1647
+ totalVesuAllocation: Web3Number,
1648
+ ): Web3Number {
1649
+ const isSinglePoolOrZeroTotal =
1650
+ this._budget.vesuPoolStates.length === 1 ||
1651
+ totalCollateral.toNumber() === 0;
1652
+
1653
+ if (isSinglePoolOrZeroTotal) return totalVesuAllocation;
1654
+
1655
+ const poolWeight = pool.collateralAmount.dividedBy(totalCollateral);
1656
+
1657
+ return new Web3Number(
1658
+ totalVesuAllocation
1659
+ .multipliedBy(poolWeight)
1660
+ .toFixed(USDC_TOKEN_DECIMALS),
1661
+ USDC_TOKEN_DECIMALS,
1662
+ );
1663
+ }
1664
+
1665
+ // ═══════════════════════════════════════════════════════════════════════════
1666
+ // Private — solve step 6: extended position deltas
1667
+ // ═══════════════════════════════════════════════════════════════════════════
1668
+
1669
+ /**
1670
+ * Computes per-position exposure deltas for delta neutrality.
1671
+ * Target: total Extended short exposure = total projected Vesu collateral
1672
+ * exposure (current + collateral deltas).
1673
+ */
1674
+ private _computeExtendedPositionDeltas(
1675
+ vesuDeltas: VesuPoolDelta[],
1676
+ ): ExtendedPositionDelta[] {
1677
+ const targetExposure =
1678
+ this._computeTargetExtendedExposure(vesuDeltas);
1679
+ const currentExposure = this._totalExtendedExposure();
1680
+ logger.debug(
1681
+ `${this._tag}::_computeExtendedPositionDeltas targetExposure=${targetExposure.toNumber()}, currentExposure=${currentExposure.toNumber()}`,
1682
+ );
1683
+ const totalExposureDelta = new Web3Number(
1684
+ targetExposure
1685
+ .minus(currentExposure)
1686
+ .toFixed(USDC_TOKEN_DECIMALS),
1687
+ USDC_TOKEN_DECIMALS,
1688
+ );
1689
+
1690
+ if (this._hasNoExtendedPositions()) {
1691
+ return this._singleInstrumentDelta(totalExposureDelta);
1692
+ }
1693
+
1694
+ return this._distributeExposureDeltaAcrossPositions(totalExposureDelta);
1695
+ }
1696
+
1697
+ /**
1698
+ * Target Extended exposure = sum of (current collateral + collateralDelta)
1699
+ * * price, across all Vesu pools.
1700
+ */
1701
+ private _computeTargetExtendedExposure(
1702
+ vesuDeltas: VesuPoolDelta[],
1703
+ ): Web3Number {
1704
+ let totalExposureCollateral = new Web3Number(0, USDC_TOKEN_DECIMALS); // just some decimals is ok
1705
+
1706
+ for (let i = 0; i < this._budget.vesuPoolStates.length; i++) {
1707
+ const pool = this._budget.vesuPoolStates[i];
1708
+ const delta = vesuDeltas[i];
1709
+ logger.debug(
1710
+ `${this._tag}::_computeTargetExtendedExposure poolId=${pool.poolId.toString()}, collateralAmount=${pool.collateralAmount.toNumber()}, collateralDelta=${delta.collateralDelta.toNumber()}`,
1711
+ );
1712
+ const projectedCollateral = pool.collateralAmount.plus(
1713
+ delta.collateralDelta,
1714
+ );
1715
+ totalExposureCollateral = totalExposureCollateral.plus(
1716
+ projectedCollateral,
1717
+ );
1718
+ }
1719
+
1720
+ return new Web3Number(
1721
+ totalExposureCollateral.toFixed(USDC_TOKEN_DECIMALS),
1722
+ USDC_TOKEN_DECIMALS,
1723
+ );
1724
+ }
1725
+
1726
+ private _hasNoExtendedPositions(): boolean {
1727
+ return this._budget.extendedPositions.length === 0;
1728
+ }
1729
+
1730
+ /**
1731
+ * Creates a single-element delta array for the default instrument
1732
+ * when no Extended positions currently exist.
1733
+ */
1734
+ private _singleInstrumentDelta(
1735
+ delta: Web3Number,
1736
+ ): ExtendedPositionDelta[] {
1737
+ return [
1738
+ {
1739
+ instrument: this._config.extendedAdapter.config.extendedMarketName,
1740
+ delta: new Web3Number(
1741
+ delta.toFixed(COLLATERAL_PRECISION),
1742
+ USDC_TOKEN_DECIMALS,
1743
+ ),
1744
+ },
1745
+ ];
1746
+ }
1747
+
1748
+ /**
1749
+ * Distributes a total exposure delta proportionally across existing
1750
+ * positions by their current USD value share.
1751
+ */
1752
+ private _distributeExposureDeltaAcrossPositions(
1753
+ totalDelta: Web3Number,
1754
+ ): ExtendedPositionDelta[] {
1755
+ const totalExposure = this._totalExtendedExposure();
1756
+
1757
+ return this._budget.extendedPositions.map((position) => {
1758
+ const share = this._positionExposureShareFraction(
1759
+ position,
1760
+ totalExposure,
1761
+ );
1762
+ return {
1763
+ instrument: position.instrument,
1764
+ delta: new Web3Number(
1765
+ totalDelta.multipliedBy(share).toFixed(COLLATERAL_PRECISION),
1766
+ USDC_TOKEN_DECIMALS,
1767
+ ),
1768
+ };
1769
+ });
1770
+ }
1771
+
1772
+ /**
1773
+ * Returns the fraction (0–1) of total Extended exposure held by
1774
+ * a single position. Returns 1 when there is only one position
1775
+ * or when total exposure is zero.
1776
+ */
1777
+ private _positionExposureShareFraction(
1778
+ position: ExtendedPositionState,
1779
+ totalExposure: Web3Number,
1780
+ ): number {
1781
+ const isSingleOrZero =
1782
+ totalExposure.toNumber() === 0 ||
1783
+ this._budget.extendedPositions.length === 1;
1784
+ if (isSingleOrZero) return 1;
1785
+ return position.valueUsd.dividedBy(totalExposure).toNumber();
1786
+ }
1787
+
1788
+ // ═══════════════════════════════════════════════════════════════════════════
1789
+ // Private — solve step 7: extended deposit delta
1790
+ // ═══════════════════════════════════════════════════════════════════════════
1791
+
1792
+ /**
1793
+ * Net deposit change = allocation target − currently available for trade.
1794
+ * Positive = need to deposit more, negative = can withdraw excess.
1795
+ */
1796
+ private _computeExtendedDepositDelta(
1797
+ extendedAllocationUsd: Web3Number,
1798
+ ): Web3Number {
1799
+ const currentAvailableForTrade =
1800
+ this._budget.extendedBalance?.availableForTrade ??
1801
+ new Web3Number(0, USDC_TOKEN_DECIMALS);
1802
+
1803
+ return new Web3Number(
1804
+ extendedAllocationUsd
1805
+ .minus(currentAvailableForTrade)
1806
+ .toFixed(USDC_TOKEN_DECIMALS),
1807
+ USDC_TOKEN_DECIMALS,
1808
+ );
1809
+ }
1810
+
1811
+ private _computeVesuDepositAmount(vesuDeltas: VesuPoolDelta[]): Web3Number {
1812
+ let totalVesuCollateral = new Web3Number(0, USDC_TOKEN_DECIMALS); // just some decimals is ok
1813
+ for (let i = 0; i < this._budget.vesuPoolStates.length; i++) {
1814
+ const pool = this._budget.vesuPoolStates[i];
1815
+ const delta = vesuDeltas[i];
1816
+ totalVesuCollateral = totalVesuCollateral.plus(delta.collateralDelta.multipliedBy(pool.collateralPrice));
1817
+ totalVesuCollateral = totalVesuCollateral.minus(delta.debtDelta.multipliedBy(pool.debtPrice));
1818
+ }
1819
+ return totalVesuCollateral;
1820
+ }
1821
+
1822
+ // ═══════════════════════════════════════════════════════════════════════════
1823
+ // Private — solve step 8: wallet delta
1824
+ // ═══════════════════════════════════════════════════════════════════════════
1825
+
1826
+ /**
1827
+ * The wallet is a pass-through for USDC flows between the vault allocator
1828
+ * and the Extended exchange.
1829
+ *
1830
+ * Positive = vault allocator → wallet → Extended (deposit)
1831
+ * Negative = Extended → wallet → vault allocator (withdrawal)
1832
+ */
1833
+ private _deriveWalletDelta(extendedDeposit: Web3Number): Web3Number {
1834
+ return extendedDeposit;
1835
+ }
1836
+
1837
+ // ═══════════════════════════════════════════════════════════════════════════
1838
+ // Private — solve step 9: bring liquidity
1839
+ // ═══════════════════════════════════════════════════════════════════════════
1840
+
1841
+ /**
1842
+ * The bringLiquidity amount is the USDC that the vault allocator must
1843
+ * transfer back to the vault contract for the user's withdrawal.
1844
+ * Equals the withdrawAmount passed into solve(); 0 during investment cycles.
1845
+ */
1846
+ private _computeBringLiquidityAmount(
1847
+ withdrawAmount: Web3Number,
1848
+ ): Web3Number {
1849
+ return withdrawAmount;
1850
+ }
1851
+
1852
+ // ═══════════════════════════════════════════════════════════════════════════
1853
+ // Private — case classification (modular)
1854
+ //
1855
+ // Cases are the source of truth. Each case carries its own routes
1856
+ // with amounts and state info. There is no global routes array —
1857
+ // consumers flatten case routes as needed.
1858
+ // ═══════════════════════════════════════════════════════════════════════════
1859
+
1860
+
1861
+ // ── ExecutionRoute-building helpers ─────────────────────────────────────────────
1862
+
1863
+ // ── Atomic route builders ────────────────────────────────────────────
1864
+
1865
+ private _buildVesuRepayRoutes(totalUsd: number, routes: ExecutionRoute[]): void {
1866
+ const { used, spendsByPool } = this._budget.repayVesuBorrowCapacity(totalUsd);
1867
+ for (const route of spendsByPool) {
1868
+ routes.push({ type: RouteType.VESU_REPAY as const, ...route, priority: routes.length });
1869
+ }
1870
+
1871
+ this._budget.spendVA(used);
1872
+ }
1873
+
1874
+ private _buildVesuBorrowRoutes(totalUsd: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
1875
+ let borrowable = this._budget.vesuBorrowCapacity;
1876
+ if (totalUsd <= CASE_THRESHOLD_USD) return { routes, remaining: totalUsd };
1877
+ if (borrowable <= CASE_THRESHOLD_USD) return { routes, remaining: totalUsd };
1878
+
1879
+ const { used, spendsByPool } = this._budget.spendVesuBorrowCapacity(totalUsd);
1880
+ for (const route of spendsByPool) {
1881
+ routes.push({ type: RouteType.VESU_BORROW as const, ...route, priority: routes.length });
1882
+ }
1883
+
1884
+ this._budget.addToVA(used);
1885
+ return { routes, remaining: totalUsd - used };
1886
+ }
1887
+
1888
+ // private _buildExtendedDepositRoutes(totalUsd: number): TransferRoute[] {
1889
+ // const routes: TransferRoute[] = [];
1890
+ // let rem = totalUsd;
1891
+ // const walletBal = this._budget.walletBalance?.amount?.toNumber() ?? 0;
1892
+ // if (walletBal > 0 && rem > 0) {
1893
+ // const use = Math.min(walletBal, rem);
1894
+ // routes.push({ type: RouteType.WALLET_TO_EXTENDED, amount: safeUsdcWeb3Number(use), priority: routes.length });
1895
+ // this._budget.applyExtendedBalanceChange(use);
1896
+ // rem -= use;
1897
+ // }
1898
+ // if (rem > 0) {
1899
+ // routes.push({ type: RouteType.VA_TO_EXTENDED, amount: safeUsdcWeb3Number(rem), priority: routes.length });
1900
+ // this._budget.applyExtendedBalanceChange(rem);
1901
+ // }
1902
+ // return routes;
1903
+ // }
1904
+
1905
+ // private _buildVesuIncreaseLeverRoutes(vesuDeltas: VesuPoolDelta[]): VesuMultiplyRoute[] {
1906
+ // return vesuDeltas
1907
+ // .filter((d) => d.collateralDelta.greaterThan(0))
1908
+ // .map((d) => {
1909
+ // this._budget.applyVesuDelta(d.poolId, d.collateralDelta, d.debtDelta);
1910
+ // return {
1911
+ // type: RouteType.VESU_MULTIPLY_INCREASE_LEVER as const,
1912
+ // poolId: d.poolId,
1913
+ // collateralToken: d.collateralToken,
1914
+ // collateralAmount: d.collateralDelta,
1915
+ // debtToken: d.debtToken,
1916
+ // debtAmount: d.debtDelta,
1917
+ // priority: 0,
1918
+ // };
1919
+ // });
1920
+ // }
1921
+
1922
+ // private _buildVesuDecreaseLeverRoutes(vesuDeltas: Web3Number[]): VesuMultiplyRoute[] {
1923
+ // return vesuDeltas
1924
+ // .filter((d) => d.collateralDelta.isNegative())
1925
+ // .map((d) => {
1926
+ // this._budget.applyVesuDelta(d.poolId, d.collateralDelta, d.debtDelta);
1927
+ // return {
1928
+ // type: RouteType.VESU_MULTIPLY_DECREASE_LEVER as const,
1929
+ // poolId: d.poolId,
1930
+ // collateralToken: d.collateralToken,
1931
+ // collateralAmount: d.collateralDelta,
1932
+ // debtToken: d.debtToken,
1933
+ // debtAmount: d.debtDelta,
1934
+ // priority: 0,
1935
+ // };
1936
+ // });
1937
+ // }
1938
+
1939
+ private _getWalletToVARoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
1940
+ const usableAmount = Math.min(tryAmount, this._budget.walletUsd);
1941
+ if (usableAmount > CASE_THRESHOLD_USD) {
1942
+ const walletUsed = this._budget.spendWallet(usableAmount);
1943
+ this._budget.addToVA(walletUsed);
1944
+ const route: TransferRoute = { type: RouteType.WALLET_TO_VA, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length };
1945
+ routes.push(route);
1946
+ return { routes, remaining: tryAmount - walletUsed };
1947
+ }
1948
+ return { routes, remaining: tryAmount };
1949
+ }
1950
+
1951
+ private _getWalletToEXTENDEDRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
1952
+ const usableAmount = Math.min(tryAmount, this._budget.walletUsd);
1953
+ if (usableAmount > CASE_THRESHOLD_USD) {
1954
+ const walletUsed = this._budget.spendWallet(usableAmount);
1955
+ this._budget.addToExtAvailTrade(walletUsed);
1956
+ routes.push({ type: RouteType.WALLET_TO_EXTENDED, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length } as TransferRoute);
1957
+
1958
+ if (shouldAddWaitRoute) {
1959
+ routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
1960
+ }
1961
+ return { routes, remaining: tryAmount - walletUsed };
1962
+ }
1963
+ return { routes, remaining: tryAmount };
1964
+ }
1965
+
1966
+ private _getVAToEXTENDEDRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
1967
+ const usableAmount = Math.min(tryAmount, this._budget.vaUsd);
1968
+ if (usableAmount > CASE_THRESHOLD_USD) {
1969
+ const vaUsed = this._budget.spendVA(usableAmount);
1970
+ this._budget.addToExtAvailTrade(vaUsed);
1971
+
1972
+ // add extended deposit route
1973
+ const route: TransferRoute = { type: RouteType.VA_TO_EXTENDED, amount: safeUsdcWeb3Number(vaUsed), priority: routes.length };
1974
+ routes.push(route);
1975
+
1976
+ // should follow up by a returning to wait route
1977
+ if (shouldAddWaitRoute) {
1978
+ routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
1979
+ }
1980
+ return { routes, remaining: tryAmount - vaUsed };
1981
+ }
1982
+ return { routes, remaining: tryAmount };
1983
+ }
1984
+
1985
+ private _getExtendedToWalletRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
1986
+ if (tryAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
1987
+ assert(tryAmount <= this._budget.extAvailWithdraw, `tryAmount is greater than extAvailTrade, tryAmount: ${tryAmount}, extAvailWithdraw: ${this._budget.extAvailWithdraw}`);
1988
+ const extWithdrawUsed = this._budget.spendExtAvailTrade(tryAmount);
1989
+ this._budget.addToWallet(Math.abs(extWithdrawUsed));
1990
+ const route: TransferRoute = { type: RouteType.EXTENDED_TO_WALLET, amount: safeUsdcWeb3Number(Math.abs(extWithdrawUsed)), priority: routes.length };
1991
+ routes.push(route);
1992
+
1993
+ if (shouldAddWaitRoute) {
1994
+ routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
1995
+ }
1996
+ return { routes, remaining: tryAmount - Math.abs(extWithdrawUsed) };
1997
+ }
1998
+
1999
+ private _getWALLETToVARoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
2000
+ if (tryAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
2001
+ const usableAmount = Math.min(tryAmount, this._budget.walletUsd);
2002
+ if (usableAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
2003
+ // assert(tryAmount <= this._budget.walletUsd, `tryAmount is greater than walletUsd, tryAmount: ${tryAmount}, walletUsd: ${this._budget.walletUsd}`);
2004
+ const walletUsed = this._budget.spendWallet(tryAmount);
2005
+ this._budget.addToVA(walletUsed);
2006
+ const route: TransferRoute = { type: RouteType.WALLET_TO_VA, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length };
2007
+ routes.push(route);
2008
+ return { routes, remaining: tryAmount - walletUsed };
2009
+ }
2010
+
2011
+ private _getUpnlRoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
2012
+ const upnl = this._budget.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
2013
+ const usableAmount = Math.min(tryAmount, upnl);
2014
+ if (usableAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
2015
+
2016
+ // if fails, ensure there is a way to choose the positio to use to create realised pnl
2017
+ // until then, only 1 position is supported
2018
+ const upnlUsed = this._budget.spendExtAvailUpnl(usableAmount); // removes from upnl
2019
+ this._budget.addToExtAvailTrade(upnlUsed); // adds to available-for-withdrawal
2020
+ assert(this._budget.extendedPositions.length == 1, 'SolveBudget::_getUpnlRoute: extendedPositions length must be 1');
2021
+ routes.push({
2022
+ type: RouteType.REALISE_PNL,
2023
+ amount: safeUsdcWeb3Number(upnlUsed),
2024
+ instrument: this._budget.extendedPositions[0].instrument,
2025
+ priority: routes.length,
2026
+ } as RealisePnlRoute);
2027
+ return { routes, remaining: tryAmount - upnlUsed };
2028
+ }
2029
+
2030
+ // ── Sub-classifiers ────────────────────────────────────────────────────
2031
+ // Each sub-classifier builds routes directly from contextual data.
2032
+
2033
+ /**
2034
+ * 1. Withdrawal — source funds in priority order:
2035
+ * 1) VA balance 2) Wallet 3) Vesu borrow capacity
2036
+ * 4) Extended available-for-withdrawal + unrealised PnL
2037
+ * 5) Unwind positions on both sides (freed funds handled next cycle)
2038
+ */
2039
+ private _classifyWithdrawal(
2040
+ withdrawAmount: Web3Number
2041
+ ): SolveCaseEntry[] {
2042
+ if (!withdrawAmount.greaterThan(CASE_THRESHOLD_USD)) return [];
2043
+
2044
+ const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
2045
+ const routes: ExecutionRoute[] = [];
2046
+ let remaining = withdrawAmount.toNumber();
2047
+
2048
+ // ── Step 1: VA balance ────────────────────────────────────────────────
2049
+ // VA funds are already in the vault allocator — no transfer route needed.
2050
+ const vaUsed = this._budget.spendVA(Math.min(this._budget.vaUsd, remaining));
2051
+ remaining -= vaUsed;
2052
+
2053
+ let totalExtUsed = 0;
2054
+
2055
+ // ── Step 2: Wallet balance → VA ──────────────────────────────────────
2056
+ if (remaining > CASE_THRESHOLD_USD) {
2057
+ const { remaining: walletToVaRemaining } = this._getWALLETToVARoute(remaining, routes);
2058
+ remaining = walletToVaRemaining;
2059
+ }
2060
+
2061
+ // ── Step 3: Borrow from Vesu (positive debt delta = borrowable) ──────
2062
+ if (remaining > CASE_THRESHOLD_USD) {
2063
+ const { remaining: borrowRemaining } = this._buildVesuBorrowRoutes(remaining, routes);
2064
+ remaining = borrowRemaining;
2065
+ }
2066
+
2067
+ // ── Step 4: Extended available-for-withdrawal, then unrealised PnL ───
2068
+ if (remaining > CASE_THRESHOLD_USD) {
2069
+ const usableWithrawAmount = Math.min(remaining, this._budget.extAvailWithdraw);
2070
+ remaining -= usableWithrawAmount;
2071
+
2072
+ let upnlUsed = 0;
2073
+ if (remaining > CASE_THRESHOLD_USD) {
2074
+ const { remaining: upnlRemaining } = this._getUpnlRoute(remaining, routes);
2075
+ upnlUsed = remaining - upnlRemaining;
2076
+ remaining = upnlRemaining;
2077
+ }
2078
+
2079
+ // keep track of total extended used
2080
+ totalExtUsed = usableWithrawAmount + upnlUsed;
2081
+ }
2082
+
2083
+ // ── Step 5: Unwind positions on both sides ───────────────────────────
2084
+ // Decrease Vesu lever + Extended exposure by equal BTC amounts to
2085
+ // maintain delta neutrality. Freed funds are picked up next cycle.
2086
+ if (remaining > CASE_THRESHOLD_USD) {
2087
+ const avgCollPrice = this._budget.vesuPoolStates[0]?.collateralPrice ?? 1;
2088
+ assert(this._budget.vesuPoolStates.length == 1, 'SolveBudget::_classifyWithdrawal: vesuPoolStates length must be 1');
2089
+ // below function doesnt handle multiple pools yet
2090
+ const { vesuPositionDelta, extendedPositionDelta, vesuAllocationUsd, extendedAllocationUsd } = this._computeAllocationSplit(new Web3Number((-remaining).toFixed(6), USDC_TOKEN_DECIMALS));
2091
+
2092
+ if (vesuPositionDelta < 0) {
2093
+ const vesuAdapter = this._config.vesuAdapters[0];
2094
+ const debtDelta = Math.min(0, vesuPositionDelta * avgCollPrice - vesuAllocationUsd.toNumber());
2095
+ const withdrawAmount = vesuAllocationUsd.dividedBy(avgCollPrice);
2096
+ withdrawAmount.decimals = vesuAdapter.config.collateral.decimals;
2097
+ routes.push({
2098
+ type: RouteType.VESU_MULTIPLY_DECREASE_LEVER,
2099
+ poolId: vesuAdapter.config.poolId,
2100
+ collateralToken: vesuAdapter.config.collateral,
2101
+ marginAmount: withdrawAmount,
2102
+ swappedCollateralAmount: (new Web3Number(vesuPositionDelta, vesuAdapter.config.collateral.decimals)).minus(withdrawAmount),
2103
+ debtToken: vesuAdapter.config.debt,
2104
+ debtAmount: new Web3Number(debtDelta, USDC_TOKEN_DECIMALS),
2105
+ priority: routes.length,
2106
+ } as VesuMultiplyRoute);
2107
+ this._budget.applyVesuDelta(vesuAdapter.config.poolId, vesuAdapter.config.collateral, vesuAdapter.config.debt, new Web3Number(vesuPositionDelta, USDC_TOKEN_DECIMALS), new Web3Number(debtDelta, USDC_TOKEN_DECIMALS));
2108
+
2109
+ // Swap freed BTC → USDC (exact amount determined at runtime)
2110
+ if (vesuAllocationUsd.toNumber() < -CASE_THRESHOLD_USD) {
2111
+ const swapAmount = new Web3Number(((vesuAllocationUsd.toNumber() / avgCollPrice) * 0.998).toFixed(6), USDC_TOKEN_DECIMALS);
2112
+ routes.push({
2113
+ type: RouteType.AVNU_WITHDRAW_SWAP,
2114
+ fromToken: vesuAdapter.config.collateral.symbol,
2115
+ // add buffer to avoid rounding errors
2116
+ fromAmount: swapAmount,
2117
+ toToken: vesuAdapter.config.debt.symbol,
2118
+ priority: routes.length,
2119
+ } as SwapRoute);
2120
+ this._budget.addToVA(vesuAllocationUsd.abs().toNumber());
2121
+ }
2122
+ }
2123
+
2124
+
2125
+ if (extendedPositionDelta < 0) {
2126
+ // Decrease Extended exposure by the same BTC amount
2127
+ routes.push({
2128
+ type: RouteType.EXTENDED_DECREASE_LEVER,
2129
+ amount: safeUsdcWeb3Number(extendedPositionDelta),
2130
+ instrument,
2131
+ priority: routes.length,
2132
+ });
2133
+ this._budget.applyExtendedExposureDelta(instrument, safeUsdcWeb3Number(extendedPositionDelta));
2134
+ this._budget.addToExtAvailTrade(extendedAllocationUsd.abs().toNumber());
2135
+ totalExtUsed += extendedAllocationUsd.abs().toNumber();
2136
+ }
2137
+ }
2138
+
2139
+ if (totalExtUsed > CASE_THRESHOLD_USD) {
2140
+ this._getExtendedToWalletRoute(totalExtUsed, routes);
2141
+ this._getWALLETToVARoute(totalExtUsed, routes);
2142
+ }
2143
+
2144
+ // ── Bring liquidity for whatever was sourced in steps 1-4 ────────────
2145
+ routes.push({
2146
+ type: RouteType.BRING_LIQUIDITY,
2147
+ amount: withdrawAmount,
2148
+ priority: routes.length,
2149
+ });
2150
+ this._budget.spendVA(withdrawAmount.toNumber() - vaUsed);
2151
+
2152
+ routes.forEach((r, i) => { r.priority = i; });
2153
+
2154
+ return [{
2155
+ case: CASE_DEFINITIONS[CaseId.WITHDRAWAL_SIMPLE],
2156
+ additionalArgs: { amount: withdrawAmount },
2157
+ routes,
2158
+ }];
2159
+ }
2160
+
2161
+ /**
2162
+ * 2a. LTV Rebalance — Vesu side
2163
+ *
2164
+ * Triggered when vesuPerPoolDebtDeltasToBorrow sum is negative (high LTV, needs repayment).
2165
+ * Sources funds to VA so the subsequent deposit classifier can build the correct lever routes.
2166
+ *
2167
+ * Priority: 1) VA + Wallet (no routes) 2) Extended available-for-withdrawal
2168
+ * 3) Extended uPnL 4) Margin crisis (future)
2169
+ *
2170
+ * Design: accumulate all ext-to-wallet moves, add transfer routes at the end (principle #3).
2171
+ */
2172
+ private _classifyLtvVesu(): SolveCaseEntry[] {
2173
+ const debtDeltaSum = this._budget.vesuPerPoolDebtDeltasToBorrow.reduce(
2174
+ (a, b) => a + b.toNumber(), 0,
2175
+ );
2176
+ logger.info(`${this._tag}::_classifyLtvVesu debtDeltaSum: ${debtDeltaSum}`);
2177
+
2178
+ // todo, even if sum is positive, but in a multi-pool situation,
2179
+ // even if one bad, its problem. need to handle this.
2180
+ const allHealthLTVs = this._budget.shouldVesuRebalance.every(shouldReb => !shouldReb)
2181
+ if (debtDeltaSum >= -CASE_THRESHOLD_USD || allHealthLTVs) return [];
2182
+
2183
+ const needed = Math.abs(debtDeltaSum);
2184
+ const routes: ExecutionRoute[] = [];
2185
+ let remaining = needed;
2186
+ let totalExtUsed = 0;
2187
+ let caseId: CaseId = CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET;
2188
+
2189
+ // ── Step 1: VA + Wallet balance ─────────────────────────────────────
2190
+ // VA funds already in VA — no transfer route needed.
2191
+ const vaUsed = this._budget.spendVA(Math.min(this._budget.vaUsd, remaining));
2192
+ remaining -= vaUsed;
2193
+
2194
+ // ── Step 2: Wallet balance → VA ──────────────────────────────────────
2195
+ if (remaining > CASE_THRESHOLD_USD) {
2196
+ const { remaining: walletToVaRemaining } = this._getWALLETToVARoute(remaining, routes);
2197
+ remaining = walletToVaRemaining;
2198
+ }
2199
+
2200
+ // ── Step 3: Extended available-for-withdrawal, then uPnL ────────────
2201
+ if (remaining > CASE_THRESHOLD_USD) {
2202
+ const usableWithdrawAmount = Math.min(remaining, this._budget.extAvailWithdraw);
2203
+ remaining -= usableWithdrawAmount;
2204
+
2205
+ let upnlUsed = 0;
2206
+ if (remaining > CASE_THRESHOLD_USD) {
2207
+ const { remaining: upnlRemaining } = this._getUpnlRoute(remaining, routes);
2208
+ upnlUsed = remaining - upnlRemaining;
2209
+ remaining = upnlRemaining;
2210
+ caseId = CaseId.LTV_EXTENDED_PROFITABLE_REALIZE;
2211
+ } else {
2212
+ caseId = CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE;
2213
+ }
2214
+
2215
+ totalExtUsed = usableWithdrawAmount + upnlUsed;
2216
+ }
2217
+
2218
+ if (remaining > CASE_THRESHOLD_USD) {
2219
+ throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
2220
+ }
2221
+
2222
+ // ── Deferred: Extended→Wallet + Wallet→VA ───────────────────────────
2223
+ if (totalExtUsed > CASE_THRESHOLD_USD) {
2224
+ this._getExtendedToWalletRoute(totalExtUsed, routes);
2225
+ this._getWALLETToVARoute(totalExtUsed, routes);
2226
+ }
2227
+
2228
+ // add routes linked to VESU repay
2229
+ this._buildVesuRepayRoutes(needed - remaining, routes);
2230
+
2231
+ routes.forEach((r, i) => { r.priority = i; });
2232
+
2233
+ return [{
2234
+ case: CASE_DEFINITIONS[caseId],
2235
+ additionalArgs: { amount: safeUsdcWeb3Number(needed) },
2236
+ routes,
2237
+ }];
2238
+ }
2239
+
2240
+ // ── LTV Vesu route builders ───────────────────────────────────────────
2241
+
2242
+ /**
2243
+ * LTV_EXTENDED_PROFITABLE_AVAILABLE:
2244
+ * Extended has enough available-to-withdraw → withdraw, move to VA, repay Vesu.
2245
+ * Routes: [EXTENDED_TO_WALLET, WALLET_TO_VA, VESU_REPAY]
2246
+ */
2247
+ // private _buildLtvVesuRepayFromExtendedRoutes(amountUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
2248
+ // const routes: ExecutionRoute[] = [];
2249
+ // const amt = safeUsdcWeb3Number(amountUsd);
2250
+
2251
+ // // Withdraw from Extended → operator wallet
2252
+ // routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
2253
+ // this._budget.applyExtendedBalanceChange(-amountUsd);
2254
+
2255
+ // // Wait for Extended withdrawal to settle before using wallet funds
2256
+ // routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
2257
+
2258
+ // // Operator wallet → VA
2259
+ // routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
2260
+
2261
+ // // Repay Vesu debt from VA funds
2262
+ // routes.push(...this._buildVesuRepayRoutesss(amountUsd, vesuDeltas)); // wrong
2263
+
2264
+ // routes.forEach((r, i) => { r.priority = i; });
2265
+ // return routes;
2266
+ // }
2267
+
2268
+ /**
2269
+ * LTV_EXTENDED_PROFITABLE_REALIZE:
2270
+ * Extended has unrealised PnL → realise it first, then withdraw + repay.
2271
+ * Routes: [REALISE_PNL, EXTENDED_TO_WALLET, WALLET_TO_VA, VESU_REPAY]
2272
+ */
2273
+ // private _buildLtvVesuRepayRealiseRoutes(amountUsd: number, availWithdrawUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
2274
+ // const routes: ExecutionRoute[] = [];
2275
+ // const amt = safeUsdcWeb3Number(amountUsd);
2276
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
2277
+ // const upnlRequired = amountUsd - availWithdrawUsd;
2278
+
2279
+ // // Realise PnL to make funds available for withdrawal
2280
+ // routes.push({ type: RouteType.REALISE_PNL, amount: safeUsdcWeb3Number(upnlRequired), instrument, priority: routes.length });
2281
+
2282
+ // // Withdraw realised funds from Extended → operator wallet
2283
+ // routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
2284
+ // this._budget.applyExtendedBalanceChange(-amountUsd);
2285
+
2286
+ // // Wait for Extended withdrawal to settle
2287
+ // routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
2288
+
2289
+ // // Operator wallet → VA
2290
+ // routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
2291
+
2292
+ // // Repay Vesu debt from VA funds
2293
+ // routes.push(...this._buildVesuRepayRoutesss(amountUsd, vesuDeltas)); // wrong
2294
+
2295
+ // routes.forEach((r, i) => { r.priority = i; });
2296
+ // return routes;
2297
+ // }
2298
+
2299
+ /**
2300
+ * MARGIN_CRISIS_VESU:
2301
+ * Neither VA/Wallet nor Extended withdrawal covers the shortfall.
2302
+ * Temporarily increase Extended leverage to free margin, withdraw to VA, then
2303
+ * decrease Vesu lever to bring LTV back in range.
2304
+ * Routes: [CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE, EXTENDED_TO_WALLET, WALLET_TO_VA,
2305
+ * VESU_MULTIPLY_DECREASE_LEVER, EXTENDED_DECREASE_LEVER, CRISIS_UNDO_EXTENDED_MAX_LEVERAGE]
2306
+ */
2307
+ // private _buildMarginCrisisVesuRoutes(neededUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
2308
+ // const routes: ExecutionRoute[] = [];
2309
+ // const amt = safeUsdcWeb3Number(neededUsd);
2310
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
2311
+
2312
+ // // Temporarily boost Extended leverage to free margin
2313
+ // routes.push({ type: RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE, priority: routes.length });
2314
+
2315
+ // // Withdraw freed margin from Extended → wallet
2316
+ // routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
2317
+ // this._budget.applyExtendedBalanceChange(-neededUsd);
2318
+
2319
+ // // Wallet → VA
2320
+ // routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
2321
+
2322
+ // // Decrease Vesu leverage to reduce LTV
2323
+ // // todo to verify if this is correct.
2324
+ // routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
2325
+
2326
+ // // Decrease Extended exposure to match reduced Vesu position
2327
+ // // ! todo incorrect, to fix
2328
+ // routes.push({ type: RouteType.EXTENDED_DECREASE_LEVER, amount: safeUsdcWeb3Number(0), instrument, priority: routes.length });
2329
+ // this._budget.applyExtendedExposureDelta(instrument, safeUsdcWeb3Number(0));
2330
+
2331
+ // // Revert Extended leverage back to normal
2332
+ // routes.push({ type: RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE, priority: routes.length });
2333
+
2334
+ // routes.forEach((r, i) => { r.priority = i; });
2335
+ // return routes;
2336
+ // }
2337
+
2338
+ /** 2b. LTV Rebalance — Extended side */
2339
+ /**
2340
+ * 2b. LTV Rebalance — Extended side
2341
+ *
2342
+ * Triggered when Extended equity is below the required margin for current positions.
2343
+ * Sources funds to Extended via VA/Wallet or Vesu borrow.
2344
+ *
2345
+ * Priority: 1) VA/Wallet → Extended 2) Vesu borrow → VA → Extended
2346
+ */
2347
+ private _classifyLtvExtended(): SolveCaseEntry[] {
2348
+ const totalExtPosUsd = this._totalExtendedExposureUsd().toNumber();
2349
+ const extEquity = this._budget.extendedBalance?.equity?.toNumber() ?? 0;
2350
+ const lev = calculateExtendedLevergae();
2351
+ const marginNeeded = lev > 0 ? totalExtPosUsd / lev - extEquity : 0;
2352
+ if (marginNeeded <= CASE_THRESHOLD_USD) return [];
2353
+
2354
+ let caseId: CaseId = CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET;
2355
+ let remaining = marginNeeded;
2356
+ const routes: ExecutionRoute[] = [];
2357
+
2358
+ // ── Step 1: VA + Wallet → Extended ──────────────────────────────────
2359
+ if (this._budget.vaWalletUsd > CASE_THRESHOLD_USD && remaining > CASE_THRESHOLD_USD) {
2360
+ const use = Math.min(this._budget.vaWalletUsd, remaining);
2361
+
2362
+ // VA first, then wallet
2363
+ if (this._budget.vaUsd > CASE_THRESHOLD_USD) {
2364
+ const { remaining: vaRem } = this._getVAToEXTENDEDRoute(remaining, routes, false);
2365
+ remaining = vaRem;
2366
+ }
2367
+ if (remaining > CASE_THRESHOLD_USD) {
2368
+ const { remaining: walletRem } = this._getWalletToEXTENDEDRoute(remaining, routes, false);
2369
+ remaining = walletRem;
2370
+ }
2371
+ }
2372
+
2373
+ // ── Step 2: Vesu borrow → VA → Extended ─────────────────────────────
2374
+ if (remaining > CASE_THRESHOLD_USD && this._budget.vesuBorrowCapacity > CASE_THRESHOLD_USD) {
2375
+ const { remaining: borrowRem } = this._buildVesuBorrowRoutes(Math.min(remaining, this._budget.vesuBorrowCapacity), routes);
2376
+ const borrowed = remaining - borrowRem;
2377
+ if (remaining != borrowRem) {
2378
+ const { remaining: vaRem } = this._getVAToEXTENDEDRoute(borrowed, routes, false);
2379
+ }
2380
+ remaining = borrowRem;
2381
+ routes.forEach((r, i) => { r.priority = i; });
2382
+ remaining -= borrowed;
2383
+ caseId = CaseId.LTV_VESU_LOW_TO_EXTENDED;
2384
+ }
2385
+
2386
+ if (remaining > CASE_THRESHOLD_USD) {
2387
+ throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
2388
+ }
2389
+
2390
+ routes.forEach((r, i) => { r.priority = i; });
2391
+
2392
+ return [{
2393
+ case: CASE_DEFINITIONS[caseId],
2394
+ additionalArgs: { amount: safeUsdcWeb3Number(marginNeeded) },
2395
+ routes,
2396
+ }];
2397
+ }
2398
+
2399
+ // ── LTV Extended route builders ───────────────────────────────────────
2400
+
2401
+ /**
2402
+ * LTV_EXTENDED_HIGH_USE_VA_OR_WALLET:
2403
+ * VA/Wallet has funds → route them to Extended.
2404
+ * Routes: [VA_TO_EXTENDED, WALLET_TO_EXTENDED] (wallet-first, then VA for remainder)
2405
+ */
2406
+ // private _buildLtvExtendedDepositFromVARoutes(amountUsd: number): ExecutionRoute[] {
2407
+ // const routes: ExecutionRoute[] = this._buildExtendedDepositRoutes(amountUsd);
2408
+ // routes.forEach((r, i) => { r.priority = i; });
2409
+ // return routes;
2410
+ // }
2411
+
2412
+ /**
2413
+ * LTV_VESU_LOW_TO_EXTENDED:
2414
+ * Borrow USDC from Vesu, route through VA to Extended.
2415
+ * Routes: [VESU_BORROW, VA_TO_EXTENDED]
2416
+ */
2417
+ // private _buildLtvExtendedBorrowFromVesuRoutes(borrowUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
2418
+ // const routes: ExecutionRoute[] = [];
2419
+ // const amt = safeUsdcWeb3Number(borrowUsd);
2420
+
2421
+ // // Borrow USDC from Vesu (lands in VA)
2422
+ // const { routes: borrowRoutes } = this._buildVesuBorrowRoutes(borrowUsd, vesuDeltas);
2423
+ // routes.push(...borrowRoutes);
2424
+
2425
+ // // VA → Extended
2426
+ // routes.push({ type: RouteType.VA_TO_EXTENDED, amount: amt, priority: routes.length });
2427
+ // this._budget.applyExtendedBalanceChange(borrowUsd);
2428
+
2429
+ // routes.forEach((r, i) => { r.priority = i; });
2430
+ // return routes;
2431
+ // }
2432
+
2433
+ /**
2434
+ * MARGIN_CRISIS_EXTENDED:
2435
+ * Borrow beyond target HF on Vesu to free USDC, deposit to Extended,
2436
+ * then decrease Vesu/Extended lever, undo crisis leverage.
2437
+ * Routes: [CRISIS_BORROW_BEYOND_TARGET_HF, VA_TO_EXTENDED,
2438
+ * VESU_MULTIPLY_DECREASE_LEVER, EXTENDED_DECREASE_LEVER,
2439
+ * CRISIS_UNDO_EXTENDED_MAX_LEVERAGE]
2440
+ */
2441
+ // private _buildMarginCrisisExtendedRoutes(neededUsd: number, vesuDeltas: VesuPoolDelta[]): ExecutionRoute[] {
2442
+ // // ! todo need to verify this case properly
2443
+
2444
+ // const routes: ExecutionRoute[] = [];
2445
+ // const amt = safeUsdcWeb3Number(neededUsd);
2446
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
2447
+
2448
+ // // Borrow beyond normal target HF to free USDC
2449
+ // const pool = vesuDeltas.find((d) => d.debtDelta.greaterThan(0)) ?? vesuDeltas[0];
2450
+ // routes.push({
2451
+ // type: RouteType.CRISIS_BORROW_BEYOND_TARGET_HF,
2452
+ // poolId: pool?.poolId ?? ('' as unknown as ContractAddr),
2453
+ // token: pool?.debtToken?.symbol ?? this._config.assetToken.symbol,
2454
+ // amount: amt,
2455
+ // priority: routes.length,
2456
+ // });
2457
+
2458
+ // // Borrowed USDC (now in VA) → Extended
2459
+ // routes.push({ type: RouteType.VA_TO_EXTENDED, amount: amt, priority: routes.length });
2460
+ // this._budget.applyExtendedBalanceChange(neededUsd);
2461
+
2462
+ // // Wait for Extended deposit to be credited
2463
+ // routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
2464
+
2465
+ // // Decrease Vesu leverage
2466
+ // routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
2467
+
2468
+ // // Decrease Extended exposure to match
2469
+ // routes.push({ type: RouteType.EXTENDED_DECREASE_LEVER, amount: amt, instrument, priority: routes.length });
2470
+ // this._budget.applyExtendedExposureDelta(instrument, new Web3Number(amt.negated().toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS));
2471
+
2472
+ // // Revert crisis leverage
2473
+ // routes.push({ type: RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE, priority: routes.length });
2474
+
2475
+ // routes.forEach((r, i) => { r.priority = i; });
2476
+ // return routes;
2477
+ // }
2478
+
2479
+ // ! todo implement max lever amount per execution cycle
2480
+
2481
+ /**
2482
+ * 3. New Deposits / Excess Funds
2483
+ *
2484
+ * Computes how much exposure to create on Vesu and Extended, then
2485
+ * distributes available funds using shortest-path priority chains:
2486
+ * Extended margin: Wallet → VA → Vesu borrow
2487
+ * Vesu margin: VA → Wallet → Extended
2488
+ */
2489
+ /**
2490
+ * 3. New Deposits / Excess Funds
2491
+ *
2492
+ * Computes allocation split between Vesu and Extended, then sources
2493
+ * funds and creates lever-increase routes.
2494
+ *
2495
+ * Fund flow (principle #3 — accumulate transfers, defer wait):
2496
+ * Phase A: fund Extended (wallet→ext, VA→ext, vesu-borrow→VA→ext)
2497
+ * Phase B: fund Vesu VA shortfall (wallet→VA, ext→wallet + wallet→VA)
2498
+ * Phase C: RETURN_TO_WAIT (if any transfer to Extended occurred)
2499
+ * Phase D: lever routes (VESU_MULTIPLY, EXTENDED_INCREASE) near each other (#4)
2500
+ */
2501
+ private _classifyDeposits(withdrawAmount: Web3Number): SolveCaseEntry[] {
2502
+ if (withdrawAmount.toNumber() > CASE_THRESHOLD_USD) return [];
2503
+
2504
+ const distributableAmount = this._computeDistributableAmount(
2505
+ this._budget.vesuPerPoolDebtDeltasToBorrow, withdrawAmount,
2506
+ );
2507
+ if (distributableAmount.toNumber() <= CASE_THRESHOLD_USD) return [];
2508
+
2509
+ const { vesuAllocationUsd, extendedAllocationUsd } =
2510
+ this._computeAllocationSplit(distributableAmount);
2511
+
2512
+ const vesuDeltas = this._computePerPoolCollateralDeltas(
2513
+ vesuAllocationUsd, this._budget.vesuPerPoolDebtDeltasToBorrow,
2514
+ );
2515
+
2516
+ const extendedPositionDeltas = this._computeExtendedPositionDeltas(vesuDeltas);
2517
+ const extendedDepositDelta = this._computeExtendedDepositDelta(extendedAllocationUsd);
2518
+ const vesuDepositAmount = this._computeVesuDepositAmount(vesuDeltas);
2519
+
2520
+ const routes: ExecutionRoute[] = [];
2521
+ let needsWait = false;
2522
+
2523
+ // ── Phase A: Fund Extended ──────────────────────────────────────────
2524
+ if (extendedDepositDelta.toNumber() > CASE_THRESHOLD_USD) {
2525
+ let rem = extendedDepositDelta.toNumber();
2526
+
2527
+ // Wallet → Extended
2528
+ if (rem > CASE_THRESHOLD_USD) {
2529
+ const { remaining } = this._getWalletToEXTENDEDRoute(rem, routes, false);
2530
+ if (remaining < rem) needsWait = true;
2531
+ rem = remaining;
2532
+ }
2533
+
2534
+ // VA → Extended
2535
+ if (rem > CASE_THRESHOLD_USD) {
2536
+ const { remaining } = this._getVAToEXTENDEDRoute(rem, routes, false);
2537
+ if (remaining < rem) needsWait = true;
2538
+ rem = remaining;
2539
+ }
2540
+
2541
+ // Vesu borrow → VA → Extended
2542
+ if (rem > CASE_THRESHOLD_USD && this._budget.vesuBorrowCapacity > CASE_THRESHOLD_USD) {
2543
+ const { remaining: borrowRem } = this._buildVesuBorrowRoutes(rem, routes);
2544
+ const borrowed = rem - borrowRem;
2545
+ if (borrowRem != rem) {
2546
+ this._getVAToEXTENDEDRoute(borrowed, routes, false);
2547
+ needsWait = true;
2548
+ rem = borrowRem;
2549
+ }
2550
+ }
2551
+ }
2552
+
2553
+ // ── Phase B: Fund Vesu VA shortfall ─────────────────────────────────
2554
+ if (vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD) {
2555
+ const vaShortfall = vesuDepositAmount.toNumber() - this._budget.vaUsd;
2556
+ if (vaShortfall > CASE_THRESHOLD_USD) {
2557
+ let rem = vaShortfall;
2558
+
2559
+ // Wallet → VA
2560
+ if (rem > CASE_THRESHOLD_USD && this._budget.walletUsd > CASE_THRESHOLD_USD) {
2561
+ const { remaining } = this._getWalletToVARoute(Math.min(this._budget.walletUsd, rem), routes);
2562
+ rem = remaining;
2563
+ }
2564
+
2565
+ // check if withdrawal is enough to cover the shortfall
2566
+ // if not, we visit upnl first
2567
+ const isWithdrawalEnough = rem <= this._budget.extAvailWithdraw;
2568
+ if (!isWithdrawalEnough && rem > CASE_THRESHOLD_USD) {
2569
+ const { remaining: upnlRem } = this._getUpnlRoute(rem, routes);
2570
+ rem = upnlRem;
2571
+ }
2572
+
2573
+ // Extended → Wallet → VA (needs wait)
2574
+ if (rem > CASE_THRESHOLD_USD && this._budget.extAvailWithdraw > CASE_THRESHOLD_USD) {
2575
+ const extUse = Math.min(rem, this._budget.extAvailWithdraw);
2576
+ this._getExtendedToWalletRoute(extUse, routes);
2577
+ this._getWALLETToVARoute(extUse, routes);
2578
+ rem -= extUse;
2579
+ needsWait = false; // _getExtendedToWalletRoute already added RETURN_TO_WAIT
2580
+ }
2581
+ }
2582
+ }
2583
+
2584
+ // ── Phase C: Wait for transfers to settle ───────────────────────────
2585
+ if (needsWait) {
2586
+ routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
2587
+ }
2588
+
2589
+ // ── Phase D: Vesu lever increase ────────────────────────────────────
2590
+ for (const vesuDelta of vesuDeltas) {
2591
+ if (vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD) {
2592
+ // add avnu deposit swap route
2593
+ // routes.push({
2594
+ // type: RouteType.AVNU_DEPOSIT_SWAP,
2595
+ // priority: routes.length,
2596
+ // fromToken: vesuDelta.collateralToken.symbol,
2597
+ // fromAmount: vesuDepositAmount,
2598
+ // toToken: vesuDelta.debtToken.symbol,
2599
+ // });
2600
+ }
2601
+ if (vesuDelta.collateralDelta.toNumber() > 0) {
2602
+ const swappedAmount = new Web3Number((vesuDepositAmount.toNumber() * vesuDelta.debtPrice / (vesuDelta.collateralPrice ?? 0)).toFixed(6), vesuDelta.collateralToken.decimals);
2603
+ routes.push({
2604
+ type: RouteType.VESU_MULTIPLY_INCREASE_LEVER,
2605
+ priority: routes.length,
2606
+ collateralToken: vesuDelta.collateralToken,
2607
+ debtToken: vesuDelta.debtToken,
2608
+ marginAmount: swappedAmount, // should be the swapped amount as per vesu multiply adapter
2609
+ swappedCollateralAmount: vesuDelta.collateralDelta.minus(swappedAmount),
2610
+ debtAmount: vesuDelta.debtDelta,
2611
+ poolId: vesuDelta.poolId,
2612
+ } as VesuMultiplyRoute);
2613
+ }
2614
+ }
2615
+
2616
+ // ── Phase D: Extended lever increase ────────────────────────────────
2617
+ for (const epDelta of extendedPositionDeltas) {
2618
+ if (epDelta.delta.toNumber() > 0) {
2619
+ routes.push({
2620
+ type: RouteType.EXTENDED_INCREASE_LEVER,
2621
+ priority: routes.length,
2622
+ instrument: epDelta.instrument,
2623
+ amount: epDelta.delta,
2624
+ } as ExtendedLeverRoute);
2625
+ }
2626
+ }
2627
+
2628
+ if (routes.length === 0) return [];
2629
+ routes.forEach((r, i) => { r.priority = i; });
2630
+ return [{
2631
+ case: CASE_DEFINITIONS[CaseId.DEPOSIT_FRESH_VAULT],
2632
+ additionalArgs: {
2633
+ amount: safeUsdcWeb3Number(distributableAmount.toNumber()),
2634
+ },
2635
+ routes,
2636
+ }];
2637
+ }
2638
+
2639
+ /** 4. Exposure Imbalance */
2640
+ // private _classifyImbalance(
2641
+ // vesuDeltas: VesuPoolDelta[],
2642
+ // ): SolveCaseEntry[] {
2643
+ // const extBtc = this._totalExtendedExposure().toNumber();
2644
+ // const vesuBtc = this._totalVesuCollateral().toNumber();
2645
+ // const maxBtc = Math.max(extBtc, vesuBtc);
2646
+ // if (maxBtc <= 0) return [];
2647
+ // const imb = extBtc - vesuBtc;
2648
+ // if (Math.abs(imb) / maxBtc <= IMBALANCE_THRESHOLD_FRACTION) return [];
2649
+ // const exposureDiffBtc = Math.abs(imb);
2650
+ // const rem = this._budget.totalUnused;
2651
+
2652
+ // if (imb > 0) {
2653
+ // // Extended excess short → need more Vesu collateral
2654
+ // if (rem > CASE_THRESHOLD_USD) {
2655
+ // return [{
2656
+ // case: CASE_DEFINITIONS[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS],
2657
+ // additionalArgs: {},
2658
+ // routes: this._buildImbalanceExtExcessShortHasFundsRoutes(rem, exposureDiffBtc, vesuDeltas),
2659
+ // }];
2660
+ // }
2661
+ // return [{
2662
+ // case: CASE_DEFINITIONS[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS],
2663
+ // additionalArgs: {},
2664
+ // routes: this._buildImbalanceExtExcessShortNoFundsRoutes(exposureDiffBtc),
2665
+ // }];
2666
+ // }
2667
+
2668
+ // // Vesu excess long → need more Extended exposure
2669
+ // if (rem > CASE_THRESHOLD_USD) {
2670
+ // return [{
2671
+ // case: CASE_DEFINITIONS[CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS],
2672
+ // additionalArgs: {},
2673
+ // routes: this._buildImbalanceVesuExcessLongHasFundsRoutes(rem, exposureDiffBtc, vesuDeltas),
2674
+ // }];
2675
+ // }
2676
+ // return [{
2677
+ // case: CASE_DEFINITIONS[CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS],
2678
+ // additionalArgs: {},
2679
+ // routes: this._buildImbalanceVesuExcessLongNoFundsRoutes(vesuDeltas),
2680
+ // }];
2681
+ // }
2682
+
2683
+ // ── Imbalance route builders ──────────────────────────────────────────
2684
+
2685
+ /**
2686
+ * IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS:
2687
+ * Extended has too much short exposure vs Vesu. Use VA/Wallet funds to
2688
+ * add Vesu collateral and reduce Extended exposure.
2689
+ *
2690
+ * The new Vesu collateral covers part of the imbalance; the remaining
2691
+ * gap is closed by reducing Extended short exposure.
2692
+ *
2693
+ * Routes: [AVNU_DEPOSIT_SWAP, VESU_MULTIPLY_INCREASE_LEVER, EXTENDED_DECREASE_LEVER]
2694
+ */
2695
+ // private _buildImbalanceExtExcessShortHasFundsRoutes(
2696
+ // fundsUsd: number, exposureDiffBtc: number, vesuDeltas: VesuPoolDelta[],
2697
+ // ): ExecutionRoute[] {
2698
+ // const routes: ExecutionRoute[] = [];
2699
+ // const collSym = this._config.collateralToken.symbol;
2700
+ // const debtSym = this._config.assetToken.symbol;
2701
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
2702
+
2703
+ // // Swap USDC → BTC for Vesu collateral
2704
+ // routes.push({
2705
+ // type: RouteType.AVNU_DEPOSIT_SWAP,
2706
+ // fromToken: debtSym,
2707
+ // fromAmount: safeUsdcWeb3Number(fundsUsd),
2708
+ // toToken: collSym,
2709
+ // priority: routes.length,
2710
+ // } as SwapRoute);
2711
+
2712
+ // // Increase Vesu lever (deposit collateral + borrow)
2713
+ // routes.push(...this._buildVesuIncreaseLeverRoutes(vesuDeltas));
2714
+
2715
+ // // New Vesu collateral covers part of the imbalance
2716
+ // const newVesuCollBtc = vesuDeltas
2717
+ // .filter((d) => d.collateralDelta.greaterThan(0))
2718
+ // .reduce((sum, d) => sum + d.collateralDelta.toNumber(), 0);
2719
+
2720
+ // // Remaining imbalance that needs to be closed on Extended
2721
+ // const extDecreaseExposureBtc = Math.max(0, exposureDiffBtc - newVesuCollBtc);
2722
+
2723
+ // if (extDecreaseExposureBtc > 0) {
2724
+ // const decDelta = new Web3Number(extDecreaseExposureBtc.toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
2725
+ // routes.push({
2726
+ // type: RouteType.EXTENDED_DECREASE_LEVER,
2727
+ // amount: decDelta,
2728
+ // instrument,
2729
+ // priority: routes.length,
2730
+ // });
2731
+ // this._budget.applyExtendedExposureDelta(instrument, new Web3Number(decDelta.negated().toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS));
2732
+
2733
+ // }
2734
+
2735
+ // routes.forEach((r, i) => { r.priority = i; });
2736
+ // return routes;
2737
+ // }
2738
+
2739
+ /**
2740
+ * IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS:
2741
+ * No available funds to add Vesu collateral → reduce Extended exposure
2742
+ * by the full imbalance amount.
2743
+ * Routes: [EXTENDED_DECREASE_LEVER]
2744
+ */
2745
+ private _buildImbalanceExtExcessShortNoFundsRoutes(exposureDiffBtc: number): ExecutionRoute[] {
2746
+ const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
2747
+ const decDelta = new Web3Number(exposureDiffBtc.toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
2748
+ this._budget.applyExtendedExposureDelta(instrument, new Web3Number(decDelta.negated().toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS));
2749
+ return [{
2750
+ type: RouteType.EXTENDED_DECREASE_LEVER,
2751
+ amount: decDelta,
2752
+ instrument,
2753
+ priority: 0,
2754
+ }];
2755
+ }
2756
+
2757
+ /**
2758
+ * IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS:
2759
+ * Vesu has too much long exposure vs Extended. Deposit funds on Extended
2760
+ * to increase short exposure, and decrease Vesu lever for the remainder.
2761
+ *
2762
+ * Routes: [WALLET_TO_EXTENDED / VA_TO_EXTENDED, EXTENDED_INCREASE_LEVER,
2763
+ * VESU_MULTIPLY_DECREASE_LEVER]
2764
+ */
2765
+ // private _buildImbalanceVesuExcessLongHasFundsRoutes(
2766
+ // fundsUsd: number, exposureDiffBtc: number, vesuDeltas: VesuPoolDelta[],
2767
+ // ): ExecutionRoute[] {
2768
+ // const routes: ExecutionRoute[] = [];
2769
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
2770
+
2771
+ // // ExecutionRoute funds to Extended (wallet-first, then VA)
2772
+ // routes.push(...this._buildExtendedDepositRoutes(fundsUsd));
2773
+
2774
+ // // Compute how much new Extended exposure the deposited funds create
2775
+ // const extLeverage = calculateExtendedLevergae();
2776
+ // const newExtExposureUsd = fundsUsd * extLeverage;
2777
+ // const avgCollPrice = this._budget.vesuPoolStates[0]?.collateralPrice ?? 1;
2778
+ // const newExtExposureBtc = avgCollPrice > 0 ? newExtExposureUsd / avgCollPrice : 0;
2779
+
2780
+ // // Increase Extended lever by the new exposure (capped to imbalance)
2781
+ // const extIncreaseBtc = Math.min(newExtExposureBtc, exposureDiffBtc);
2782
+ // if (extIncreaseBtc > 0) {
2783
+ // const incDelta = new Web3Number(extIncreaseBtc.toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
2784
+ // routes.push({
2785
+ // type: RouteType.EXTENDED_INCREASE_LEVER,
2786
+ // amount: incDelta,
2787
+ // instrument,
2788
+ // priority: routes.length,
2789
+ // });
2790
+ // this._budget.applyExtendedExposureDelta(instrument, incDelta);
2791
+ // }
2792
+
2793
+ // // Remaining imbalance closed by decreasing Vesu lever
2794
+ // const vesuDecreaseBtc = Math.max(0, exposureDiffBtc - extIncreaseBtc);
2795
+ // if (vesuDecreaseBtc > 0) {
2796
+ // routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
2797
+ // }
2798
+
2799
+ // routes.forEach((r, i) => { r.priority = i; });
2800
+ // return routes;
2801
+ // }
2802
+
2803
+ /**
2804
+ * IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS:
2805
+ * No funds to increase Extended → decrease Vesu lever by the full imbalance.
2806
+ * Routes: [VESU_MULTIPLY_DECREASE_LEVER]
2807
+ */
2808
+ // private _buildImbalanceVesuExcessLongNoFundsRoutes(vesuDeltas: VesuPoolDelta[]): ExecutionRoute[] {
2809
+ // const routes: ExecutionRoute[] = this._buildVesuDecreaseLeverRoutes(vesuDeltas);
2810
+ // routes.forEach((r, i) => { r.priority = i; });
2811
+ // return routes;
2812
+ // }
2813
+
2814
+ // ── Main classifier (orchestrator) ─────────────────────────────────────
2815
+
2816
+ /**
2817
+ * Classifies the current state into actionable cases. Each case carries
2818
+ * its own execution routes with amounts and state info.
2819
+ */
2820
+ private _classifyCases(withdrawAmount: Web3Number): SolveCaseEntry[] {
2821
+ this._budget.initBudget();
2822
+
2823
+ // withdrawal is simply about available funds and unwinding positions.
2824
+ const withdrawalCases = this._classifyWithdrawal(withdrawAmount);
2825
+
2826
+ // 2. LTV Rebalance — Vesu high LTV (fund movement to VA)
2827
+ const ltvVesuCases = this._classifyLtvVesu();
2828
+
2829
+ // 3. LTV Rebalance — Extended low margin
2830
+ const ltvExtendedCases = this._classifyLtvExtended();
2831
+
2832
+ // 4. New Deposits — allocate remaining budget to lever operations
2833
+ const depositCases = this._classifyDeposits(withdrawAmount);
2834
+
2835
+ // ...this._classifyImbalance(vesuDeltas),
2836
+
2837
+ return [
2838
+ ...withdrawalCases,
2839
+ ...ltvVesuCases,
2840
+ ...ltvExtendedCases,
2841
+ ...depositCases,
2842
+ ];
2843
+ }
2844
+
2845
+
2846
+
2847
+ // ═══════════════════════════════════════════════════════════════════════════
2848
+ // Private — aggregation helpers
2849
+ // ═══════════════════════════════════════════════════════════════════════════
2850
+
2851
+ private _totalVesuCollateral(): Web3Number {
2852
+ return this._budget.vesuPoolStates.reduce(
2853
+ (acc, pool) =>
2854
+ acc.plus(
2855
+ pool.collateralAmount,
2856
+ ),
2857
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
2858
+ );
2859
+ }
2860
+
2861
+ private _totalVesuCollateralUsd(): Web3Number {
2862
+ return this._budget.vesuPoolStates.reduce(
2863
+ (acc, pool) =>
2864
+ acc.plus(
2865
+ pool.collateralAmount.multipliedBy(pool.collateralPrice),
2866
+ ),
2867
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
2868
+ );
2869
+ }
2870
+
2871
+ private _totalExtendedExposure(): Web3Number {
2872
+ return this._budget.extendedPositions.reduce(
2873
+ (acc, position) => acc.plus(position.size),
2874
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
2875
+ );
2876
+ }
2877
+
2878
+ private _totalExtendedExposureUsd(): Web3Number {
2879
+ return this._budget.extendedPositions.reduce(
2880
+ (acc, position) => acc.plus(position.valueUsd),
2881
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
2882
+ );
2883
+ }
2884
+
2885
+ // ═══════════════════════════════════════════════════════════════════════════
2886
+ // Private — logging
2887
+ // ═══════════════════════════════════════════════════════════════════════════
2888
+
2889
+ private _logSolveResult(result: SolveResult): void {
2890
+ // Log detected cases
2891
+ logger.info(
2892
+ `${this._tag}::solve detected ${result.cases.length} case(s): ` +
2893
+ result.cases
2894
+ .map((c) => `[${c.case.category}] ${c.case.id}`)
2895
+ .join(', '),
2896
+ );
2897
+ for (const entry of result.cases) {
2898
+ logger.info(
2899
+ `${this._tag}::solve case "${entry.case.title}" — ` +
2900
+ `steps: ${entry.case.steps.length}`,
2901
+ );
2902
+ }
2903
+
2904
+ logger.info(
2905
+ `${this._tag}::solve result — ` +
2906
+ `extendedDeposit: ${result.extendedDeposit.toNumber()}, ` +
2907
+ `vesuAllocUsd: ${result.vesuAllocationUsd.toNumber()}, ` +
2908
+ `extAllocUsd: ${result.extendedAllocationUsd.toNumber()}, ` +
2909
+ `bringLiquidity: ${result.bringLiquidityAmount.toNumber()}`,
2910
+ );
2911
+
2912
+ for (let i = 0; i < result.vesuDeltas.length; i++) {
2913
+ const delta = result.vesuDeltas[i];
2914
+ logger.info(
2915
+ `${this._tag}::solve vesu[${i}] ` +
2916
+ `pool=${delta.poolId.shortString()} ` +
2917
+ `debtDelta=${delta.debtDelta.toNumber()} ` +
2918
+ `collateralDelta=${delta.collateralDelta.toNumber()}`,
2919
+ );
2920
+ }
2921
+
2922
+ for (let i = 0; i < result.extendedPositionDeltas.length; i++) {
2923
+ const delta = result.extendedPositionDeltas[i];
2924
+ logger.info(
2925
+ `${this._tag}::solve extended[${i}] ` +
2926
+ `instrument=${delta.instrument} ` +
2927
+ `delta=${delta.delta.toNumber()}`,
2928
+ );
2929
+ }
2930
+
2931
+ for (const entry of result.cases) {
2932
+ const caseRoutes = entry.routes;
2933
+ logger.info(
2934
+ `${this._tag}::solve case "${entry.case.id}" routes (${caseRoutes.length}): ` +
2935
+ caseRoutes
2936
+ .map((r) => `[${r.priority}] ${r.type} ${routeSummary(r)}`)
2937
+ .join(' → '),
2938
+ );
2939
+ }
2940
+ }
2941
+ }