@strkfarm/sdk 2.0.0-dev.3 → 2.0.0-dev.31

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 (75) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +78475 -45620
  4. package/dist/index.browser.mjs +19580 -9901
  5. package/dist/index.d.ts +3763 -1424
  6. package/dist/index.js +20977 -11063
  7. package/dist/index.mjs +20945 -11087
  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/bignumber.browser.ts +6 -1
  13. package/src/dataTypes/bignumber.node.ts +5 -1
  14. package/src/dataTypes/index.ts +3 -2
  15. package/src/dataTypes/mynumber.ts +141 -0
  16. package/src/global.ts +76 -41
  17. package/src/index.browser.ts +2 -1
  18. package/src/interfaces/common.tsx +175 -3
  19. package/src/modules/ExtendedWrapperSDk/types.ts +28 -5
  20. package/src/modules/ExtendedWrapperSDk/wrapper.ts +275 -59
  21. package/src/modules/apollo-client-config.ts +28 -0
  22. package/src/modules/avnu.ts +4 -4
  23. package/src/modules/ekubo-pricer.ts +79 -0
  24. package/src/modules/ekubo-quoter.ts +48 -30
  25. package/src/modules/erc20.ts +17 -0
  26. package/src/modules/harvests.ts +43 -29
  27. package/src/modules/pragma.ts +23 -8
  28. package/src/modules/pricer-from-api.ts +156 -15
  29. package/src/modules/pricer-lst.ts +1 -1
  30. package/src/modules/pricer.ts +40 -4
  31. package/src/modules/pricerBase.ts +2 -1
  32. package/src/node/deployer.ts +36 -1
  33. package/src/node/pricer-redis.ts +2 -1
  34. package/src/strategies/base-strategy.ts +78 -10
  35. package/src/strategies/ekubo-cl-vault.tsx +906 -347
  36. package/src/strategies/factory.ts +159 -0
  37. package/src/strategies/index.ts +7 -1
  38. package/src/strategies/registry.ts +239 -0
  39. package/src/strategies/sensei.ts +335 -7
  40. package/src/strategies/svk-strategy.ts +97 -27
  41. package/src/strategies/types.ts +4 -0
  42. package/src/strategies/universal-adapters/adapter-utils.ts +2 -1
  43. package/src/strategies/universal-adapters/avnu-adapter.ts +180 -265
  44. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  45. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  46. package/src/strategies/universal-adapters/extended-adapter.ts +490 -316
  47. package/src/strategies/universal-adapters/index.ts +11 -8
  48. package/src/strategies/universal-adapters/svk-troves-adapter.ts +364 -0
  49. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  50. package/src/strategies/universal-adapters/usdc<>usdce-adapter.ts +200 -0
  51. package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
  52. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +476 -0
  53. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1067 -704
  54. package/src/strategies/universal-adapters/vesu-position-common.ts +251 -0
  55. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  56. package/src/strategies/universal-lst-muliplier-strategy.tsx +397 -204
  57. package/src/strategies/universal-strategy.tsx +1426 -1173
  58. package/src/strategies/vesu-extended-strategy/services/executionService.ts +2233 -0
  59. package/src/strategies/vesu-extended-strategy/services/extended-vesu-state-manager.ts +4087 -0
  60. package/src/strategies/vesu-extended-strategy/services/ltv-imbalance-rebalance-math.ts +783 -0
  61. package/src/strategies/vesu-extended-strategy/services/operationService.ts +38 -16
  62. package/src/strategies/vesu-extended-strategy/types/transaction-metadata.ts +88 -0
  63. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +1 -0
  64. package/src/strategies/vesu-extended-strategy/utils/constants.ts +5 -6
  65. package/src/strategies/vesu-extended-strategy/utils/helper.ts +259 -103
  66. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +688 -817
  67. package/src/strategies/vesu-rebalance.tsx +255 -152
  68. package/src/utils/cacheClass.ts +11 -2
  69. package/src/utils/health-factor-math.ts +4 -1
  70. package/src/utils/index.ts +3 -1
  71. package/src/utils/logger.browser.ts +22 -4
  72. package/src/utils/logger.node.ts +259 -24
  73. package/src/utils/starknet-call-parser.ts +1036 -0
  74. package/src/utils/strategy-utils.ts +61 -0
  75. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
@@ -0,0 +1,4087 @@
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
+ import { VesuConfig } from "../utils/config.runtime";
16
+ import { rebalance, routableDrawAmount, type RebalanceDeltas } from "./ltv-imbalance-rebalance-math";
17
+
18
+ // ─── State types ───────────────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Snapshot of a single Vesu pool's position: collateral, debt, and their prices.
22
+ */
23
+ export interface VesuPoolState {
24
+ poolId: ContractAddr;
25
+ collateralToken: TokenInfo;
26
+ debtToken: TokenInfo;
27
+ collateralAmount: Web3Number;
28
+ collateralUsdValue: number;
29
+ debtAmount: Web3Number;
30
+ debtUsdValue: number;
31
+ collateralPrice: number;
32
+ debtPrice: number;
33
+ }
34
+
35
+ /**
36
+ * Snapshot of a single Extended exchange open position.
37
+ */
38
+ export interface ExtendedPositionState {
39
+ instrument: string;
40
+ side: string;
41
+ size: Web3Number;
42
+ valueUsd: Web3Number;
43
+ leverage: string;
44
+ }
45
+
46
+ /**
47
+ * Snapshot of the Extended exchange account-level balance.
48
+ */
49
+ export interface ExtendedBalanceState {
50
+ equity: Web3Number;
51
+ availableForTrade: Web3Number;
52
+ availableForWithdrawal: Web3Number;
53
+ unrealisedPnl: Web3Number;
54
+ balance: Web3Number;
55
+ /**
56
+ * Funds in transit to/from Extended.
57
+ * Positive = deposit in transit (funds left wallet, not yet credited on Extended).
58
+ * Negative = withdrawal in transit (funds left Extended, not yet received in wallet).
59
+ */
60
+ pendingDeposit: Web3Number;
61
+ }
62
+
63
+
64
+ type Inputs = {
65
+ extAvlWithdraw: number;
66
+ extUpnl: number;
67
+ vaUsd: number;
68
+ walletUsd: number;
69
+ vesuBorrowCapacity: number;
70
+ vesuLeverage: number;
71
+ extendedLeverage: number;
72
+ };
73
+
74
+ type Deltas = {
75
+ dExtAvlWithdraw: number;
76
+ dExtUpnl: number;
77
+ dVaUsd: number;
78
+ dWalletUsd: number;
79
+ dVesuBorrowCapacity: number;
80
+ finalVaUsd: number;
81
+ finalExtended: number;
82
+ isExtendedToVesu: boolean;
83
+ };
84
+
85
+ /**
86
+ * Generic token balance with USD valuation.
87
+ */
88
+ export interface TokenBalance {
89
+ token: TokenInfo;
90
+ amount: Web3Number;
91
+ usdValue: number;
92
+ }
93
+
94
+ // ─── Solve result types ────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Per-position exposure change on the Extended exchange.
98
+ * Positive delta = increase position size, negative = reduce.
99
+ */
100
+ export interface ExtendedPositionDelta {
101
+ instrument: string;
102
+ delta: Web3Number;
103
+ }
104
+
105
+ /**
106
+ * Per-pool position change on Vesu.
107
+ * debtDelta: positive = borrow more, negative = repay.
108
+ * collateralDelta: positive = add collateral, negative = remove.
109
+ */
110
+ export interface VesuPoolDelta {
111
+ poolId: ContractAddr;
112
+ collateralToken: TokenInfo;
113
+ debtToken: TokenInfo;
114
+ debtDelta: Web3Number;
115
+ collateralDelta: Web3Number;
116
+ collateralPrice: number;
117
+ debtPrice: number;
118
+ }
119
+
120
+ // ─── ExecutionRoute types ───────────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Enumerates all possible fund-routing paths used during execution.
124
+ */
125
+ export enum RouteType {
126
+ /** Deposit USDC from operator wallet directly to Extended exchange */
127
+ WALLET_TO_EXTENDED = 'WALLET_TO_EXTENDED',
128
+ /** USDC from vault allocator → deposit to Extended (via manager) */
129
+ VA_TO_EXTENDED = 'VA_TO_EXTENDED',
130
+ /** Withdraw from Extended exchange → operator wallet */
131
+ EXTENDED_TO_WALLET = 'EXTENDED_TO_WALLET',
132
+ /** Swap USDC → BTC to deposit to Vesu */
133
+ AVNU_DEPOSIT_SWAP = 'AVNU_DEPOSIT_SWAP',
134
+ /** Increase leverage on Vesu i.e. deposit on vesu, borrow, in one go to create lever */
135
+ VESU_MULTIPLY_INCREASE_LEVER = 'VESU_MULTIPLY_INCREASE_LEVER',
136
+ /** Decrease leverage on Vesu i.e. withdraw from vesu, repay, in one go to reduce leverage */
137
+ VESU_MULTIPLY_DECREASE_LEVER = 'VESU_MULTIPLY_DECREASE_LEVER',
138
+ /** Swap BTC → USDC to withdraw from Vesu */
139
+ AVNU_WITHDRAW_SWAP = 'AVNU_WITHDRAW_SWAP',
140
+ /** Borrow additional USDC from Vesu (when wallet + VA insufficient for Extended) */
141
+ VESU_BORROW = 'VESU_BORROW',
142
+ /** Repay USDC debt to Vesu (debtDelta < 0) */
143
+ VESU_REPAY = 'VESU_REPAY',
144
+ /** Transfer USDC from operator wallet to vault allocator */
145
+ WALLET_TO_VA = 'WALLET_TO_VA',
146
+ /** Realize PnL on Extended exchange */
147
+ REALISE_PNL = 'REALISE_PNL',
148
+ /** Increase leverage on Extended exchange i.e. deposit on extended, in one go to create lever */
149
+ EXTENDED_INCREASE_LEVER = 'EXTENDED_INCREASE_LEVER',
150
+ /** Decrease leverage on Extended exchange i.e. withdraw from extended, in one go to reduce leverage */
151
+ EXTENDED_DECREASE_LEVER = 'EXTENDED_DECREASE_LEVER',
152
+
153
+ /** Increase leverage on Extended exchange to max leverage (e.g. 4x from 3x) */
154
+ CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE = 'CRISIS_INCREASE_EXTENDER_MAX_LEVERAGE',
155
+ /** Undo max leverage on Extended exchange to reduce leverage (e.g. 4x to 3x) */
156
+ CRISIS_UNDO_EXTENDED_MAX_LEVERAGE = 'CRISIS_UNDO_EXTENDED_MAX_LEVERAGE',
157
+
158
+ /** Borrow beyyond target HF (e.g. 1.2 from 1.4) */
159
+ CRISIS_BORROW_BEYOND_TARGET_HF = 'CRISIS_BORROW_BEYOND_TARGET_HF',
160
+
161
+ /** Bring liquidity from vault allocator to vault contract (for user withdrawals) */
162
+ BRING_LIQUIDITY = 'BRING_LIQUIDITY',
163
+
164
+ // often a bridge tx is involved. when the solve cycle runs again
165
+ // bridges funds shall be handled again
166
+ RETURN_TO_WAIT = 'RETURN_TO_WAIT',
167
+ }
168
+
169
+ // ─── Per-route-type metadata ─────────────────────────────────────────────────
170
+
171
+ /** Common fields shared by every route variant */
172
+ interface RouteBase {
173
+ /** Execution order — lower values execute first */
174
+ priority: number;
175
+ }
176
+
177
+ /** Simple fund-transfer routes: WALLET_TO_EXTENDED, VA_TO_EXTENDED, EXTENDED_TO_WALLET, WALLET_TO_VA */
178
+ export interface TransferRoute extends RouteBase {
179
+ type:
180
+ | RouteType.WALLET_TO_EXTENDED
181
+ | RouteType.VA_TO_EXTENDED
182
+ | RouteType.EXTENDED_TO_WALLET
183
+ | RouteType.WALLET_TO_VA;
184
+ /** Amount to transfer */
185
+ amount: Web3Number;
186
+ }
187
+
188
+ /** AVNU swap routes: AVNU_DEPOSIT_SWAP (USDC→BTC), AVNU_WITHDRAW_SWAP (BTC→USDC) */
189
+ export interface SwapRoute extends RouteBase {
190
+ type: RouteType.AVNU_DEPOSIT_SWAP | RouteType.AVNU_WITHDRAW_SWAP;
191
+ /** Source token symbol */
192
+ fromToken: string;
193
+ /** Source amount */
194
+ fromAmount: Web3Number;
195
+ /** Destination token symbol */
196
+ toToken: string;
197
+ /** Exact output amount (if known; otherwise undefined means best-effort) */
198
+ toAmount?: Web3Number;
199
+ }
200
+
201
+ /** Vesu multiply lever routes: deposit+borrow or withdraw+repay in one go */
202
+ export interface VesuMultiplyRoute extends RouteBase {
203
+ type: RouteType.VESU_MULTIPLY_INCREASE_LEVER | RouteType.VESU_MULTIPLY_DECREASE_LEVER;
204
+ /** Pool to interact with */
205
+ poolId: ContractAddr;
206
+ /** Collateral token info */
207
+ collateralToken: TokenInfo;
208
+ /** Collateral amount delta (positive = deposit, negative = withdraw) */
209
+ marginAmount: Web3Number; // in collateral token units
210
+ swappedCollateralAmount: Web3Number; // debt swapped, in collateral token units
211
+ /** Debt token info */
212
+ debtToken: TokenInfo;
213
+ /** Debt amount delta (positive = borrow, negative = repay) */
214
+ debtAmount: Web3Number;
215
+ }
216
+
217
+ /** Vesu single-side borrow / repay routes */
218
+ export interface VesuDebtRoute extends RouteBase {
219
+ type: RouteType.VESU_BORROW | RouteType.VESU_REPAY;
220
+ /** Pool to interact with */
221
+ poolId: ContractAddr;
222
+ /** Amount to borrow (positive) or repay (negative) */
223
+ amount: Web3Number;
224
+ /** Collateral token info */
225
+ collateralToken: TokenInfo;
226
+ /** Debt token info */
227
+ debtToken: TokenInfo;
228
+ }
229
+
230
+ /** Realise PnL on Extended exchange */
231
+ export interface RealisePnlRoute extends RouteBase {
232
+ type: RouteType.REALISE_PNL;
233
+ /** Amount of PnL to realise (the shortfall beyond available-to-withdraw) */
234
+ amount: Web3Number;
235
+ /** Extended instrument name (e.g. "BTC-USD") */
236
+ instrument: string;
237
+ }
238
+
239
+ /** Increase / decrease leverage on Extended exchange */
240
+ export interface ExtendedLeverRoute extends RouteBase {
241
+ type: RouteType.EXTENDED_INCREASE_LEVER | RouteType.EXTENDED_DECREASE_LEVER;
242
+ /** Change in exposure denominated in the exposure token */
243
+ amount: Web3Number;
244
+ /** Extended instrument name (e.g. "BTC-USD") */
245
+ instrument: string;
246
+ }
247
+
248
+ /** Crisis: temporarily increase / undo max leverage on Extended — no args for now */
249
+ export interface CrisisExtendedLeverRoute extends RouteBase {
250
+ type: RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE | RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE;
251
+ }
252
+
253
+ /** Crisis: borrow beyond the normal target HF */
254
+ export interface CrisisBorrowRoute extends RouteBase {
255
+ type: RouteType.CRISIS_BORROW_BEYOND_TARGET_HF;
256
+ /** Pool to borrow from */
257
+ poolId: ContractAddr;
258
+ /** Token being borrowed */
259
+ token: string;
260
+ /** Amount to borrow */
261
+ amount: Web3Number;
262
+ }
263
+
264
+ /** Bring liquidity from vault allocator to vault contract for user withdrawals */
265
+ export interface BringLiquidityRoute extends RouteBase {
266
+ type: RouteType.BRING_LIQUIDITY;
267
+ /** Amount to bring */
268
+ amount: Web3Number;
269
+ }
270
+
271
+ /** Control routes: RETURN_TO_WAIT (stop execution, resume in next cycle) */
272
+ export interface WaitRoute extends RouteBase {
273
+ type: RouteType.RETURN_TO_WAIT;
274
+ }
275
+
276
+ /**
277
+ * A single step in the execution plan produced by the solver.
278
+ * Discriminated union keyed on `type` — each variant carries only
279
+ * the metadata that specific route needs.
280
+ */
281
+ export type ExecutionRoute =
282
+ | TransferRoute
283
+ | SwapRoute
284
+ | VesuMultiplyRoute
285
+ | VesuDebtRoute
286
+ | RealisePnlRoute
287
+ | ExtendedLeverRoute
288
+ | CrisisExtendedLeverRoute
289
+ | CrisisBorrowRoute
290
+ | BringLiquidityRoute
291
+ | WaitRoute;
292
+
293
+ // ─── Case classification types ──────────────────────────────────────────────────
294
+
295
+ /**
296
+ * Broad category of a detected case. Multiple categories may apply simultaneously.
297
+ */
298
+ export enum CaseCategory {
299
+ LTV_REBALANCE = 'LTV_REBALANCE',
300
+ NEW_DEPOSITS = 'NEW_DEPOSITS',
301
+ WITHDRAWAL = 'WITHDRAWAL',
302
+ EXPOSURE_IMBALANCE = 'EXPOSURE_IMBALANCE',
303
+ MARGIN_CRISIS = 'MARGIN_CRISIS',
304
+ }
305
+
306
+ /**
307
+ * Specific case IDs corresponding to the situations defined in cases.json.
308
+ */
309
+ export enum CaseId {
310
+ // LTV Rebalance
311
+ MANAGE_LTV = 'MANAGE_LTV',
312
+ /** @deprecated use MANAGE_LTV */ LTV_VESU_LOW_TO_EXTENDED = 'LTV_VESU_LOW_TO_EXTENDED',
313
+ /** @deprecated use MANAGE_LTV */ LTV_EXTENDED_PROFITABLE_AVAILABLE = 'LTV_EXTENDED_PROFITABLE_AVAILABLE',
314
+ /** @deprecated use MANAGE_LTV */ LTV_EXTENDED_PROFITABLE_REALIZE = 'LTV_EXTENDED_PROFITABLE_REALIZE',
315
+ /** @deprecated use MANAGE_LTV */ LTV_VESU_HIGH_USE_VA_OR_WALLET = 'LTV_VESU_HIGH_USE_VA_OR_WALLET',
316
+ /** @deprecated use MANAGE_LTV */ LTV_EXTENDED_HIGH_USE_VA_OR_WALLET = 'LTV_EXTENDED_HIGH_USE_VA_OR_WALLET',
317
+
318
+ // New Deposits / Excess Funds
319
+ DEPOSIT_FRESH_VAULT = 'DEPOSIT_FRESH_VAULT',
320
+ DEPOSIT_EXTENDED_AVAILABLE = 'DEPOSIT_EXTENDED_AVAILABLE',
321
+ DEPOSIT_VESU_BORROW_CAPACITY = 'DEPOSIT_VESU_BORROW_CAPACITY',
322
+ DEPOSIT_COMBINATION = 'DEPOSIT_COMBINATION',
323
+ // Withdrawal
324
+ WITHDRAWAL_SIMPLE = 'WITHDRAWAL_SIMPLE',
325
+ // Margin Crisis
326
+ MARGIN_CRISIS_EXTENDED = 'MARGIN_CRISIS_EXTENDED',
327
+ MARGIN_CRISIS_VESU = 'MARGIN_CRISIS_VESU',
328
+ // Exposure Imbalance
329
+ IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS = 'IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS',
330
+ IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS = 'IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS',
331
+ IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS = 'IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS',
332
+ IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS = 'IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS',
333
+ }
334
+
335
+ /**
336
+ * A detected case describing the current system state and the
337
+ * high-level steps required to resolve it.
338
+ */
339
+ export interface SolveCase {
340
+ id: CaseId;
341
+ category: CaseCategory;
342
+ title: string;
343
+ description: string;
344
+ /** High-level steps describing what needs to happen for this case */
345
+ steps: string[];
346
+ }
347
+
348
+ /** Minimum USD amount to consider a case significant */
349
+ const CASE_THRESHOLD_USD = 5;
350
+ const CASE_MIN_BRIDING_USD = 10;
351
+ /** Decimal places for rounding collateral / exposure deltas in token terms (e.g. BTC) */
352
+ export const COLLATERAL_PRECISION = 4;
353
+
354
+ /** Safely create a Web3Number from a float, avoiding >15 significant digit errors */
355
+ function safeUsdcWeb3Number(value: number): Web3Number {
356
+ return new Web3Number(value.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
357
+ }
358
+
359
+ /**
360
+ * A single detected case with its metadata, amounts, and the execution
361
+ * routes that belong to this case.
362
+ */
363
+ export interface SolveCaseEntry {
364
+ case: SolveCase;
365
+ additionalArgs: { amount?: Web3Number };
366
+ routes: ExecutionRoute[];
367
+ }
368
+
369
+ /**
370
+ * Maps each CaseId to the RouteTypes that are relevant for resolving it.
371
+ * Used to filter the global route list into per-case route subsets.
372
+ */
373
+ export const CASE_ROUTE_TYPES: Record<CaseId, RouteType[]> = {
374
+ // LTV Rebalance — unified
375
+ [CaseId.MANAGE_LTV]: [
376
+ RouteType.REALISE_PNL, RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT,
377
+ RouteType.WALLET_TO_VA, RouteType.VESU_BORROW, RouteType.VESU_REPAY, RouteType.VA_TO_EXTENDED,
378
+ RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.VESU_MULTIPLY_INCREASE_LEVER,
379
+ RouteType.AVNU_DEPOSIT_SWAP,
380
+ RouteType.EXTENDED_DECREASE_LEVER, RouteType.EXTENDED_INCREASE_LEVER,
381
+ // Second-phase VA / Extended funding after lever routes (same types may repeat).
382
+ RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VA_TO_EXTENDED,
383
+ ],
384
+ /** @deprecated */ [CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET]: [RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
385
+ /** @deprecated */ [CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE]: [RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
386
+ /** @deprecated */ [CaseId.LTV_EXTENDED_PROFITABLE_REALIZE]: [RouteType.REALISE_PNL, RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
387
+ /** @deprecated */ [CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET]: [RouteType.VA_TO_EXTENDED, RouteType.WALLET_TO_EXTENDED],
388
+ /** @deprecated */ [CaseId.LTV_VESU_LOW_TO_EXTENDED]: [RouteType.VESU_BORROW, RouteType.VA_TO_EXTENDED],
389
+
390
+ // New Deposits
391
+ // @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
392
+ // Sequence: fund-movement transfers first (WALLET_TO_EXTENDED, VA_TO_EXTENDED, WALLET_TO_VA, EXTENDED_TO_WALLET),
393
+ // then RETURN_TO_WAIT, then optional second WALLET_TO_VA + RETURN_TO_WAIT, then lever routes.
394
+ [CaseId.DEPOSIT_FRESH_VAULT]: [
395
+ // May repeat after MANAGE_LTV (VA top-up → Extended → wait → levers).
396
+ RouteType.WALLET_TO_VA,
397
+ RouteType.VA_TO_EXTENDED,
398
+ RouteType.RETURN_TO_WAIT,
399
+ RouteType.VA_TO_EXTENDED,
400
+ RouteType.VESU_BORROW,
401
+ RouteType.WALLET_TO_EXTENDED,
402
+ RouteType.VA_TO_EXTENDED,
403
+ RouteType.WALLET_TO_VA,
404
+ RouteType.EXTENDED_TO_WALLET,
405
+ RouteType.RETURN_TO_WAIT,
406
+ RouteType.WALLET_TO_VA,
407
+ RouteType.AVNU_DEPOSIT_SWAP,
408
+ RouteType.VESU_MULTIPLY_INCREASE_LEVER,
409
+ RouteType.EXTENDED_INCREASE_LEVER,
410
+ ],
411
+ [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],
412
+ [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],
413
+ [CaseId.DEPOSIT_COMBINATION]: [], // exroutes to be computed on the fly based on above sub routes and where deposit is available
414
+ // Withdrawal
415
+ // Sequence: transfer out first (EXTENDED_TO_WALLET, RETURN_TO_WAIT, WALLET_TO_VA), then unwind lever, then BRING_LIQUIDITY
416
+ [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],
417
+
418
+ // Margin Crisis
419
+ // amounts must be chosen such that, when decresing lever, the LTV reached should be adequate, not high not low.
420
+ [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],
421
+ [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],
422
+
423
+ // Exposure Imbalance
424
+ // try to use available funds in VA/Wallet to increase lever in vesu. if not possible, reduce lever on extended for remaining
425
+ [CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS]: [RouteType.AVNU_DEPOSIT_SWAP, RouteType.VESU_MULTIPLY_INCREASE_LEVER, RouteType.EXTENDED_DECREASE_LEVER],
426
+
427
+ // no available funds in VA/Wallet to increase lever in vesu. reduce lever on extended for remaining
428
+ [CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS]: [RouteType.EXTENDED_DECREASE_LEVER],
429
+
430
+ // try to use available funds in VA/Wallet to increase lever in extended. if not possible, reduce lever on vesu for remaining
431
+ [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],
432
+
433
+ // no available funds in VA/Wallet to increase lever in extended. reduce lever on vesu for remaining
434
+ [CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS]: [RouteType.VESU_MULTIPLY_DECREASE_LEVER],
435
+ };
436
+
437
+ /** Minimum exposure imbalance fraction to flag an imbalance case */
438
+ const IMBALANCE_THRESHOLD_FRACTION = 0.0002; // 0.02%
439
+
440
+ /**
441
+ * Static case definitions derived from cases.json.
442
+ * Each entry maps a CaseId to its metadata and descriptive steps.
443
+ */
444
+ const CASE_DEFINITIONS: Record<CaseId, SolveCase> = {
445
+ [CaseId.MANAGE_LTV]: {
446
+ id: CaseId.MANAGE_LTV,
447
+ category: CaseCategory.LTV_REBALANCE,
448
+ title: 'LTV Rebalance: Unified Vesu repay + Extended margin management',
449
+ description: 'Manages both Vesu high-LTV repayment and Extended low-margin funding in a single pass.',
450
+ steps: [
451
+ 'Compute vesu repay needed and extended margin needed',
452
+ 'Allocate funds: VA > Wallet > ExtAvl > ExtUpnl for Vesu; Wallet > VA > Borrow for Extended',
453
+ 'Build combined transfer and repay/margin routes',
454
+ ],
455
+ },
456
+ [CaseId.LTV_VESU_LOW_TO_EXTENDED]: {
457
+ id: CaseId.LTV_VESU_LOW_TO_EXTENDED,
458
+ category: CaseCategory.LTV_REBALANCE,
459
+ title: 'LTV Rebalance: Vesu Low LTV → Extended (No BTC Imbalance)',
460
+ description: 'Exposures are balanced on both sides, but Vesu has low LTV (can borrow more). Extended needs additional margin.',
461
+ steps: [
462
+ 'Borrow additional USDC from Vesu (up to target LTV)',
463
+ 'Vault → Extended: Deposit borrowed USDC as margin',
464
+ ],
465
+ },
466
+ [CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE]: {
467
+ id: CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE,
468
+ category: CaseCategory.LTV_REBALANCE,
469
+ title: 'LTV Rebalance: Extended Profitable → Vesu (Funds Available to Withdraw)',
470
+ description: 'Exposures balanced. Extended is profitable with sufficient available-to-withdraw funds. Vesu needs more collateral.',
471
+ steps: [
472
+ 'Withdraw USDC from Extended to Wallet',
473
+ 'Wallet → Vault: Transfer USDC',
474
+ 'Vault → Vesu: Deposit USDC to repay',
475
+ ],
476
+ },
477
+ [CaseId.LTV_EXTENDED_PROFITABLE_REALIZE]: {
478
+ id: CaseId.LTV_EXTENDED_PROFITABLE_REALIZE,
479
+ category: CaseCategory.LTV_REBALANCE,
480
+ title: 'LTV Rebalance: Extended Profitable → Vesu (Need to Realize PnL)',
481
+ description: 'Exposures balanced. Extended is profitable but available-to-withdraw is insufficient. Unrealized PnL needs to be realized.',
482
+ steps: [
483
+ 'Partially close short BTC position on Extended (realize PnL)',
484
+ 'Immediately reopen short BTC position on Extended (maintain exposure)',
485
+ 'Withdraw realized USDC from Extended to Wallet',
486
+ 'Wallet → Vault: Transfer USDC',
487
+ 'Vault → Vesu: Deposit USDC as collateral',
488
+ ],
489
+ },
490
+ [CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET]: {
491
+ id: CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET,
492
+ category: CaseCategory.LTV_REBALANCE,
493
+ title: 'LTV Rebalance: Vesu High LTV (Use VA or Wallet)',
494
+ description: 'Vesu has high LTV (can repay debt). funds available in VA and/or Wallet can be used to repay debt.',
495
+ steps: [
496
+ 'Wallet → Vesu -> Vesu: Repay using USDC',
497
+ ],
498
+ },
499
+ [CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET]: {
500
+ id: CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET,
501
+ category: CaseCategory.LTV_REBALANCE,
502
+ title: 'LTV Rebalance: Extended High LTV (Use VA or Wallet)',
503
+ description: 'Extended has high LTV (can borrow more). funds available in VA and/or Wallet can be used to add margin',
504
+ steps: [
505
+ 'VA or Wallet → Extended -> Extended: Borrow using USDC',
506
+ ],
507
+ },
508
+ [CaseId.DEPOSIT_FRESH_VAULT]: {
509
+ id: CaseId.DEPOSIT_FRESH_VAULT,
510
+ category: CaseCategory.NEW_DEPOSITS,
511
+ title: 'New Deposits/Excess Funds: Fresh USDC in Vault',
512
+ description: 'Fresh USDC deposits received in Vault contract.',
513
+ steps: [
514
+ 'Unused funds in Vault or in Wallet',
515
+ 'Calculate allocation amounts for Extended and Vesu',
516
+ 'Vault and/or Wallet → Extended: Deposit allocated USDC',
517
+ 'Once extended receives the funds, open short BTC position',
518
+ 'Vault and/or Wallet → Vesu: Deposit allocated USDC, borrow USDC, open long BTC position',
519
+ ],
520
+ },
521
+ [CaseId.DEPOSIT_EXTENDED_AVAILABLE]: {
522
+ id: CaseId.DEPOSIT_EXTENDED_AVAILABLE,
523
+ category: CaseCategory.NEW_DEPOSITS,
524
+ title: 'New Deposits/Excess Funds: Available to Withdraw from Extended',
525
+ description: 'Extended has excess available-to-withdraw funds that can be deployed without LTV rebalancing or BTC imbalance concerns.',
526
+ steps: [
527
+ 'Calculate position increase amounts for both protocols',
528
+ '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.',
529
+ 'Once wallet receives the funds, use available funds on Extended to increase short BTC position',
530
+ 'Wallet → Vault → Vesu: Deposit USDC as collateral',
531
+ 'Borrow USDC from Vesu, open additional long BTC position',
532
+ ],
533
+ },
534
+ [CaseId.DEPOSIT_VESU_BORROW_CAPACITY]: {
535
+ id: CaseId.DEPOSIT_VESU_BORROW_CAPACITY,
536
+ category: CaseCategory.NEW_DEPOSITS,
537
+ title: 'New Deposits/Excess Funds: Available Borrowing Capacity on Vesu',
538
+ description: 'Vesu has low LTV with excess borrowing capacity that can be deployed for position expansion.',
539
+ steps: [
540
+ 'Borrow additional USDC from Vesu (within safe LTV limits)',
541
+ 'Calculate allocation for position expansion',
542
+ 'Deposit portion of funds on Extended',
543
+ 'Once extended receives the funds, open short BTC position',
544
+ 'Use portion to open additional long BTC position on Vesu',
545
+ ],
546
+ },
547
+ [CaseId.DEPOSIT_COMBINATION]: {
548
+ id: CaseId.DEPOSIT_COMBINATION,
549
+ category: CaseCategory.NEW_DEPOSITS,
550
+ title: 'New Deposits/Excess Funds: Combination case',
551
+ description: 'Combination of new deposits/excess funds on vault/extended/vesu.',
552
+ steps: [
553
+ 'Optimally distribute funds from multiple sources and create new positions. Avoid unnecessary routes.',
554
+ ],
555
+ },
556
+ [CaseId.WITHDRAWAL_SIMPLE]: {
557
+ id: CaseId.WITHDRAWAL_SIMPLE,
558
+ category: CaseCategory.WITHDRAWAL,
559
+ title: 'User Withdrawal: Simple Position Reduction',
560
+ description: 'User requests withdrawal with sufficient available funds to close positions proportionally.',
561
+ steps: [
562
+ 'First check if any available funds can be used to settle the withdrawal (vault, available to withdraw or borrowing from vesu)',
563
+ 'If yes, do necessary transfers such that funds are available in vault',
564
+ 'If not or if more required, calculate position sizes to close on both sides',
565
+ 'Close long BTC position on Vesu (partially or fully)',
566
+ 'Repay borrowed USDC on Vesu',
567
+ 'Withdraw collateral from Vesu to Vault',
568
+ 'Close short BTC position on Extended',
569
+ 'Withdraw margin from Extended to Wallet',
570
+ 'Wallet → Vault: Transfer USDC',
571
+ 'Vault → User: Process withdrawal',
572
+ ],
573
+ },
574
+ [CaseId.MARGIN_CRISIS_EXTENDED]: {
575
+ id: CaseId.MARGIN_CRISIS_EXTENDED,
576
+ category: CaseCategory.MARGIN_CRISIS,
577
+ title: 'Margin Crisis: LTV rebalance requires more funds than available, margin required on Extended',
578
+ description: 'LTV rebalance requires more funds than available on vault/extended/vesu. Extended needs margin urgently.',
579
+ steps: [
580
+ 'Calculate required position reduction in vesu and extended',
581
+ 'Borrow more on vesu such that HF can go max up to 1.2',
582
+ 'Use the borrowed funds to settle margin requirements on Extended (vault to extended)',
583
+ 'Once funds reach on extended, close portion of long BTC position on Vesu',
584
+ 'Convert BTC to USDC',
585
+ 'Repay portion of borrowed USDC on Vesu such that HF can go back to target',
586
+ 'Close corresponding short BTC position on Extended (match reduced exposure)',
587
+ ],
588
+ },
589
+ [CaseId.MARGIN_CRISIS_VESU]: {
590
+ id: CaseId.MARGIN_CRISIS_VESU,
591
+ category: CaseCategory.MARGIN_CRISIS,
592
+ title: 'Margin Crisis: LTV rebalance requires more funds than available, margin required on Vesu',
593
+ description: 'LTV rebalance requires more funds than available on vault/extended/vesu. Vesu needs margin urgently.',
594
+ steps: [
595
+ 'Calculate required position reduction in extended',
596
+ 'Update leverage to 4x and try to use available to withdraw funds on extended',
597
+ 'Wait until funds reach on wallet',
598
+ 'Close portion of short BTC position on Extended',
599
+ 'Move wallet to vault and use the USDC to repay on vesu',
600
+ 'Reduce long position to match the short position on extended',
601
+ ],
602
+ },
603
+ [CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS]: {
604
+ id: CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS,
605
+ category: CaseCategory.EXPOSURE_IMBALANCE,
606
+ title: 'BTC Imbalance: Extended Has Excess Short (No funds to match)',
607
+ description: 'Extended has more short BTC exposure than Vesu long. No funds available to increase Vesu long.',
608
+ steps: [
609
+ 'Reduce short BTC position on Extended to match the long position on Vesu',
610
+ ],
611
+ },
612
+ [CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS]: {
613
+ id: CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS,
614
+ category: CaseCategory.EXPOSURE_IMBALANCE,
615
+ title: 'BTC Imbalance: Extended Has Excess Short (Vault/Wallet Has Funds)',
616
+ description: 'Extended has more short BTC exposure than Vesu long. Use available funds to increase long on Vesu.',
617
+ steps: [
618
+ 'Use available funds to increase long on Vesu as much as possible',
619
+ 'If still not matched, reduce short BTC position on Extended to match the long position on Vesu',
620
+ ],
621
+ },
622
+ [CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS]: {
623
+ id: CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS,
624
+ category: CaseCategory.EXPOSURE_IMBALANCE,
625
+ title: 'BTC Imbalance: Vesu Has Excess Long (No funds to match)',
626
+ description: 'Vesu has more long BTC exposure than Extended short. Extended does not have enough available to trade funds.',
627
+ steps: [
628
+ 'Reduce long BTC position on Vesu to match the short position on Extended',
629
+ ],
630
+ },
631
+ [CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS]: {
632
+ id: CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS,
633
+ category: CaseCategory.EXPOSURE_IMBALANCE,
634
+ title: 'BTC Imbalance: Vesu Has Excess Long (Extended has funds)',
635
+ description: 'Vesu has more long BTC exposure than Extended short. Extended has available to trade funds to match.',
636
+ steps: [
637
+ 'Use available funds to increase short on Extended as much as possible',
638
+ 'If still not matched, reduce long BTC position on Vesu to match the short position on Extended',
639
+ ],
640
+ },
641
+ };
642
+
643
+ /**
644
+ * Complete output from the solver describing all needed state changes.
645
+ *
646
+ * - cases: detected cases describing the system state and needed actions.
647
+ * - extendedDeposit: positive = deposit USDC into Extended, negative = withdraw.
648
+ * - extendedPositionDeltas: per-instrument exposure changes (with instrument id).
649
+ * - vesuDeltas: per-pool debt & collateral changes (with pool id and token pair).
650
+ * - vesuAllocationUsd: USDC directed to Vesu side (negative = unwind from Vesu).
651
+ * - extendedAllocationUsd: USDC directed to Extended side (negative = withdraw from Extended).
652
+ * - bringLiquidityAmount: amount VA should send via bringLiquidity to the vault
653
+ * (= withdrawAmount input; 0 during investment cycles).
654
+ */
655
+ export interface SolveResult {
656
+ /** Detected cases describing the current situation, amounts, and per-case routes */
657
+ cases: SolveCaseEntry[];
658
+ extendedDeposit: Web3Number;
659
+ extendedPositionDeltas: ExtendedPositionDelta[];
660
+ vesuDepositAmount: Web3Number;
661
+ vesuDeltas: VesuPoolDelta[];
662
+ vesuAllocationUsd: Web3Number;
663
+ extendedAllocationUsd: Web3Number;
664
+ bringLiquidityAmount: Web3Number;
665
+ /**
666
+ * Net pending deposit for Extended.
667
+ * Positive = deposit in transit (wallet → Extended), negative = withdrawal in transit.
668
+ * Used by ExecutionService to avoid double-sending transfers.
669
+ */
670
+ pendingDeposit: Web3Number;
671
+ }
672
+
673
+ // ─── Config ────────────────────────────────────────────────────────────────────
674
+
675
+ export interface StateManagerConfig {
676
+ pricer: PricerBase;
677
+ networkConfig: IConfig;
678
+ vesuAdapters: VesuMultiplyAdapter[];
679
+ extendedAdapter: ExtendedAdapter;
680
+ vaultAllocator: ContractAddr;
681
+ walletAddress: string;
682
+ /** Strategy / vault token — idle balance in the vault allocator is denominated in this asset */
683
+ assetToken: TokenInfo;
684
+ /**
685
+ * Native USDC (Starknet) — operator wallet stablecoin balance is always read for this token,
686
+ * independent of {@link assetToken} (e.g. when the strategy vault uses another asset).
687
+ */
688
+ usdcToken: TokenInfo;
689
+ /** Collateral token (e.g. WBTC) for wallet balance checks */
690
+ collateralToken: TokenInfo;
691
+ limitBalanceBufferFactor: number;
692
+ }
693
+
694
+ // ─── ExecutionRoute helpers ──────────────────────────────────────────────────────────────
695
+
696
+ /**
697
+ * Returns a short human-readable summary of a route's key fields for logging.
698
+ */
699
+ export function routeSummary(r: ExecutionRoute): string {
700
+ switch (r.type) {
701
+ case RouteType.WALLET_TO_EXTENDED:
702
+ case RouteType.VA_TO_EXTENDED:
703
+ case RouteType.EXTENDED_TO_WALLET:
704
+ case RouteType.WALLET_TO_VA:
705
+ return `${r.amount.toNumber()}`;
706
+ case RouteType.AVNU_DEPOSIT_SWAP:
707
+ case RouteType.AVNU_WITHDRAW_SWAP:
708
+ return `${r.fromAmount.toNumber()} ${r.fromToken}→${r.toToken}`;
709
+ case RouteType.VESU_MULTIPLY_INCREASE_LEVER:
710
+ case RouteType.VESU_MULTIPLY_DECREASE_LEVER:
711
+ 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()}`;
712
+ case RouteType.VESU_BORROW:
713
+ case RouteType.VESU_REPAY:
714
+ return `${r.amount.toNumber()} ${r.debtToken.symbol} pool=${r.poolId.shortString()} [Collateral: ${r.collateralToken.symbol}]`;
715
+ case RouteType.REALISE_PNL:
716
+ return `${r.amount.toNumber()} ${r.instrument}`;
717
+ case RouteType.EXTENDED_INCREASE_LEVER:
718
+ case RouteType.EXTENDED_DECREASE_LEVER:
719
+ return `${r.amount.toNumber()} ${r.instrument}`;
720
+ case RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE:
721
+ case RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE:
722
+ return '(no args)';
723
+ case RouteType.CRISIS_BORROW_BEYOND_TARGET_HF:
724
+ return `${r.amount.toNumber()} ${r.token} pool=${r.poolId.shortString()}`;
725
+ case RouteType.BRING_LIQUIDITY:
726
+ return `${r.amount.toNumber()}`;
727
+ case RouteType.RETURN_TO_WAIT:
728
+ return '(control)';
729
+ default:
730
+ return '';
731
+ }
732
+ }
733
+
734
+ // ─── Solve Budget ───────────────────────────────────────────────────────────────
735
+
736
+ /**
737
+ * Single source of truth for all mutable state during a solve() call.
738
+ *
739
+ * Stores **raw** snapshots from refresh (no safety buffer applied in fields).
740
+ * Buffer {@link limitBalanceBufferFactor} is applied only in **getters** used
741
+ * during solve (caps, diagnostics). **Spend / add** methods mutate balances
742
+ * in **raw** USD only (no buffer on debits or credits).
743
+ *
744
+ * **Extended deposits** ({@link SolveBudget.addToExtAvailTrade}): while account equity is
745
+ * below required margin (Σ position value ÷ Extended leverage), incoming USDC is credited
746
+ * only to **balance** and **equity**. Only the excess is credited to **availableForWithdrawal**
747
+ * and **availableForTrade**, matching “margin first, then free collateral”.
748
+ */
749
+ export class SolveBudget {
750
+ // ── Refreshed state (mutable during solve, raw on-chain / API values) ─
751
+ private unusedBalance: TokenBalance[];
752
+ private walletBalance: TokenBalance | null;
753
+ /**
754
+ * Idle non-stable {@link StateManagerConfig.assetToken} in the vault allocator.
755
+ * Null when asset and USDC are the same token (VA idle stablecoin is only in {@link vaultUsdcBalance}).
756
+ */
757
+ private vaultAssetBalance: TokenBalance | null;
758
+ /** Idle {@link StateManagerConfig.usdcToken} in the vault allocator (including when it is also the strategy asset). */
759
+ private vaultUsdcBalance: TokenBalance | null;
760
+ private extendedPositions: ExtendedPositionState[];
761
+ private extendedBalance: ExtendedBalanceState | null;
762
+ private vesuPoolStates: VesuPoolState[];
763
+ private vesuPerPoolDebtDeltasToBorrow: Web3Number[];
764
+ private shouldVesuRebalance: boolean[]; // should be same length as vesuPerPoolDebtDeltasToBorrow
765
+
766
+ readonly assetToken: TokenInfo;
767
+ readonly usdcToken: TokenInfo;
768
+
769
+ constructor(state: {
770
+ assetToken: TokenInfo;
771
+ usdcToken: TokenInfo;
772
+ unusedBalance: TokenBalance[];
773
+ walletBalance: TokenBalance | null;
774
+ vaultAssetBalance: TokenBalance | null;
775
+ vaultUsdcBalance: TokenBalance | null;
776
+ extendedPositions: ExtendedPositionState[];
777
+ extendedBalance: ExtendedBalanceState | null;
778
+ vesuPoolStates: VesuPoolState[];
779
+ }) {
780
+ this.assetToken = state.assetToken;
781
+ this.usdcToken = state.usdcToken;
782
+
783
+ const cloneTb = (b: TokenBalance): TokenBalance => ({
784
+ token: b.token,
785
+ amount: new Web3Number(b.amount.toFixed(b.token.decimals), b.token.decimals),
786
+ usdValue: b.usdValue,
787
+ });
788
+
789
+ this.unusedBalance = state.unusedBalance.map((item) => cloneTb(item));
790
+ this.walletBalance = state.walletBalance ? cloneTb(state.walletBalance) : null;
791
+ this.vaultAssetBalance = state.vaultAssetBalance ? cloneTb(state.vaultAssetBalance) : null;
792
+ this.vaultUsdcBalance = state.vaultUsdcBalance ? cloneTb(state.vaultUsdcBalance) : null;
793
+
794
+ // Deep-clone mutable Extended / Vesu snapshots so route-building dry-runs (e.g. _classifyLTV)
795
+ // cannot mutate shared objects when callers reuse the same fixture for multiple budgets.
796
+ this.extendedPositions = state.extendedPositions.map((p) => ({
797
+ ...p,
798
+ size: new Web3Number(p.size.toFixed(8), 8),
799
+ valueUsd: new Web3Number(p.valueUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS),
800
+ }));
801
+ this.extendedBalance = state.extendedBalance
802
+ ? {
803
+ equity: new Web3Number(
804
+ state.extendedBalance.equity.toFixed(USDC_TOKEN_DECIMALS),
805
+ USDC_TOKEN_DECIMALS,
806
+ ),
807
+ availableForTrade: new Web3Number(
808
+ state.extendedBalance.availableForTrade.toFixed(USDC_TOKEN_DECIMALS),
809
+ USDC_TOKEN_DECIMALS,
810
+ ),
811
+ availableForWithdrawal: new Web3Number(
812
+ state.extendedBalance.availableForWithdrawal.toFixed(USDC_TOKEN_DECIMALS),
813
+ USDC_TOKEN_DECIMALS,
814
+ ),
815
+ unrealisedPnl: new Web3Number(
816
+ state.extendedBalance.unrealisedPnl.toFixed(USDC_TOKEN_DECIMALS),
817
+ USDC_TOKEN_DECIMALS,
818
+ ),
819
+ balance: new Web3Number(
820
+ state.extendedBalance.balance.toFixed(USDC_TOKEN_DECIMALS),
821
+ USDC_TOKEN_DECIMALS,
822
+ ),
823
+ pendingDeposit: new Web3Number(
824
+ state.extendedBalance.pendingDeposit.toFixed(USDC_TOKEN_DECIMALS),
825
+ USDC_TOKEN_DECIMALS,
826
+ ),
827
+ }
828
+ : null;
829
+ this.vesuPoolStates = state.vesuPoolStates.map((p) => ({
830
+ ...p,
831
+ collateralAmount: new Web3Number(
832
+ p.collateralAmount.toFixed(p.collateralToken.decimals),
833
+ p.collateralToken.decimals,
834
+ ),
835
+ debtAmount: new Web3Number(
836
+ p.debtAmount.toFixed(p.debtToken.decimals),
837
+ p.debtToken.decimals,
838
+ ),
839
+ }));
840
+ const vesuPerPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
841
+ assert(vesuPerPoolDebtDeltasToBorrow.length === this.vesuPoolStates.length, 'vesuPerPoolDebtDeltasToBorrow length must match vesuPoolStates length');
842
+ this.vesuPerPoolDebtDeltasToBorrow = vesuPerPoolDebtDeltasToBorrow.map((item) => item.deltaDebt);
843
+ this.shouldVesuRebalance = vesuPerPoolDebtDeltasToBorrow.map((item) => item.shouldRebalance);
844
+ }
845
+
846
+ /** `1 - limitBalanceBufferFactor` — multiplier applied to raw notionals for “usable” USD. */
847
+ private _usableFraction(): number {
848
+ return 1;
849
+ }
850
+
851
+ /**
852
+ * Raw USD notional for a token row. USDC (and configured {@link usdcToken}) uses 1:1 from amount;
853
+ * non-stable assets (e.g. WBTC in VA) use {@link TokenBalance.usdValue} from the pricer at refresh.
854
+ */
855
+ private _rawTokenUsd(tb: TokenBalance | null | undefined): number {
856
+ if (!tb) return 0;
857
+ if (this.usdcToken.address.eq(tb.token.address)) {
858
+ return Number(tb.amount.toFixed(tb.token.decimals));
859
+ }
860
+ return tb.usdValue;
861
+ }
862
+
863
+ /** Apply safety buffer to a raw USD scalar. */
864
+ bufferedUsd(rawUsd: number): number {
865
+ return rawUsd * this._usableFraction();
866
+ }
867
+
868
+ /** Convert a buffered “usable” USD amount to raw nominal USD (inverse of {@link bufferedUsd}). */
869
+ rawUsdFromBuffered(bufferedUsd: number): number {
870
+ const bf = this._usableFraction();
871
+ assert(bf > 0, 'SolveBudget::rawUsdFromBuffered usable fraction must be positive');
872
+ return bufferedUsd / bf;
873
+ }
874
+
875
+ /** Buffered USD notional for one token balance row. */
876
+ bufferedTokenUsd(tb: TokenBalance | null | undefined): number {
877
+ return this.bufferedUsd(this._rawTokenUsd(tb));
878
+ }
879
+
880
+ logStateSummary() {
881
+ console.log("===== state summary =====");
882
+ const aggregatedData = {
883
+ unusedBalances: this.unusedBalance.map((b) => ({
884
+ token: b.token.symbol,
885
+ amount: b.amount.toNumber(),
886
+ })),
887
+ walletBalance: this.walletBalance
888
+ ? {
889
+ token: this.walletBalance.token.symbol,
890
+ amount: this.walletBalance.amount.toNumber(),
891
+ }
892
+ : undefined,
893
+ vaultAssetBalance: this.vaultAssetBalance
894
+ ? {
895
+ token: this.vaultAssetBalance.token.symbol,
896
+ amount: this.vaultAssetBalance.amount.toNumber(),
897
+ }
898
+ : undefined,
899
+ vaultUsdcBalance: this.vaultUsdcBalance
900
+ ? {
901
+ token: this.vaultUsdcBalance.token.symbol,
902
+ amount: this.vaultUsdcBalance.amount.toNumber(),
903
+ }
904
+ : undefined,
905
+ vesuPoolStates: this.vesuPoolStates.map((p) => ({
906
+ poolId: p.poolId,
907
+ collateralAmount: p.collateralAmount.toNumber(),
908
+ debtAmount: p.debtAmount.toNumber(),
909
+ })),
910
+ vesuBorrowCapacity: this.vesuBorrowCapacity,
911
+ vesuRebalance: this.shouldVesuRebalance,
912
+ vesuPerPoolDebtDeltasToBorrow: this.vesuPerPoolDebtDeltasToBorrow.map((d) => d.toNumber()),
913
+ extendedBalance: this.extendedBalance?.balance.toNumber(),
914
+ extendedEquity: this.extendedBalance?.equity.toNumber(),
915
+ extendedAvailableForTrade: this.extendedBalance?.availableForTrade.toNumber(),
916
+ extendedAvailableForWithdrawal: this.extendedBalance?.availableForWithdrawal.toNumber(),
917
+ extendedUnrealisedPnl: this.extendedBalance?.unrealisedPnl.toNumber(),
918
+ extendedPendingDeposit: this.extendedBalance?.pendingDeposit.toNumber(),
919
+ extendedPositions: this.extendedPositions.map((p) => ({
920
+ instrument: p.instrument,
921
+ size: p.size.toNumber(),
922
+ valueUsd: p.valueUsd.toNumber(),
923
+ })),
924
+ }
925
+ // unused balances
926
+ console.log(
927
+ "unused balances",
928
+ aggregatedData.unusedBalances
929
+ .map((b) => `${b.token}=${b.amount}`)
930
+ .join(", "),
931
+ );
932
+ console.log(
933
+ "wallet balance",
934
+ aggregatedData.walletBalance
935
+ ? `${aggregatedData.walletBalance.token}=${aggregatedData.walletBalance.amount}`
936
+ : undefined,
937
+ );
938
+ console.log(
939
+ "vault asset balance",
940
+ aggregatedData.vaultAssetBalance
941
+ ? `${aggregatedData.vaultAssetBalance.token}=${aggregatedData.vaultAssetBalance.amount}`
942
+ : undefined,
943
+ );
944
+ console.log(
945
+ "vault usdc balance",
946
+ aggregatedData.vaultUsdcBalance
947
+ ? `${aggregatedData.vaultUsdcBalance.token}=${aggregatedData.vaultUsdcBalance.amount}`
948
+ : undefined,
949
+ );
950
+
951
+ // vesu info
952
+ console.log(
953
+ "vesu pool states",
954
+ aggregatedData.vesuPoolStates
955
+ .map(
956
+ (p) =>
957
+ `${p.poolId.shortString()}=${p.collateralAmount} ${p.debtAmount}`,
958
+ )
959
+ .join(", "),
960
+ );
961
+ console.log("vesu borrow capacity", aggregatedData.vesuBorrowCapacity);
962
+ console.log(
963
+ "vesu rebalance",
964
+ aggregatedData.vesuRebalance.map(String).join(", "),
965
+ );
966
+ console.log("vesu per pool debt deltas to borrow", aggregatedData.vesuPerPoolDebtDeltasToBorrow.join(", "));
967
+ // extended info
968
+ console.log("extended balance", aggregatedData.extendedBalance);
969
+ console.log("extended equity", aggregatedData.extendedEquity);
970
+ console.log("extended available for trade", aggregatedData.extendedAvailableForTrade);
971
+ console.log("extended available for withdrawal", aggregatedData.extendedAvailableForWithdrawal);
972
+ console.log("extended unrealised pnl", aggregatedData.extendedUnrealisedPnl);
973
+ console.log("extended pending deposit", aggregatedData.extendedPendingDeposit);
974
+ console.log(
975
+ "extended positions",
976
+ aggregatedData.extendedPositions
977
+ .map(
978
+ (p) => `${p.instrument}=${p.size} ${p.valueUsd}`,
979
+ )
980
+ .join(", "),
981
+ );
982
+
983
+ return aggregatedData
984
+ }
985
+
986
+ /**
987
+ * Initialise derived views for a solve cycle. Mutates only when pending
988
+ * withdrawal from Extended is in transit (credits wallet raw balance).
989
+ */
990
+ initBudget(): void {
991
+ const pendingDeposit = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
992
+ if (pendingDeposit < 0) {
993
+ const inTransit = Math.abs(pendingDeposit);
994
+ if (this.walletBalance) {
995
+ this._addUsdToTokenBalance(this.walletBalance, inTransit);
996
+ }
997
+ logger.debug(`SolveBudget::initBudget pendingDeposit=${pendingDeposit} -> increased wallet raw USD by ${inTransit}`);
998
+ }
999
+
1000
+ this._recomputeUnusedBalance();
1001
+ }
1002
+
1003
+ /**
1004
+ * Apply a safety buffer to all liquid balances (VA, wallet, extended trade/withdraw/upnl,
1005
+ * unused balances). Call after withdrawal classification but before LTV/deposit classifiers
1006
+ * so that withdrawal uses full raw amounts while subsequent classifiers see buffered values.
1007
+ */
1008
+ applyBuffer(factor: number): void {
1009
+ if (factor <= 0 || factor >= 1) return;
1010
+ const mult = 1 - factor;
1011
+
1012
+ const scaleTokenBalance = (tb: TokenBalance | null) => {
1013
+ if (!tb) return;
1014
+ const newAmount = tb.amount.multipliedBy(mult);
1015
+ tb.amount = new Web3Number(newAmount.toFixed(tb.token.decimals), tb.token.decimals);
1016
+ tb.usdValue = tb.usdValue * mult;
1017
+ };
1018
+
1019
+ scaleTokenBalance(this.vaultAssetBalance);
1020
+ scaleTokenBalance(this.vaultUsdcBalance);
1021
+ scaleTokenBalance(this.walletBalance);
1022
+ for (const ub of this.unusedBalance) {
1023
+ scaleTokenBalance(ub);
1024
+ }
1025
+
1026
+ if (this.extendedBalance) {
1027
+ this.extendedBalance.availableForTrade = this.extendedBalance.availableForTrade.multipliedBy(mult);
1028
+ this.extendedBalance.availableForWithdrawal = this.extendedBalance.availableForWithdrawal.multipliedBy(mult);
1029
+ this.extendedBalance.unrealisedPnl = this.extendedBalance.unrealisedPnl.multipliedBy(mult);
1030
+ }
1031
+
1032
+ this._recomputeUnusedBalance();
1033
+ }
1034
+
1035
+ get vesuPoolState(): VesuPoolState {
1036
+ assert(this.vesuPoolStates.length === 1, 'SolveBudget::vesuPoolState: vesuPoolStates length must be 1');
1037
+ return this.vesuPoolStates[0];
1038
+ }
1039
+
1040
+ // ── Derived getters (buffered where applicable) ─────────────────────
1041
+
1042
+ /** Buffered VA USD: non-stable asset slot (if any) + USDC slot. */
1043
+ get vaUsd(): number {
1044
+ // return this.bufferedTokenUsd(this.vaultAssetBalance) + this.bufferedTokenUsd(this.vaultUsdcBalance);
1045
+ return this.bufferedTokenUsd(this.vaultUsdcBalance); // we dont have swap routes and is not correct, to consider asset balance directlyt in strategy
1046
+ }
1047
+
1048
+ /** Buffered USD in VA strategy-asset bucket only. */
1049
+ get vaAssetUsd(): number {
1050
+ return this.bufferedTokenUsd(this.vaultAssetBalance);
1051
+ }
1052
+
1053
+ /** Buffered USD in VA USDC bucket (includes full VA idle when asset === USDC). */
1054
+ get vaUsdcUsd(): number {
1055
+ return this.bufferedTokenUsd(this.vaultUsdcBalance);
1056
+ }
1057
+
1058
+ get walletUsd(): number {
1059
+ return this.bufferedUsd(this._rawTokenUsd(this.walletBalance));
1060
+ }
1061
+
1062
+ get vaWalletUsd(): number {
1063
+ // va buffered + wallet raw
1064
+ return this.vaUsd + this.walletUsd;
1065
+ }
1066
+
1067
+ get extAvailWithdraw(): number {
1068
+ const raw = this.extendedBalance?.availableForWithdrawal?.toNumber() ?? 0;
1069
+ return this.bufferedUsd(raw);
1070
+ }
1071
+
1072
+ get extAvailUpnl(): number {
1073
+ const raw = this.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
1074
+ return this.bufferedUsd(raw);
1075
+ }
1076
+
1077
+ /**
1078
+ * Buffered Extended available-for-trade plus positive {@link ExtendedBalanceState.pendingDeposit}
1079
+ * (deposit in transit is usable the same way as the pre-buffer implementation).
1080
+ */
1081
+ get extAvailTrade(): number {
1082
+ const raw = this.extendedBalance?.availableForTrade?.toNumber() ?? 0;
1083
+ let v = this.bufferedUsd(raw);
1084
+ const pd = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
1085
+ if (pd > 0) v += pd;
1086
+ return v;
1087
+ }
1088
+
1089
+ get extPendingDeposit(): number {
1090
+ return this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
1091
+ }
1092
+
1093
+ /**
1094
+ * Aggregate positive per-pool borrow headroom (USD). Repay/borrow routes update
1095
+ * {@link vesuPerPoolDebtDeltasToBorrow}; no separate counter.
1096
+ */
1097
+ get vesuBorrowCapacity(): number {
1098
+ return this.vesuPerPoolDebtDeltasToBorrow.reduce(
1099
+ (a, d) => a + Math.max(0, d.toNumber()),
1100
+ 0,
1101
+ );
1102
+ }
1103
+
1104
+ /** Diagnostic: buffered idle + positive debt delta + buffered Extended afT + in-flight deposit. */
1105
+ get totalUnused(): number {
1106
+ const debtSum = this.vesuPerPoolDebtDeltasToBorrow.reduce((a, b) => a + b.toNumber(), 0);
1107
+ let u = this.unusedBalance.reduce((a, b) => a + this.bufferedTokenUsd(b), 0);
1108
+ if (debtSum > 0) u += debtSum;
1109
+ const rawAft = this.extendedBalance?.availableForTrade?.toNumber() ?? 0;
1110
+ const aftBuf = this.bufferedUsd(rawAft);
1111
+ if (aftBuf > 0) u += aftBuf;
1112
+ const pd = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
1113
+ if (pd > 0) u += pd;
1114
+ return u;
1115
+ }
1116
+
1117
+ /** Sum of buffered USD across merged unused-balance rows (VA + wallet). */
1118
+ get unusedBalancesBufferedUsdSum(): number {
1119
+ return this.unusedBalance.reduce((a, b) => a + this.bufferedTokenUsd(b), 0);
1120
+ }
1121
+
1122
+ /** Read-only snapshot view for validation / logging. */
1123
+ get unusedBalanceRows(): readonly TokenBalance[] {
1124
+ return this.unusedBalance;
1125
+ }
1126
+
1127
+ /** Read-only Vesu pool view for solve computations. */
1128
+ get vesuPools(): readonly VesuPoolState[] {
1129
+ return this.vesuPoolStates;
1130
+ }
1131
+
1132
+ /** Read-only Extended positions view for solve computations. */
1133
+ get extendedPositionsView(): readonly ExtendedPositionState[] {
1134
+ return this.extendedPositions;
1135
+ }
1136
+
1137
+ /** Read-only Extended balance view for diagnostics / margin checks. */
1138
+ get extendedBalanceView(): ExtendedBalanceState | null {
1139
+ return this.extendedBalance;
1140
+ }
1141
+
1142
+ /** Current debt deltas per pool (positive=borrow, negative=repay). */
1143
+ get vesuDebtDeltas(): readonly Web3Number[] {
1144
+ return this.vesuPerPoolDebtDeltasToBorrow;
1145
+ }
1146
+
1147
+ /** Per-pool rebalance flags derived from target HF checks. */
1148
+ get vesuRebalanceFlags(): readonly boolean[] {
1149
+ return this.shouldVesuRebalance;
1150
+ }
1151
+
1152
+ /** Raw USD in VA (USDC slot + non-stable asset slot when distinct); spend caps when executing transfers. */
1153
+ private _vaRawUsd(): number {
1154
+ // return this._rawTokenUsd(this.vaultUsdcBalance) + this._rawTokenUsd(this.vaultAssetBalance);
1155
+ return this._rawTokenUsd(this.vaultUsdcBalance); // we dont have swap routes and is not correct, to consider asset balance directlyt in strategy
1156
+ }
1157
+
1158
+ /** VA liquidity usable for repay / {@link spendVaRawUsd} (matches nominal balances after any {@link applyBuffer} scaling). */
1159
+ get vaRawUsd(): number {
1160
+ return this._vaRawUsd();
1161
+ }
1162
+
1163
+ private _walletRawUsd(): number {
1164
+ return this._rawTokenUsd(this.walletBalance);
1165
+ }
1166
+
1167
+ // ── Token snapshot helpers (keep vault / wallet / unusedBalance aligned) ─
1168
+
1169
+ /** Remove up to `usd` notional from a token balance, scaling token amount proportionally. */
1170
+ private _deductUsdFromTokenBalance(tb: TokenBalance, usd: number): void {
1171
+ if (usd <= 0) return;
1172
+ const take = Math.min(usd, tb.usdValue);
1173
+ if (take <= 0) return;
1174
+ const oldUsd = tb.usdValue;
1175
+ const newUsd = Math.max(0, oldUsd - take);
1176
+ tb.usdValue = newUsd;
1177
+ if (oldUsd <= 0) return;
1178
+ const ratio = newUsd / oldUsd;
1179
+ tb.amount = new Web3Number(
1180
+ (tb.amount.toNumber() * ratio).toFixed(tb.token.decimals),
1181
+ tb.token.decimals,
1182
+ );
1183
+ }
1184
+
1185
+ /** Add USD notional; infers price from current amount/usd when possible, else 1:1. */
1186
+ private _addUsdToTokenBalance(tb: TokenBalance, usd: number): void {
1187
+ if (usd <= 0) return;
1188
+ const amtNum = tb.amount.toNumber();
1189
+ const price =
1190
+ amtNum > 0 && tb.usdValue > 0 ? tb.usdValue / amtNum : 1;
1191
+ const deltaTok = usd / price;
1192
+ tb.usdValue += usd;
1193
+ tb.amount = tb.amount.plus(
1194
+ new Web3Number(deltaTok.toFixed(tb.token.decimals), tb.token.decimals),
1195
+ );
1196
+ }
1197
+
1198
+ /**
1199
+ * Rebuilds {@link unusedBalance} from vault + wallet snapshots (same merge as refresh).
1200
+ */
1201
+ private _recomputeUnusedBalance(): void {
1202
+ const balanceMap = new Map<string, TokenBalance>();
1203
+
1204
+ const merge = (b: TokenBalance | null) => {
1205
+ if (!b) return;
1206
+ const key = b.token.address.toString();
1207
+ const row: TokenBalance = {
1208
+ token: b.token,
1209
+ amount: new Web3Number(b.amount.toFixed(b.token.decimals), b.token.decimals),
1210
+ usdValue: b.usdValue,
1211
+ };
1212
+ const existing = balanceMap.get(key);
1213
+ if (existing) {
1214
+ existing.amount = new Web3Number(
1215
+ existing.amount.plus(row.amount).toFixed(existing.token.decimals),
1216
+ existing.token.decimals,
1217
+ );
1218
+ existing.usdValue += row.usdValue;
1219
+ } else {
1220
+ balanceMap.set(key, row);
1221
+ }
1222
+ };
1223
+
1224
+ merge(this.vaultAssetBalance);
1225
+ merge(this.vaultUsdcBalance);
1226
+ merge(this.walletBalance);
1227
+
1228
+ this.unusedBalance = Array.from(balanceMap.values());
1229
+ }
1230
+
1231
+ // ── Spend methods (return amount consumed, auto-decrement totalUnused) ─
1232
+
1233
+ /**
1234
+ * Spend VA **raw** USD (up to {@link vaRawUsd}). Prefer {@link vaultUsdcBalance} when present, then {@link vaultAssetBalance}.
1235
+ */
1236
+ spendVA(rawDesired: number): number {
1237
+ const capRaw = this._vaRawUsd();
1238
+ const usedRaw = Math.min(capRaw, Math.max(0, rawDesired));
1239
+ if (usedRaw <= CASE_THRESHOLD_USD) return 0;
1240
+ let rem = usedRaw;
1241
+ if (rem > 0 && this.vaultUsdcBalance && this.vaultUsdcBalance.usdValue > 0) {
1242
+ const fromUsdc = Math.min(rem, this.vaultUsdcBalance.usdValue);
1243
+ this._deductUsdFromTokenBalance(this.vaultUsdcBalance, fromUsdc);
1244
+ rem -= fromUsdc;
1245
+ }
1246
+ if (rem > 0 && this.vaultAssetBalance) {
1247
+ // this._deductUsdFromTokenBalance(this.vaultAssetBalance, rem);
1248
+ throw new Error(`Not implemented: spendVA with vaultAssetBalance`);
1249
+ }
1250
+ this._recomputeUnusedBalance();
1251
+ logger.debug(`SolveBudget::spendVA usedRaw=${usedRaw}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
1252
+ return usedRaw;
1253
+ }
1254
+
1255
+ /**
1256
+ * Spend nominal/raw USD from VA (e.g. Vesu repay, on-chain USDC). Does not apply the safety buffer to the cap.
1257
+ */
1258
+ spendVaRawUsd(rawUsdDesired: number): number {
1259
+ const capRaw =
1260
+ // this._rawTokenUsd(this.vaultUsdcBalance) + this._rawTokenUsd(this.vaultAssetBalance);
1261
+ this._rawTokenUsd(this.vaultUsdcBalance); // we dont have swap routes and is not correct, to consider asset balance directlyt in strategy
1262
+ const usedRaw = Math.min(capRaw, Math.max(0, rawUsdDesired));
1263
+ if (usedRaw <= CASE_THRESHOLD_USD) return 0;
1264
+ let rem = usedRaw;
1265
+ if (rem > 0 && this.vaultUsdcBalance && this.vaultUsdcBalance.usdValue > 0) {
1266
+ const fromUsdc = Math.min(rem, this.vaultUsdcBalance.usdValue);
1267
+ this._deductUsdFromTokenBalance(this.vaultUsdcBalance, fromUsdc);
1268
+ rem -= fromUsdc;
1269
+ }
1270
+ if (rem > 0 && this.vaultAssetBalance) {
1271
+ // this._deductUsdFromTokenBalance(this.vaultAssetBalance, rem);
1272
+ throw new Error(`Not implemented: spendVaRawUsd with vaultAssetBalance`);
1273
+ }
1274
+ this._recomputeUnusedBalance();
1275
+ logger.debug(`SolveBudget::spendVaRawUsd usedRaw=${usedRaw}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
1276
+ return usedRaw;
1277
+ }
1278
+
1279
+ /**
1280
+ * Add **raw nominal USD** to VA (borrow proceeds, wallet→VA in raw USDC, etc.).
1281
+ */
1282
+ addToVA(rawUsd: number): void {
1283
+ assert(rawUsd >= 0, 'SolveBudget::addToVA amount must be positive');
1284
+ if (rawUsd === 0) return;
1285
+ if (this.vaultUsdcBalance) {
1286
+ this._addUsdToTokenBalance(this.vaultUsdcBalance, rawUsd);
1287
+ } else if (this.vaultAssetBalance) {
1288
+ // this._addUsdToTokenBalance(this.vaultAssetBalance, rawUsd);
1289
+ throw new Error(`Not implemented: addToVA with vaultAssetBalance`);
1290
+ }
1291
+ this._recomputeUnusedBalance();
1292
+ logger.debug(`SolveBudget::addToVA rawUsd=${rawUsd}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
1293
+ }
1294
+
1295
+ spendWallet(rawDesired: number): number {
1296
+ const capRaw = this._walletRawUsd();
1297
+ const usedRaw = Math.min(capRaw, Math.max(0, rawDesired));
1298
+ if (usedRaw <= CASE_THRESHOLD_USD) return 0;
1299
+ if (this.walletBalance) {
1300
+ this._deductUsdFromTokenBalance(this.walletBalance, usedRaw);
1301
+ }
1302
+ this._recomputeUnusedBalance();
1303
+ logger.debug(`SolveBudget::spendWallet usedRaw=${usedRaw}, walletUsd=${this.walletUsd}, totalUnused=${this.totalUnused}`);
1304
+ return usedRaw;
1305
+ }
1306
+
1307
+ /** Add **raw nominal USD** to the operator wallet balance (e.g. Extended→wallet withdrawal). */
1308
+ addToWallet(rawUsd: number): void {
1309
+ assert(rawUsd >= 0, 'SolveBudget::addToWallet amount must be positive');
1310
+ if (rawUsd === 0) return;
1311
+ if (this.walletBalance) {
1312
+ this._addUsdToTokenBalance(this.walletBalance, rawUsd);
1313
+ }
1314
+ this._recomputeUnusedBalance();
1315
+ logger.debug(`SolveBudget::addToWallet rawUsd=${rawUsd}, walletUsd=${this.walletUsd}, totalUnused=${this.totalUnused}`);
1316
+ }
1317
+
1318
+ spendVAWallet(rawDesired: number): number {
1319
+ let remaining = Math.max(0, rawDesired);
1320
+ const vaSpent = this.spendVA(remaining);
1321
+ remaining -= vaSpent;
1322
+ const walletSpent = this.spendWallet(remaining);
1323
+ return vaSpent + walletSpent;
1324
+ }
1325
+
1326
+ private _updateExtAvailWithdraw(desiredRaw: number, isSpend: boolean): number {
1327
+ assert(desiredRaw > 0, 'SolveBudget::_updateExtAvailWithdraw amount must be positive');
1328
+ let rawDelta: number;
1329
+ if (isSpend) {
1330
+ const capRaw = this.extendedBalance?.availableForWithdrawal?.toNumber() ?? 0;
1331
+ const useRaw = Math.min(capRaw, desiredRaw);
1332
+ if (useRaw <= 0) return 0;
1333
+ rawDelta = -useRaw;
1334
+ } else {
1335
+ rawDelta = desiredRaw;
1336
+ }
1337
+
1338
+ if (this.extendedBalance) {
1339
+ this.extendedBalance.availableForWithdrawal = safeUsdcWeb3Number(this.extendedBalance.availableForWithdrawal.toNumber() + rawDelta);
1340
+ this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() + rawDelta);
1341
+ this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + rawDelta);
1342
+ this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + rawDelta);
1343
+ }
1344
+ logger.debug(`SolveBudget::updateExtAvailWithdraw rawDelta=${rawDelta}, extAvailWithdraw=${this.extAvailWithdraw}, totalUnused=${this.totalUnused}`);
1345
+ return rawDelta;
1346
+ }
1347
+
1348
+ private _updateExtAvailUpnl(desiredRaw: number, isSpend: boolean): number {
1349
+ assert(desiredRaw > 0, 'SolveBudget::_updateExtAvailUpnl amount must be positive');
1350
+ let rawDelta: number;
1351
+ if (isSpend) {
1352
+ const capRaw = this.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
1353
+ const useRaw = Math.min(capRaw, desiredRaw);
1354
+ if (useRaw <= 0) return 0;
1355
+ rawDelta = -useRaw;
1356
+ } else {
1357
+ rawDelta = desiredRaw;
1358
+ }
1359
+
1360
+ if (this.extendedBalance) {
1361
+ this.extendedBalance.unrealisedPnl = safeUsdcWeb3Number(this.extendedBalance.unrealisedPnl.toNumber() + rawDelta);
1362
+ this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + rawDelta);
1363
+ this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + rawDelta);
1364
+ this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() + rawDelta);
1365
+ }
1366
+ logger.debug(`SolveBudget::updateExtAvailUpnl rawDelta=${rawDelta}, extAvailUpnl=${this.extAvailUpnl}, totalUnused=${this.totalUnused}`);
1367
+ return rawDelta;
1368
+ }
1369
+
1370
+ spendExtAvailTrade(rawDesired: number): number {
1371
+ const usedWd = this._updateExtAvailWithdraw(rawDesired, true);
1372
+ const tookWd = Math.abs(usedWd);
1373
+ const rem = rawDesired - tookWd;
1374
+ const usedUpnl = rem > 0 ? this._updateExtAvailUpnl(rem, true) : 0;
1375
+ logger.debug(`SolveBudget::updateExtAvailTrade rawSum=${usedWd + usedUpnl}, extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`);
1376
+ return usedWd + usedUpnl;
1377
+ }
1378
+
1379
+ // simply reduces available amounts, but maintains equity and balance.
1380
+ spendExtAvailTradeToEquityOnly(rawDesired: number): number {
1381
+ const used = this._updateExtAvailWithdraw(rawDesired, true);
1382
+ const remaining = rawDesired - Math.abs(used);
1383
+ const usedUpnl = remaining > 0 ? this._updateExtAvailUpnl(remaining, true) : 0;
1384
+ if (this.extendedBalance) {
1385
+ // add whats subtracted earlier to equity
1386
+ const net = Math.abs(used) + Math.abs(usedUpnl);
1387
+ if (net.toFixed(0) != rawDesired.toFixed(0)) {
1388
+ throw new Error(`SolveBudget::spendExtAvailTradeToEquityOnly net=${net} != rawDesired=${rawDesired}`);
1389
+ }
1390
+ this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + net);
1391
+ this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + net);
1392
+ }
1393
+ logger.debug(`SolveBudget::updateExtAvailTrade rawSum=${used + usedUpnl}, extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`);
1394
+ return used + usedUpnl;
1395
+ }
1396
+
1397
+ spendExtAvailWithdraw(rawDesired: number): number {
1398
+ return this._updateExtAvailWithdraw(rawDesired, true);
1399
+ }
1400
+
1401
+ spendExtAvailUpnl(rawDesired: number): number {
1402
+ return this._updateExtAvailUpnl(rawDesired, true);
1403
+ }
1404
+
1405
+ /**
1406
+ * Withdraw from Extended **withdrawal bucket only** to operator wallet (planning).
1407
+ * Used when VA must be funded from Extended and withdraw should be exhausted before unrealised PnL.
1408
+ */
1409
+ spendAvailWithdrawToWallet(rawDesired: number): number {
1410
+ const want = Math.max(0, rawDesired);
1411
+ if (want <= CASE_THRESHOLD_USD) return 0;
1412
+ const rawDelta = this._updateExtAvailWithdraw(want, true);
1413
+ if (rawDelta === 0) return 0;
1414
+ const used = -rawDelta;
1415
+ this.addToWallet(used);
1416
+ return used;
1417
+ }
1418
+
1419
+ /**
1420
+ * Required Extended equity (USD) for current open positions: total notional ÷ strategy leverage.
1421
+ * Same basis as {@link ExtendedSVKVesuStateManager._classifyLtvExtended} margin check.
1422
+ */
1423
+ private _extendedMarginRequirementUsd(): number {
1424
+ const lev = calculateExtendedLevergae();
1425
+ if (lev <= 0 || this.extendedPositions.length === 0) return 0;
1426
+ const totalPosUsd = this.extendedPositions.reduce(
1427
+ (s, p) => s + p.valueUsd.toNumber(),
1428
+ 0,
1429
+ );
1430
+ return totalPosUsd / lev;
1431
+ }
1432
+
1433
+ /** How much more equity is needed before deposits should increase withdraw / trade availability. */
1434
+ private _extendedEquityShortfallUsd(): number {
1435
+ if (!this.extendedBalance) return 0;
1436
+ const req = this._extendedMarginRequirementUsd();
1437
+ const eq = this.extendedBalance.equity.toNumber();
1438
+ return Math.max(0, req - eq);
1439
+ }
1440
+
1441
+ /**
1442
+ * Credits a USDC inflow on Extended. Fills margin shortfall (balance+equity only) first;
1443
+ * any remainder is credited across balance, equity, availableForWithdrawal, and availableForTrade.
1444
+ */
1445
+ addToExtAvailTrade(rawUsd: number): void {
1446
+ assert(rawUsd >= 0, 'SolveBudget::addToExtAvailTrade amount must be non-negative');
1447
+ if (rawUsd <= CASE_THRESHOLD_USD) return;
1448
+ if (!this.extendedBalance) {
1449
+ logger.warn('SolveBudget::addToExtAvailTrade skipped — no extendedBalance');
1450
+ return;
1451
+ }
1452
+
1453
+ const shortfall = this._extendedEquityShortfallUsd();
1454
+ const toMargin = Math.min(rawUsd, shortfall);
1455
+ const toLiquid = rawUsd - toMargin;
1456
+
1457
+ if (toMargin > CASE_THRESHOLD_USD) {
1458
+ const b = this.extendedBalance.balance.toNumber();
1459
+ const e = this.extendedBalance.equity.toNumber();
1460
+ this.extendedBalance.balance = safeUsdcWeb3Number(b + toMargin);
1461
+ this.extendedBalance.equity = safeUsdcWeb3Number(e + toMargin);
1462
+ logger.debug(
1463
+ `SolveBudget::addToExtAvailTrade margin-first rawUsd=${toMargin} ` +
1464
+ `(shortfallBefore=${shortfall}, balance=${b + toMargin}, equity=${e + toMargin})`,
1465
+ );
1466
+ }
1467
+
1468
+ if (toLiquid > CASE_THRESHOLD_USD) {
1469
+ this._updateExtAvailWithdraw(toLiquid, false);
1470
+ }
1471
+
1472
+ logger.debug(
1473
+ `SolveBudget::addToExtAvailTrade total rawUsd=${rawUsd} toLiquid=${toLiquid} ` +
1474
+ `extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`,
1475
+ );
1476
+ }
1477
+
1478
+ spendVesuBorrowCapacity(desired: number): { used: number, spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] } {
1479
+ const used = Math.min(this.vesuBorrowCapacity, Math.max(0, desired));
1480
+
1481
+ let spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] = [];
1482
+ // reduce the debt delta for the pool
1483
+ for (let index = 0; index < this.vesuPerPoolDebtDeltasToBorrow.length; index++) {
1484
+ const d = this.vesuPerPoolDebtDeltasToBorrow[index];
1485
+ if (d.toNumber() <= CASE_THRESHOLD_USD) continue;
1486
+ const borrowed = Math.min(d.toNumber(), used);
1487
+ this.vesuPerPoolDebtDeltasToBorrow[index] = d.minus(safeUsdcWeb3Number(borrowed));
1488
+ this.vesuPoolStates[index].debtAmount = safeUsdcWeb3Number(this.vesuPoolStates[index].debtAmount.toNumber() + borrowed);
1489
+ this.vesuPoolStates[index].debtUsdValue = this.vesuPoolStates[index].debtAmount.toNumber() * this.vesuPoolStates[index].debtPrice;
1490
+ spendsByPool.push({ poolId: this.vesuPoolStates[index].poolId, amount: safeUsdcWeb3Number(borrowed), collateralToken: this.vesuPoolStates[index].collateralToken, debtToken: this.vesuPoolStates[index].debtToken });
1491
+ }
1492
+
1493
+ logger.debug(`SolveBudget::spendVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this.vesuBorrowCapacity}, totalUnused=${this.totalUnused}`);
1494
+ return { used, spendsByPool };
1495
+ }
1496
+
1497
+ repayVesuBorrowCapacity(desired: number): { used: number, spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] } {
1498
+ assert(desired > 0, 'SolveBudget::repayVesuBorrowCapacity desired must be positive');
1499
+ const used = desired;
1500
+
1501
+ const spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] = [];
1502
+ for (let index = 0; index < this.vesuPerPoolDebtDeltasToBorrow.length; index++) {
1503
+ const d = this.vesuPerPoolDebtDeltasToBorrow[index];
1504
+ if (d.toNumber() >= -CASE_THRESHOLD_USD) continue; // positive means, dont repay
1505
+ const repaid = Math.min(Math.abs(d.toNumber()), used);
1506
+ this.vesuPerPoolDebtDeltasToBorrow[index] = d.plus(safeUsdcWeb3Number(repaid));
1507
+ this.vesuPoolStates[index].debtAmount = safeUsdcWeb3Number(this.vesuPoolStates[index].debtAmount.toNumber() - repaid);
1508
+ spendsByPool.push({ poolId: this.vesuPoolStates[index].poolId, amount: safeUsdcWeb3Number(-repaid), collateralToken: this.vesuPoolStates[index].collateralToken, debtToken: this.vesuPoolStates[index].debtToken });
1509
+ }
1510
+
1511
+ logger.debug(`SolveBudget::repayVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this.vesuBorrowCapacity}, totalUnused=${this.totalUnused}`);
1512
+ return { used, spendsByPool };
1513
+ }
1514
+
1515
+ // ── State mutation ──────────────────────────────────────────────────
1516
+
1517
+ /** Update a Vesu pool's collateral and debt after a lever / borrow / repay route. */
1518
+ applyVesuDelta(poolId: ContractAddr, collToken: TokenInfo, debtToken: TokenInfo, collDelta: Web3Number, debtDelta: Web3Number): void {
1519
+ const pool = this.vesuPoolStates.find(
1520
+ (p) => p.poolId.toString() === poolId.toString() && p.collateralToken.symbol === collToken.symbol && p.debtToken.symbol === debtToken.symbol,
1521
+ );
1522
+ if (!pool) throw new Error(`SolveBudget::applyVesuDelta pool not found: poolId=${poolId.toString()}, collToken=${collToken.symbol}, debtToken=${debtToken.symbol}`);
1523
+ pool.collateralAmount = new Web3Number(
1524
+ pool.collateralAmount.plus(collDelta).toFixed(pool.collateralToken.decimals),
1525
+ pool.collateralToken.decimals,
1526
+ );
1527
+ pool.collateralUsdValue = pool.collateralAmount.toNumber() * pool.collateralPrice;
1528
+ pool.debtAmount = new Web3Number(
1529
+ pool.debtAmount.plus(debtDelta).toFixed(pool.debtToken.decimals),
1530
+ pool.debtToken.decimals,
1531
+ );
1532
+ pool.debtUsdValue = pool.debtAmount.toNumber() * pool.debtPrice;
1533
+
1534
+ // recompute per pool deltas here
1535
+ const vesuPerPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
1536
+ this.vesuPerPoolDebtDeltasToBorrow = vesuPerPoolDebtDeltasToBorrow.map((item) => item.deltaDebt);
1537
+ this.shouldVesuRebalance = vesuPerPoolDebtDeltasToBorrow.map((item) => item.shouldRebalance);
1538
+ }
1539
+
1540
+ /**
1541
+ * Update Extended position size after a lever route; sync {@link ExtendedPositionState.valueUsd}
1542
+ * and margin buckets (released USD on decrease, locked USD on increase).
1543
+ *
1544
+ * @param collateralPriceUsd BTC collateral price for notional / margin math; if omitted, uses
1545
+ * existing valueUsd / |size| or the first Vesu pool collateral price.
1546
+ */
1547
+ applyExtendedExposureDelta(
1548
+ instrument: string,
1549
+ sizeDelta: Web3Number,
1550
+ collateralPriceUsd?: number,
1551
+ ): void {
1552
+ const btcEps = 10 ** -COLLATERAL_PRECISION;
1553
+ const lev = calculateExtendedLevergae();
1554
+
1555
+ let pos = this.extendedPositions.find((p) => p.instrument === instrument);
1556
+ const oldAbs = pos ? Math.abs(pos.size.toNumber()) : 0;
1557
+ const oldValUsd = pos ? pos.valueUsd.toNumber() : 0;
1558
+
1559
+ if (pos) {
1560
+ pos.size = new Web3Number(pos.size.plus(sizeDelta).toFixed(8), 8);
1561
+ } else if (sizeDelta.toNumber() !== 0) {
1562
+ this.extendedPositions.push({
1563
+ instrument,
1564
+ side: 'SHORT',
1565
+ size: sizeDelta,
1566
+ valueUsd: new Web3Number(0, USDC_TOKEN_DECIMALS),
1567
+ leverage: '0',
1568
+ });
1569
+ pos = this.extendedPositions[this.extendedPositions.length - 1];
1570
+ } else {
1571
+ return;
1572
+ }
1573
+
1574
+ const newAbs = Math.abs(pos.size.toNumber());
1575
+ let price = collateralPriceUsd;
1576
+ if (price === undefined || price <= 0) {
1577
+ price = oldAbs > btcEps ? oldValUsd / oldAbs : (this.vesuPoolStates[0]?.collateralPrice ?? 0);
1578
+ }
1579
+
1580
+ if (price > 0) {
1581
+ pos.valueUsd = new Web3Number(
1582
+ (newAbs * price).toFixed(USDC_TOKEN_DECIMALS),
1583
+ USDC_TOKEN_DECIMALS,
1584
+ );
1585
+ }
1586
+
1587
+ if (!this.extendedBalance || lev <= 0 || price <= 0) return;
1588
+
1589
+ const dAbs = newAbs - oldAbs;
1590
+ if (dAbs < -btcEps) {
1591
+ const releasedUsd = (-dAbs) * price / lev;
1592
+ if (releasedUsd > CASE_THRESHOLD_USD) {
1593
+ this.addToExtAvailTrade(releasedUsd);
1594
+ }
1595
+ } else if (dAbs > btcEps) {
1596
+ const lockedUsd = dAbs * price / lev;
1597
+ if (lockedUsd > CASE_THRESHOLD_USD) {
1598
+ this._lockExtendedMarginUsd(lockedUsd);
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ /** Pull margin for larger Extended exposure from liquid buckets, then balance/equity. */
1604
+ private _lockExtendedMarginUsd(lockedUsd: number): void {
1605
+ if (lockedUsd <= CASE_THRESHOLD_USD || !this.extendedBalance) return;
1606
+ let rem = lockedUsd;
1607
+
1608
+ const uw = Math.min(rem, Math.max(0, this.extendedBalance.availableForWithdrawal.toNumber()));
1609
+ if (uw > 0) {
1610
+ this._updateExtAvailWithdraw(uw, true);
1611
+ rem -= uw;
1612
+ }
1613
+ if (rem > 0) {
1614
+ const uu = Math.min(rem, Math.max(0, this.extendedBalance.unrealisedPnl.toNumber()));
1615
+ if (uu > 0) {
1616
+ this._updateExtAvailUpnl(uu, true);
1617
+ rem -= uu;
1618
+ }
1619
+ }
1620
+ if (rem > 0) {
1621
+ const b = this.extendedBalance.balance.toNumber();
1622
+ const e = this.extendedBalance.equity.toNumber();
1623
+ this.extendedBalance.balance = safeUsdcWeb3Number(b - rem);
1624
+ this.extendedBalance.equity = safeUsdcWeb3Number(e - rem);
1625
+ }
1626
+ }
1627
+
1628
+
1629
+ /**
1630
+ * For each Vesu pool, computes the debt delta needed to bring the position
1631
+ * back to the target health factor.
1632
+ *
1633
+ * Positive = can borrow more, negative = need to repay.
1634
+ */
1635
+ private _computeperPoolDebtDeltasToBorrow(): { deltaDebt: Web3Number, shouldRebalance: boolean }[] {
1636
+ const output = this.vesuPoolStates.map((pool) =>
1637
+ calculateDeltaDebtAmount(
1638
+ pool.collateralAmount,
1639
+ pool.debtAmount,
1640
+ pool.debtPrice,
1641
+ pool.collateralPrice,
1642
+ ),
1643
+ );
1644
+
1645
+ // assert al pools share same collateral and debt tokens
1646
+ const collateralToken = this.vesuPoolStates[0].collateralToken;
1647
+ const debtToken = this.vesuPoolStates[0].debtToken;
1648
+ for (const pool of this.vesuPoolStates) {
1649
+ if (pool.collateralToken.symbol !== collateralToken.symbol || pool.debtToken.symbol !== debtToken.symbol) {
1650
+ // not yet handled in code with multiple pool types. will be handled in future.
1651
+ throw new Error(`SolveBudget::_computeperPoolDebtDeltasToBorrow: All pools must share same collateral and debt tokens`);
1652
+ }
1653
+ }
1654
+ return output;
1655
+ }
1656
+ }
1657
+
1658
+ export function createSolveBudgetFromRawState(params: {
1659
+ assetToken: TokenInfo;
1660
+ usdcToken: TokenInfo;
1661
+ unusedBalance: TokenBalance[];
1662
+ walletBalance: TokenBalance | null;
1663
+ vaultAssetBalance: TokenBalance | null;
1664
+ vaultUsdcBalance: TokenBalance | null;
1665
+ extendedPositions: ExtendedPositionState[];
1666
+ extendedBalance: ExtendedBalanceState | null;
1667
+ vesuPoolStates: VesuPoolState[];
1668
+ limitBalanceBufferFactor?: number;
1669
+ }): SolveBudget {
1670
+ const budget = new SolveBudget({
1671
+ assetToken: params.assetToken,
1672
+ usdcToken: params.usdcToken,
1673
+ unusedBalance: params.unusedBalance,
1674
+ walletBalance: params.walletBalance,
1675
+ vaultAssetBalance: params.vaultAssetBalance,
1676
+ vaultUsdcBalance: params.vaultUsdcBalance,
1677
+ extendedPositions: params.extendedPositions,
1678
+ extendedBalance: params.extendedBalance,
1679
+ vesuPoolStates: params.vesuPoolStates,
1680
+ });
1681
+ if (params.limitBalanceBufferFactor && params.limitBalanceBufferFactor > 0) {
1682
+ budget.applyBuffer(params.limitBalanceBufferFactor);
1683
+ }
1684
+ return budget;
1685
+ }
1686
+
1687
+ // ─── State Manager ─────────────────────────────────────────────────────────────
1688
+
1689
+ /**
1690
+ * Reads all on-chain / off-chain state for the Extended + SVK + Vesu strategy,
1691
+ * then solves for the optimal rebalancing deltas.
1692
+ *
1693
+ * Usage:
1694
+ * const manager = new ExtendedSVKVesuStateManager(config);
1695
+ * const result = await manager.solve();
1696
+ */
1697
+ export class ExtendedSVKVesuStateManager {
1698
+ private readonly _config: StateManagerConfig;
1699
+ private readonly _tag = "ExtendedSVKVesuStateManager";
1700
+
1701
+ /** Single mutable state holder — initialised by _refresh(), budget by initBudget(). */
1702
+ private _budget!: SolveBudget;
1703
+
1704
+ constructor(config: StateManagerConfig) {
1705
+ this._config = config;
1706
+ }
1707
+
1708
+ // ═══════════════════════════════════════════════════════════════════════════
1709
+ // Public API
1710
+ // ═══════════════════════════════════════════════════════════════════════════
1711
+
1712
+ /**
1713
+ * Main entry point. Refreshes all state, then computes and returns
1714
+ * the optimal deltas for rebalancing the strategy.
1715
+ *
1716
+ * @param withdrawAmount — amount (in vault asset, e.g. USDC) the user wants
1717
+ * to withdraw. When > 0 the solver shrinks protocol allocations so that the
1718
+ * vault allocator ends up with enough balance to execute bringLiquidity.
1719
+ * Pass 0 (default) for normal investment / rebalancing cycles.
1720
+ */
1721
+ async solve(
1722
+ withdrawAmount: Web3Number = new Web3Number(0, USDC_TOKEN_DECIMALS),
1723
+ ): Promise<SolveResult | null> {
1724
+ await this._refresh();
1725
+ if (Math.abs(this._budget.extPendingDeposit) > 0) {
1726
+ logger.warn(`${this._tag}::solve extPendingDeposit=${this._budget.extPendingDeposit}`);
1727
+ return null;
1728
+ }
1729
+ this._validateRefreshedState();
1730
+
1731
+ // // Step 1: For each Vesu pool, compute debt delta needed for LTV maintenance
1732
+ // const perPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
1733
+
1734
+ // // Step 2: Determine distributable capital after debt and withdrawal adjustments
1735
+ // const distributableAmount = this._computeDistributableAmount(
1736
+ // perPoolDebtDeltasToBorrow,
1737
+ // withdrawAmount,
1738
+ // );
1739
+
1740
+ // // Step 3: Split distributable capital between Vesu and Extended allocations
1741
+ // const { vesuAllocationUsd, extendedAllocationUsd } =
1742
+ // this._computeAllocationSplit(distributableAmount, perPoolDebtDeltasToBorrow);
1743
+
1744
+ // // Step 4: Convert Vesu allocation to per-pool collateral deltas
1745
+ // const vesuDeltas =
1746
+ // this._computePerPoolCollateralDeltas(vesuAllocationUsd, perPoolDebtDeltasToBorrow);
1747
+
1748
+ // // Step 5: Compute Extended position deltas for delta-neutral matching
1749
+ // const extendedPositionDeltas =
1750
+ // this._computeExtendedPositionDeltas(vesuDeltas);
1751
+ // console.log({ extendedPositionDeltas: extendedPositionDeltas.map((delta) => ({
1752
+ // instrument: delta.instrument,
1753
+ // delta: delta.delta.toNumber(),
1754
+ // })) });
1755
+
1756
+ // // Step 6: Compute net deposit/withdrawal change for Extended platform
1757
+ // const extendedDeposit =
1758
+ // this._computeExtendedDepositDelta(extendedAllocationUsd);
1759
+ // console.log({ extendedDeposit: extendedDeposit.toNumber() });
1760
+
1761
+ // // Step 7: compute vesu deposit amount (i.e. collateral required to open position may be)
1762
+ // const vesuDepositAmount =
1763
+ // this._computeVesuDepositAmount(vesuDeltas);
1764
+
1765
+ // // Step 7: Compute bringLiquidity amount (= withdrawAmount for the vault)
1766
+ // const bringLiquidityAmount =
1767
+ // this._computeBringLiquidityAmount(withdrawAmount);
1768
+ // console.log({ bringLiquidityAmount: bringLiquidityAmount.toNumber() });
1769
+
1770
+ // Step 8: Classify state into actionable cases — each case carries
1771
+ // its own routes with amounts and state info.
1772
+ const cases = this._classifyCases(withdrawAmount);
1773
+
1774
+ const result: SolveResult = {
1775
+ cases,
1776
+ // ignore these fields for now. only cases are relevant.
1777
+ extendedDeposit: safeUsdcWeb3Number(0),
1778
+ extendedPositionDeltas: [],
1779
+ vesuDepositAmount: safeUsdcWeb3Number(0),
1780
+ vesuDeltas: [],
1781
+ vesuAllocationUsd: safeUsdcWeb3Number(0),
1782
+ extendedAllocationUsd: safeUsdcWeb3Number(0),
1783
+ bringLiquidityAmount: safeUsdcWeb3Number(0),
1784
+ pendingDeposit: safeUsdcWeb3Number(0),
1785
+ };
1786
+
1787
+ this._logSolveResult(result);
1788
+ return result;
1789
+ }
1790
+
1791
+ // ═══════════════════════════════════════════════════════════════════════════
1792
+ // Private — state refresh
1793
+ // ═══════════════════════════════════════════════════════════════════════════
1794
+
1795
+ /**
1796
+ * Reads all on-chain and off-chain state in parallel and stores
1797
+ * results in private instance variables.
1798
+ */
1799
+ private async _refresh(): Promise<void> {
1800
+ logger.info(`${this._tag}::_refresh starting`);
1801
+
1802
+ const [
1803
+ vaultAssetBalance,
1804
+ vaultUsdcBalance,
1805
+ walletBalance,
1806
+ vesuPoolStates,
1807
+ extendedBalance,
1808
+ extendedPositions,
1809
+ ] = await Promise.all([
1810
+ this._fetchVaultAllocatorAssetBalance(),
1811
+ this._fetchVaultAllocatorUsdcBalanceIfDistinct(),
1812
+ this._fetchWalletBalances(),
1813
+ this._fetchAllVesuPoolStates(),
1814
+ this._fetchExtendedBalance(),
1815
+ this._fetchExtendedPositions(),
1816
+ ]);
1817
+
1818
+ logger.verbose(
1819
+ `${this._tag}::_refresh ` +
1820
+ `${vaultAssetBalance ? `VA asset ${vaultAssetBalance.token.symbol}=$${vaultAssetBalance.usdValue.toFixed(2)}, ` : ""}` +
1821
+ `VA USDC=${vaultUsdcBalance.usdValue.toFixed(2)}` +
1822
+ `, wallet=${walletBalance.usdValue}`,
1823
+ );
1824
+ const unusedBalance = this._computeUnusedBalances(
1825
+ vaultAssetBalance,
1826
+ vaultUsdcBalance,
1827
+ walletBalance,
1828
+ );
1829
+
1830
+ this._budget = createSolveBudgetFromRawState({
1831
+ assetToken: this._config.assetToken,
1832
+ usdcToken: this._config.usdcToken,
1833
+ unusedBalance,
1834
+ walletBalance,
1835
+ vaultAssetBalance,
1836
+ vaultUsdcBalance,
1837
+ extendedPositions,
1838
+ extendedBalance: {
1839
+ availableForTrade:
1840
+ extendedBalance?.availableForTrade ||
1841
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
1842
+ availableForWithdrawal:
1843
+ extendedBalance?.availableForWithdrawal ||
1844
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
1845
+ unrealisedPnl:
1846
+ extendedBalance?.unrealisedPnl ||
1847
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
1848
+ balance:
1849
+ extendedBalance?.balance || new Web3Number(0, USDC_TOKEN_DECIMALS),
1850
+ equity:
1851
+ extendedBalance?.equity || new Web3Number(0, USDC_TOKEN_DECIMALS),
1852
+ pendingDeposit:
1853
+ extendedBalance?.pendingDeposit ||
1854
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
1855
+ },
1856
+ vesuPoolStates,
1857
+ });
1858
+ this._budget.logStateSummary();
1859
+
1860
+ const totalUnusedUsd = unusedBalance.reduce(
1861
+ (acc, b) => acc + b.usdValue,
1862
+ 0,
1863
+ );
1864
+ logger.info(
1865
+ `${this._tag}::_refresh completed — ` +
1866
+ `unusedBalances: ${unusedBalance.length} tokens [${unusedBalance.map((b) => `${b.token.symbol}=$${b.usdValue.toFixed(2)}`).join(', ')}], ` +
1867
+ `totalUnusedUsd: ${totalUnusedUsd.toFixed(2)}, ` +
1868
+ `extendedPositions: ${extendedPositions.length} [${extendedPositions.map((p) => `${p.instrument}=${p.size.toFixed(6)} ${p.side}, ${p.valueUsd.toFixed(6)} ${p.instrument}`).join(', ')}], ` +
1869
+ `vesuPools: ${vesuPoolStates.length} [${vesuPoolStates.map((p) => `${p.poolId.shortString()}=${p.debtAmount.toFixed(6)} ${p.debtToken.symbol}, ${p.collateralAmount.toFixed(6)} ${p.collateralToken.symbol}`).join(', ')}], ` +
1870
+ `availableForTrade: ${extendedBalance?.availableForTrade.toNumber()} - ` +
1871
+ `availableForWithdrawal: ${extendedBalance?.availableForWithdrawal.toNumber()} - ` +
1872
+ `unrealisedPnl: ${extendedBalance?.unrealisedPnl.toNumber()} - ` +
1873
+ `extendedBalance::balance: ${extendedBalance?.balance.toNumber()} - ` +
1874
+ `extendedBalance::pendingDeposit: ${extendedBalance?.pendingDeposit.toNumber()} - ` +
1875
+ `extendedBalance::equity: ${extendedBalance?.equity.toNumber()}`,
1876
+ );
1877
+ }
1878
+
1879
+ // todo add communication check with python server of extended. if not working, throw error in solve function.
1880
+
1881
+ /** True when strategy asset and USDC share one token — VA idle balance is tracked as USDC, not as asset. */
1882
+ private _vaultAssetAndUsdcAreSameToken(): boolean {
1883
+ return this._config.assetToken.address.eq(this._config.usdcToken.address);
1884
+ }
1885
+
1886
+ /**
1887
+ * Reads idle {@link StateManagerConfig.assetToken} in the vault allocator when it differs from USDC.
1888
+ * When asset and USDC are the same token, returns null (that balance is reported via {@link _fetchVaultAllocatorUsdcBalanceIfDistinct} only).
1889
+ */
1890
+ private async _fetchVaultAllocatorAssetBalance(): Promise<TokenBalance | null> {
1891
+ if (this._vaultAssetAndUsdcAreSameToken()) {
1892
+ return null;
1893
+ }
1894
+ const { assetToken, vaultAllocator, networkConfig, pricer } = this._config;
1895
+ const balance = await new ERC20(networkConfig).balanceOf(
1896
+ assetToken.address,
1897
+ vaultAllocator,
1898
+ assetToken.decimals,
1899
+ );
1900
+ const price = await pricer.getPrice(assetToken.symbol);
1901
+ const usdValue =
1902
+ Number(balance.toFixed(assetToken.decimals)) * price.price;
1903
+
1904
+ return { token: assetToken, amount: balance, usdValue };
1905
+ }
1906
+
1907
+ /**
1908
+ * Reads {@link StateManagerConfig.usdcToken} idle in the vault allocator (always — distinct asset or USDC-as-asset).
1909
+ */
1910
+ private async _fetchVaultAllocatorUsdcBalanceIfDistinct(): Promise<TokenBalance> {
1911
+ const { usdcToken, vaultAllocator, networkConfig, pricer } = this._config;
1912
+ const balance = await new ERC20(networkConfig).balanceOf(
1913
+ usdcToken.address,
1914
+ vaultAllocator,
1915
+ usdcToken.decimals,
1916
+ );
1917
+ const tokenPrice = await pricer.getPrice(
1918
+ usdcToken.priceProxySymbol || usdcToken.symbol,
1919
+ );
1920
+ const usdValue =
1921
+ Number(balance.toFixed(usdcToken.decimals)) * tokenPrice.price;
1922
+
1923
+ return { token: usdcToken, amount: balance, usdValue };
1924
+ }
1925
+
1926
+ /**
1927
+ * Merges vault-allocator asset (if any), vault-allocator USDC, and operator wallet
1928
+ * balances into entries keyed by token address.
1929
+ */
1930
+ private _computeUnusedBalances(
1931
+ vaultAssetBalance: TokenBalance | null,
1932
+ vaultUsdcBalance: TokenBalance,
1933
+ walletBalance: TokenBalance,
1934
+ ): TokenBalance[] {
1935
+ const balanceMap = new Map<string, TokenBalance>();
1936
+
1937
+ const put = (tb: TokenBalance) => {
1938
+ balanceMap.set(tb.token.address.toString(), {
1939
+ token: tb.token,
1940
+ amount: tb.amount,
1941
+ usdValue: tb.usdValue,
1942
+ });
1943
+ };
1944
+
1945
+ if (vaultAssetBalance) put(vaultAssetBalance);
1946
+ put(vaultUsdcBalance);
1947
+
1948
+ // Merge wallet balances by token address
1949
+ const key = walletBalance.token.address.toString();
1950
+ const existing = balanceMap.get(key);
1951
+ if (existing) {
1952
+ existing.amount = new Web3Number(
1953
+ existing.amount.plus(walletBalance.amount).toFixed(existing.token.decimals),
1954
+ existing.token.decimals,
1955
+ );
1956
+ existing.usdValue += walletBalance.usdValue;
1957
+ } else {
1958
+ balanceMap.set(key, {
1959
+ token: walletBalance.token,
1960
+ amount: walletBalance.amount,
1961
+ usdValue: walletBalance.usdValue,
1962
+ });
1963
+ }
1964
+
1965
+ return Array.from(balanceMap.values());
1966
+ }
1967
+
1968
+ /**
1969
+ * Reads the operator wallet balance for {@link StateManagerConfig.usdcToken} only
1970
+ * (wallet stablecoin is always USDC, regardless of strategy {@link StateManagerConfig.assetToken}).
1971
+ */
1972
+ private async _fetchWalletBalances(): Promise<TokenBalance> {
1973
+ const {
1974
+ networkConfig,
1975
+ pricer,
1976
+ walletAddress,
1977
+ usdcToken,
1978
+ } = this._config;
1979
+ const erc20 = new ERC20(networkConfig);
1980
+
1981
+ const [balance, tokenPrice] =
1982
+ await Promise.all([
1983
+ erc20.balanceOf(
1984
+ usdcToken.address,
1985
+ walletAddress,
1986
+ usdcToken.decimals,
1987
+ ),
1988
+ pricer.getPrice(usdcToken.priceProxySymbol || usdcToken.symbol),
1989
+ ]);
1990
+
1991
+ return {
1992
+ token: usdcToken,
1993
+ amount: balance,
1994
+ usdValue:
1995
+ Number(balance.toFixed(usdcToken.decimals)) * tokenPrice.price,
1996
+ };
1997
+ }
1998
+
1999
+ /**
2000
+ * Reads the Extended exchange account-level balance (equity, available for
2001
+ * trade/withdrawal, etc.). Returns null if the API call fails.
2002
+ */
2003
+ private async _fetchExtendedBalance(): Promise<ExtendedBalanceState | null> {
2004
+ const [holdings, pendingDeposit] = await Promise.all([
2005
+ this._config.extendedAdapter.getExtendedDepositAmount(),
2006
+ this._fetchPendingDeposit(),
2007
+ ]);
2008
+ if (!holdings) return null;
2009
+
2010
+ return {
2011
+ equity: new Web3Number(holdings.equity, USDC_TOKEN_DECIMALS),
2012
+ availableForTrade: new Web3Number(
2013
+ holdings.availableForTrade,
2014
+ USDC_TOKEN_DECIMALS,
2015
+ ),
2016
+ availableForWithdrawal: new Web3Number(
2017
+ holdings.availableForWithdrawal,
2018
+ USDC_TOKEN_DECIMALS,
2019
+ ),
2020
+ unrealisedPnl: new Web3Number(holdings.unrealisedPnl, USDC_TOKEN_DECIMALS),
2021
+ balance: new Web3Number(holdings.balance, USDC_TOKEN_DECIMALS),
2022
+ pendingDeposit,
2023
+ };
2024
+ }
2025
+
2026
+ /**
2027
+ * Computes the net pending deposit by subtracting pending withdrawals from
2028
+ * pending deposits. Uses the Extended exchange asset operations API.
2029
+ *
2030
+ * Positive = deposit in transit (wallet → Extended).
2031
+ * Negative = withdrawal in transit (Extended → wallet).
2032
+ */
2033
+ private async _fetchPendingDeposit(): Promise<Web3Number> {
2034
+ try {
2035
+ const client = this._config.extendedAdapter.client;
2036
+ const [pendingDeposits, pendingWithdrawals] = await Promise.all([
2037
+ client.getAssetOperations({
2038
+ operationsType: [AssetOperationType.DEPOSIT],
2039
+ operationsStatus: [AssetOperationStatus.IN_PROGRESS],
2040
+ }),
2041
+ client.getAssetOperations({
2042
+ operationsType: [AssetOperationType.WITHDRAWAL],
2043
+ operationsStatus: [AssetOperationStatus.IN_PROGRESS],
2044
+ }),
2045
+ ]);
2046
+
2047
+ const depositTotal = pendingDeposits.data.reduce(
2048
+ (sum, op) => sum + parseFloat(op.amount || '0'), 0,
2049
+ );
2050
+ const withdrawTotal = pendingWithdrawals.data.reduce(
2051
+ (sum, op) => sum + parseFloat(op.amount || '0'), 0,
2052
+ );
2053
+
2054
+ const net = depositTotal - withdrawTotal;
2055
+ logger.info(
2056
+ `${this._tag}::_fetchPendingDeposit deposits=${depositTotal}, withdrawals=${withdrawTotal}, net=${net}`,
2057
+ );
2058
+ return new Web3Number(net.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
2059
+ } catch (err) {
2060
+ logger.error(`${this._tag}::_fetchPendingDeposit error: ${err}`);
2061
+ return new Web3Number(0, USDC_TOKEN_DECIMALS);
2062
+ }
2063
+ }
2064
+
2065
+ /**
2066
+ * Reads all open positions on the Extended exchange. Each position
2067
+ * includes instrument, side, size, USD value, and leverage.
2068
+ */
2069
+ private async _fetchExtendedPositions(): Promise<ExtendedPositionState[]> {
2070
+ const positions =
2071
+ await this._config.extendedAdapter.getAllOpenPositions();
2072
+ if (!positions) return [];
2073
+
2074
+ return positions.map((position) => ({
2075
+ instrument: position.market,
2076
+ side: position.side,
2077
+ size: new Web3Number(position.size, USDC_TOKEN_DECIMALS), // though in different token terms, this wont matter from DEX perspective, hence 6decimals is ok
2078
+ valueUsd: new Web3Number(position.value, USDC_TOKEN_DECIMALS),
2079
+ leverage: position.leverage,
2080
+ }));
2081
+ }
2082
+
2083
+ /**
2084
+ * Reads a single Vesu pool's position data: collateral amount/price,
2085
+ * debt amount/price.
2086
+ */
2087
+ private async _fetchSingleVesuPoolState(
2088
+ adapter: VesuMultiplyAdapter,
2089
+ ): Promise<VesuPoolState> {
2090
+ const assetPrices = await adapter._vesuAdapter.getAssetPrices();
2091
+ return {
2092
+ poolId: adapter.config.poolId,
2093
+ collateralToken: adapter.config.collateral,
2094
+ debtToken: adapter.config.debt,
2095
+ collateralAmount: assetPrices.collateralTokenAmount,
2096
+ collateralUsdValue: assetPrices.collateralUSDAmount,
2097
+ debtAmount: assetPrices.debtTokenAmount,
2098
+ debtUsdValue: assetPrices.debtUSDAmount,
2099
+ collateralPrice: assetPrices.collateralPrice,
2100
+ debtPrice: assetPrices.debtPrice,
2101
+ };
2102
+ }
2103
+
2104
+ /**
2105
+ * Reads all Vesu pool states in parallel (one per configured adapter).
2106
+ */
2107
+ private async _fetchAllVesuPoolStates(): Promise<VesuPoolState[]> {
2108
+ return Promise.all(
2109
+ this._config.vesuAdapters.map((adapter) =>
2110
+ this._fetchSingleVesuPoolState(adapter),
2111
+ ),
2112
+ );
2113
+ }
2114
+
2115
+ // ═══════════════════════════════════════════════════════════════════════════
2116
+ // Private — validation
2117
+ // ═══════════════════════════════════════════════════════════════════════════
2118
+
2119
+ /**
2120
+ * Validates that all critical refreshed state is present and contains
2121
+ * finite, sensible values. Throws on invalid state.
2122
+ */
2123
+ private _validateRefreshedState(): void {
2124
+ if (this._budget.unusedBalanceRows.length === 0) {
2125
+ throw new Error(
2126
+ `${this._tag}: unusedBalance is empty after refresh`,
2127
+ );
2128
+ }
2129
+ for (const balance of this._budget.unusedBalanceRows) {
2130
+ this._validateTokenBalanceOrThrow(
2131
+ balance,
2132
+ `unusedBalance[${balance.token.symbol}]`,
2133
+ );
2134
+ }
2135
+ this._validateVesuPoolPricesOrThrow();
2136
+ this._validateExtendedBalanceOrThrow();
2137
+ }
2138
+
2139
+ private _validateTokenBalanceOrThrow(
2140
+ balance: TokenBalance | null,
2141
+ label: string,
2142
+ ): void {
2143
+ if (!balance) {
2144
+ throw new Error(`${this._tag}: ${label} is null after refresh`);
2145
+ }
2146
+ if (!Number.isFinite(balance.usdValue) || balance.usdValue < 0) {
2147
+ throw new Error(
2148
+ `${this._tag}: ${label} has invalid usdValue: ${balance.usdValue}`,
2149
+ );
2150
+ }
2151
+ }
2152
+
2153
+ private _validateVesuPoolPricesOrThrow(): void {
2154
+ for (const pool of this._budget.vesuPools) {
2155
+ const poolLabel = pool.poolId.shortString();
2156
+ this._assertPositiveFinite(
2157
+ pool.collateralPrice,
2158
+ `Vesu pool ${poolLabel} collateralPrice`,
2159
+ );
2160
+ this._assertPositiveFinite(
2161
+ pool.debtPrice,
2162
+ `Vesu pool ${poolLabel} debtPrice`,
2163
+ );
2164
+ }
2165
+ }
2166
+
2167
+ private _validateExtendedBalanceOrThrow(): void {
2168
+ if (!this._budget.extendedBalanceView) return; // null is acceptable; treated as zero
2169
+
2170
+ const { equity, availableForTrade } = this._budget.extendedBalanceView;
2171
+ if (
2172
+ !Number.isFinite(equity.toNumber()) ||
2173
+ !Number.isFinite(availableForTrade.toNumber())
2174
+ ) {
2175
+ throw new Error(
2176
+ `${this._tag}: Extended balance contains non-finite values — ` +
2177
+ `equity: ${equity.toNumber()}, availableForTrade: ${availableForTrade.toNumber()}`,
2178
+ );
2179
+ }
2180
+ }
2181
+
2182
+ private _assertPositiveFinite(value: number, label: string): void {
2183
+ if (!Number.isFinite(value) || value <= 0) {
2184
+ throw new Error(
2185
+ `${this._tag}: ${label} is invalid (${value}). Expected finite positive number.`,
2186
+ );
2187
+ }
2188
+ }
2189
+
2190
+ // ═══════════════════════════════════════════════════════════════════════════
2191
+ // Private — solve step 1: per-pool debt deltas (LTV maintenance)
2192
+ // ═══════════════════════════════════════════════════════════════════════════
2193
+
2194
+ // ═══════════════════════════════════════════════════════════════════════════
2195
+ // Private — solve step 2: distributable amount
2196
+ // ═══════════════════════════════════════════════════════════════════════════
2197
+
2198
+ /**
2199
+ * Computes total distributable capital by combining idle vault balance
2200
+ * and Extended available balance, then adjusting for aggregate debt
2201
+ * delta and any pending withdrawal.
2202
+ *
2203
+ * When withdrawAmount > 0 the distributable pool shrinks, forcing the
2204
+ * allocation split to produce smaller (or negative) allocations — which
2205
+ * in turn causes protocol positions to unwind and free capital for the
2206
+ * vault allocator to execute bringLiquidity.
2207
+ */
2208
+ private _computeDistributableAmount(
2209
+ perPoolDebtDeltasToBorrow: readonly Web3Number[],
2210
+ withdrawAmount: Web3Number,
2211
+ ): Web3Number {
2212
+ const totalInvestable = this._computeTotalInvestableAmount();
2213
+ const aggregateDebtAdjustment = this._sumDebtDeltas(perPoolDebtDeltasToBorrow);
2214
+ return new Web3Number(
2215
+ totalInvestable
2216
+ .plus(aggregateDebtAdjustment)
2217
+ .minus(withdrawAmount)
2218
+ .toFixed(USDC_TOKEN_DECIMALS),
2219
+ USDC_TOKEN_DECIMALS,
2220
+ );
2221
+ }
2222
+
2223
+ /**
2224
+ * Total investable = vault allocator balance + Extended available-for-trade +
2225
+ * buffered unrealised PnL, matching `deposit_cases_extended_vesu.xlsx`:
2226
+ * `(VA + wallet + EXT_WITH_AVL + EXT_UPNL) * (1 − buffer)`.
2227
+ * Positive {@link ExtendedBalanceState.pendingDeposit} stays on the afT leg only (see {@link SolveBudget.extAvailTrade}).
2228
+ */
2229
+ private _computeTotalInvestableAmount(): Web3Number {
2230
+ const totalUnusedUsd = this._budget.unusedBalancesBufferedUsdSum;
2231
+ logger.debug(
2232
+ `${this._tag}::_computeTotalInvestableAmount unusedBalances=` +
2233
+ `${JSON.stringify(this._budget.unusedBalanceRows.map((b) => ({ token: b.token.symbol, amount: b.amount.toNumber(), usdValue: b.usdValue })))}`,
2234
+ );
2235
+ const extBal = this._budget.extendedBalanceView;
2236
+ const rawAft = extBal?.availableForWithdrawal?.toNumber() ?? 0;
2237
+ const rawUpnl = extBal?.unrealisedPnl?.toNumber() ?? 0;
2238
+ let extBuffered =
2239
+ this._budget.bufferedUsd(rawAft) + this._budget.bufferedUsd(rawUpnl);
2240
+ const pd = extBal?.pendingDeposit?.toNumber() ?? 0;
2241
+ if (pd > 0) extBuffered += pd;
2242
+ const extendedAvailable = new Web3Number(
2243
+ extBuffered.toFixed(USDC_TOKEN_DECIMALS),
2244
+ USDC_TOKEN_DECIMALS,
2245
+ );
2246
+ logger.verbose(`_computeTotalInvestableAmount totalUnusedUsd: ${totalUnusedUsd}, extendedAvailable: ${extendedAvailable.toNumber()}`);
2247
+ return new Web3Number(totalUnusedUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS)
2248
+ .plus(extendedAvailable)
2249
+ }
2250
+
2251
+ private _sumDebtDeltas(deltas: readonly Web3Number[]): Web3Number {
2252
+ return deltas.reduce(
2253
+ (sum, delta) => sum.plus(delta),
2254
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
2255
+ );
2256
+ }
2257
+
2258
+ // ═══════════════════════════════════════════════════════════════════════════
2259
+ // Private — solve step 3: allocation split
2260
+ // ═══════════════════════════════════════════════════════════════════════════
2261
+
2262
+ /**
2263
+ * Splits distributable capital between Vesu and Extended using leverage
2264
+ * ratios and existing exposure to match delta-neutral targets.
2265
+ *
2266
+ * Formula (from existing strategy):
2267
+ * ExtendedAlloc = (vL * distributable + vesuExposure - extendedExposure) / (vL + eL)
2268
+ * VesuAlloc = distributable - ExtendedAlloc
2269
+ */
2270
+ private _computeAllocationSplit(distributableAmount: Web3Number, deltaVesuCollateral = 0, deltaExtendedCollateral = 0, isRecursive = false): {
2271
+ vesuAllocationUsd: Web3Number;
2272
+ extendedAllocationUsd: Web3Number;
2273
+ vesuPositionDelta: number;
2274
+ extendedPositionDelta: number;
2275
+ } {
2276
+ if (this._hasNoVesuAdapters()) {
2277
+ throw new Error(`${this._tag}: No Vesu adapters found`);
2278
+ }
2279
+
2280
+ const vesuLeverage = calculateVesuLeverage();
2281
+ const extendedLeverage = calculateExtendedLevergae();
2282
+ const denominator = vesuLeverage + extendedLeverage;
2283
+
2284
+ if (denominator === 0) {
2285
+ throw new Error(`${this._tag}: Denominator is zero`);
2286
+ }
2287
+
2288
+ // get long/short exposures on both sides
2289
+ const collateralPrice = this._budget.vesuPools[0]?.collateralPrice ?? 0;
2290
+ const totalVesuExposureUsd = this._totalVesuCollateralUsd().plus(new Web3Number((deltaVesuCollateral * collateralPrice).toFixed(6), USDC_TOKEN_DECIMALS));
2291
+ const totalExtendedExposureUsd = this._totalExtendedExposureUsd().plus(new Web3Number((deltaExtendedCollateral * collateralPrice).toFixed(6), USDC_TOKEN_DECIMALS));
2292
+
2293
+ const numerator =
2294
+ vesuLeverage * distributableAmount.toNumber() +
2295
+ totalVesuExposureUsd.toNumber() -
2296
+ totalExtendedExposureUsd.toNumber();
2297
+
2298
+ const extendedAllocationUsd = new Web3Number(
2299
+ (numerator / denominator).toFixed(USDC_TOKEN_DECIMALS),
2300
+ USDC_TOKEN_DECIMALS,
2301
+ );
2302
+ let vesuAllocationUsd = new Web3Number(
2303
+ distributableAmount
2304
+ .minus(extendedAllocationUsd)
2305
+ .toFixed(USDC_TOKEN_DECIMALS),
2306
+ USDC_TOKEN_DECIMALS,
2307
+ );
2308
+
2309
+ // add debt repayments to vesu allocation (-1 to convert to repay amount)
2310
+ // const perPoolDebtDeltasToBorrow = this._budget.vesuDebtDeltas;
2311
+ // const debtDeltasSum = this._sumDebtDeltas(perPoolDebtDeltasToBorrow);
2312
+ // if (debtDeltasSum.toNumber() > 0) {
2313
+ // vesuAllocationUsd = vesuAllocationUsd.plus(this._sumDebtDeltas(perPoolDebtDeltasToBorrow).multipliedBy(-1));
2314
+
2315
+ let vesuPositionDelta = Number((new Web3Number((vesuAllocationUsd.toNumber() * (vesuLeverage) / collateralPrice).toFixed(6), 6)).toFixedRoundDown(COLLATERAL_PRECISION));
2316
+ let extendedPositionDelta = Number((new Web3Number((extendedAllocationUsd.toNumber() * (extendedLeverage) / collateralPrice).toFixed(6), 6)).toFixedRoundDown(COLLATERAL_PRECISION));
2317
+
2318
+ // for the first call, reconsider the newly created position delta to actually recompute the allocation split
2319
+ if (!isRecursive) {
2320
+ return this._computeAllocationSplit(distributableAmount, vesuPositionDelta, extendedPositionDelta, true);
2321
+ }
2322
+
2323
+ // subtract any deltas passed
2324
+ // throw new Error(`${this._tag}: Recursive allocation vesuPositionDelta: ${vesuPositionDelta}, extendedPositionDelta: ${extendedPositionDelta}, vesuAllocationUsd: ${vesuAllocationUsd.toNumber()}, extendedAllocationUsd: ${extendedAllocationUsd.toNumber()}`);
2325
+ return { vesuAllocationUsd, extendedAllocationUsd, vesuPositionDelta, extendedPositionDelta };
2326
+ }
2327
+
2328
+ private _hasNoVesuAdapters(): boolean {
2329
+ return this._config.vesuAdapters.length === 0;
2330
+ }
2331
+
2332
+ // ═══════════════════════════════════════════════════════════════════════════
2333
+ // Private — solve step 4: per-pool collateral deltas
2334
+ // ═══════════════════════════════════════════════════════════════════════════
2335
+
2336
+ /**
2337
+ * Distributes the total Vesu USD allocation across pools proportionally
2338
+ * by existing collateral value, then converts each share to collateral
2339
+ * token units.
2340
+ */
2341
+ private _computePerPoolCollateralDeltas(
2342
+ vesuAllocationUsd: Web3Number
2343
+ ): VesuPoolDelta[] {
2344
+ const vesuLeverage = calculateVesuLeverage();
2345
+
2346
+ const availableVesuCollateralAllocationUsd = vesuAllocationUsd;
2347
+ const postLeverageAllocationUsd = availableVesuCollateralAllocationUsd.multipliedBy(vesuLeverage);
2348
+ const totalCollateralExisting = this._totalVesuCollateral();
2349
+
2350
+ return this._budget.vesuPools.map((pool, index) => {
2351
+ const _postLeverageAllocation = postLeverageAllocationUsd.dividedBy(pool.collateralPrice);
2352
+
2353
+ const postLeverageAllocation = new Web3Number((
2354
+ (
2355
+ _postLeverageAllocation.plus(totalCollateralExisting)
2356
+ ).toFixedRoundDown(COLLATERAL_PRECISION)
2357
+ ), pool.collateralToken.decimals
2358
+ ).minus(totalCollateralExisting);
2359
+ const _poolCollateralDelta = this._computePoolCollateralShare(
2360
+ pool,
2361
+ totalCollateralExisting,
2362
+ postLeverageAllocation,
2363
+ );
2364
+ const poolCollateralDelta = new Web3Number(
2365
+ _poolCollateralDelta.toFixed(COLLATERAL_PRECISION),
2366
+ pool.collateralToken.decimals,
2367
+ );
2368
+
2369
+ // the excess collateral should come from debt.
2370
+ const newDebt = (postLeverageAllocation.multipliedBy(pool.collateralPrice)).minus(availableVesuCollateralAllocationUsd).dividedBy(pool.debtPrice);
2371
+ return {
2372
+ poolId: pool.poolId,
2373
+ collateralToken: pool.collateralToken,
2374
+ debtToken: pool.debtToken,
2375
+ debtDelta: newDebt,
2376
+ collateralDelta: poolCollateralDelta,
2377
+ collateralPrice: pool.collateralPrice,
2378
+ debtPrice: pool.debtPrice,
2379
+ };
2380
+ });
2381
+ }
2382
+
2383
+ /**
2384
+ * Determines how much of the total Vesu allocation goes to a single pool.
2385
+ * Single-pool or zero-total cases get the entire allocation.
2386
+ * Multi-pool cases split proportionally by current collateral USD value.
2387
+ */
2388
+ private _computePoolCollateralShare(
2389
+ pool: VesuPoolState,
2390
+ totalCollateral: Web3Number,
2391
+ totalVesuAllocation: Web3Number,
2392
+ ): Web3Number {
2393
+ const isSinglePoolOrZeroTotal =
2394
+ this._budget.vesuPools.length === 1 ||
2395
+ totalCollateral.toNumber() === 0;
2396
+
2397
+ if (isSinglePoolOrZeroTotal) return totalVesuAllocation;
2398
+
2399
+ const poolWeight = pool.collateralAmount.dividedBy(totalCollateral);
2400
+
2401
+ return new Web3Number(
2402
+ totalVesuAllocation
2403
+ .multipliedBy(poolWeight)
2404
+ .toFixed(USDC_TOKEN_DECIMALS),
2405
+ USDC_TOKEN_DECIMALS,
2406
+ );
2407
+ }
2408
+
2409
+ // ═══════════════════════════════════════════════════════════════════════════
2410
+ // Private — solve step 6: extended position deltas
2411
+ // ═══════════════════════════════════════════════════════════════════════════
2412
+
2413
+ /**
2414
+ * Computes per-position exposure deltas for delta neutrality.
2415
+ * Target: total Extended short exposure = total projected Vesu collateral
2416
+ * exposure (current + collateral deltas).
2417
+ */
2418
+ private _computeExtendedPositionDeltas(
2419
+ vesuDeltas: VesuPoolDelta[],
2420
+ ): ExtendedPositionDelta[] {
2421
+ const targetExposure =
2422
+ this._computeTargetExtendedExposure(vesuDeltas);
2423
+ const currentExposure = this._totalExtendedExposure();
2424
+ logger.debug(
2425
+ `${this._tag}::_computeExtendedPositionDeltas targetExposure=${targetExposure.toNumber()}, currentExposure=${currentExposure.toNumber()}`,
2426
+ );
2427
+ const totalExposureDelta = new Web3Number(
2428
+ targetExposure
2429
+ .minus(currentExposure)
2430
+ .toFixed(USDC_TOKEN_DECIMALS),
2431
+ USDC_TOKEN_DECIMALS,
2432
+ );
2433
+
2434
+ if (this._hasNoExtendedPositions()) {
2435
+ return this._singleInstrumentDelta(totalExposureDelta);
2436
+ }
2437
+
2438
+ return this._distributeExposureDeltaAcrossPositions(totalExposureDelta);
2439
+ }
2440
+
2441
+ /**
2442
+ * Target Extended exposure = sum of (current collateral + collateralDelta)
2443
+ * * price, across all Vesu pools.
2444
+ */
2445
+ private _computeTargetExtendedExposure(
2446
+ vesuDeltas: VesuPoolDelta[],
2447
+ ): Web3Number {
2448
+ let totalExposureCollateral = new Web3Number(0, USDC_TOKEN_DECIMALS); // just some decimals is ok
2449
+
2450
+ for (let i = 0; i < this._budget.vesuPools.length; i++) {
2451
+ const pool = this._budget.vesuPools[i];
2452
+ const delta = vesuDeltas[i];
2453
+ logger.debug(
2454
+ `${this._tag}::_computeTargetExtendedExposure poolId=${pool.poolId.toString()}, collateralAmount=${pool.collateralAmount.toNumber()}, collateralDelta=${delta.collateralDelta.toNumber()}`,
2455
+ );
2456
+ const projectedCollateral = pool.collateralAmount.plus(
2457
+ delta.collateralDelta,
2458
+ );
2459
+ totalExposureCollateral = totalExposureCollateral.plus(
2460
+ projectedCollateral,
2461
+ );
2462
+ }
2463
+
2464
+ return new Web3Number(
2465
+ totalExposureCollateral.toFixed(USDC_TOKEN_DECIMALS),
2466
+ USDC_TOKEN_DECIMALS,
2467
+ );
2468
+ }
2469
+
2470
+ private _hasNoExtendedPositions(): boolean {
2471
+ return this._budget.extendedPositionsView.length === 0;
2472
+ }
2473
+
2474
+ /**
2475
+ * Creates a single-element delta array for the default instrument
2476
+ * when no Extended positions currently exist.
2477
+ */
2478
+ private _singleInstrumentDelta(
2479
+ delta: Web3Number,
2480
+ ): ExtendedPositionDelta[] {
2481
+ return [
2482
+ {
2483
+ instrument: this._config.extendedAdapter.config.extendedMarketName,
2484
+ delta: new Web3Number(
2485
+ delta.toFixedRoundDown(COLLATERAL_PRECISION),
2486
+ USDC_TOKEN_DECIMALS,
2487
+ ),
2488
+ },
2489
+ ];
2490
+ }
2491
+
2492
+ /**
2493
+ * Distributes a total exposure delta proportionally across existing
2494
+ * positions by their current USD value share.
2495
+ */
2496
+ private _distributeExposureDeltaAcrossPositions(
2497
+ totalDelta: Web3Number,
2498
+ ): ExtendedPositionDelta[] {
2499
+ const totalExposure = this._totalExtendedExposure();
2500
+
2501
+ return this._budget.extendedPositionsView.map((position) => {
2502
+ const share = this._positionExposureShareFraction(
2503
+ position,
2504
+ totalExposure,
2505
+ );
2506
+ return {
2507
+ instrument: position.instrument,
2508
+ delta: new Web3Number(
2509
+ totalDelta.multipliedBy(share).toFixedRoundDown(COLLATERAL_PRECISION),
2510
+ USDC_TOKEN_DECIMALS,
2511
+ ),
2512
+ };
2513
+ });
2514
+ }
2515
+
2516
+ /**
2517
+ * Returns the fraction (0–1) of total Extended exposure held by
2518
+ * a single position. Returns 1 when there is only one position
2519
+ * or when total exposure is zero.
2520
+ */
2521
+ private _positionExposureShareFraction(
2522
+ position: ExtendedPositionState,
2523
+ totalExposure: Web3Number,
2524
+ ): number {
2525
+ const isSingleOrZero =
2526
+ totalExposure.toNumber() === 0 ||
2527
+ this._budget.extendedPositionsView.length === 1;
2528
+ if (isSingleOrZero) return 1;
2529
+ return position.valueUsd.dividedBy(totalExposure).toNumber();
2530
+ }
2531
+
2532
+ // ═══════════════════════════════════════════════════════════════════════════
2533
+ // Private — solve step 7: extended deposit delta
2534
+ // ═══════════════════════════════════════════════════════════════════════════
2535
+
2536
+ /**
2537
+ * Net deposit change = allocation target − currently available for trade.
2538
+ * Positive = need to deposit more, negative = can withdraw excess.
2539
+ */
2540
+ private _computeExtendedDepositDelta(
2541
+ extendedAllocationUsd: Web3Number,
2542
+ ): Web3Number {
2543
+ const currentAvailableForTrade = new Web3Number(
2544
+ this._budget.extAvailTrade.toFixed(USDC_TOKEN_DECIMALS),
2545
+ USDC_TOKEN_DECIMALS,
2546
+ );
2547
+
2548
+ return new Web3Number(
2549
+ extendedAllocationUsd
2550
+ .minus(currentAvailableForTrade)
2551
+ .toFixed(USDC_TOKEN_DECIMALS),
2552
+ USDC_TOKEN_DECIMALS,
2553
+ );
2554
+ }
2555
+
2556
+ private _computeVesuDepositAmount(vesuDeltas: VesuPoolDelta[]): Web3Number {
2557
+ let totalVesuCollateral = new Web3Number(0, USDC_TOKEN_DECIMALS); // just some decimals is ok
2558
+ for (let i = 0; i < this._budget.vesuPools.length; i++) {
2559
+ const pool = this._budget.vesuPools[i];
2560
+ const delta = vesuDeltas[i];
2561
+ totalVesuCollateral = totalVesuCollateral.plus(delta.collateralDelta.multipliedBy(pool.collateralPrice));
2562
+ totalVesuCollateral = totalVesuCollateral.minus(delta.debtDelta.multipliedBy(pool.debtPrice));
2563
+ }
2564
+ return totalVesuCollateral;
2565
+ }
2566
+
2567
+ // ═══════════════════════════════════════════════════════════════════════════
2568
+ // Private — solve step 8: wallet delta
2569
+ // ═══════════════════════════════════════════════════════════════════════════
2570
+
2571
+ /**
2572
+ * The wallet is a pass-through for USDC flows between the vault allocator
2573
+ * and the Extended exchange.
2574
+ *
2575
+ * Positive = vault allocator → wallet → Extended (deposit)
2576
+ * Negative = Extended → wallet → vault allocator (withdrawal)
2577
+ */
2578
+ private _deriveWalletDelta(extendedDeposit: Web3Number): Web3Number {
2579
+ return extendedDeposit;
2580
+ }
2581
+
2582
+ // ═══════════════════════════════════════════════════════════════════════════
2583
+ // Private — solve step 9: bring liquidity
2584
+ // ═══════════════════════════════════════════════════════════════════════════
2585
+
2586
+ /**
2587
+ * The bringLiquidity amount is the USDC that the vault allocator must
2588
+ * transfer back to the vault contract for the user's withdrawal.
2589
+ * Equals the withdrawAmount passed into solve(); 0 during investment cycles.
2590
+ */
2591
+ private _computeBringLiquidityAmount(
2592
+ withdrawAmount: Web3Number,
2593
+ ): Web3Number {
2594
+ return withdrawAmount;
2595
+ }
2596
+
2597
+ // ═══════════════════════════════════════════════════════════════════════════
2598
+ // Private — case classification (modular)
2599
+ //
2600
+ // Cases are the source of truth. Each case carries its own routes
2601
+ // with amounts and state info. There is no global routes array —
2602
+ // consumers flatten case routes as needed.
2603
+ // ═══════════════════════════════════════════════════════════════════════════
2604
+
2605
+
2606
+ // ── ExecutionRoute-building helpers ─────────────────────────────────────────────
2607
+
2608
+ // ── Atomic route builders ────────────────────────────────────────────
2609
+
2610
+ private _buildVesuRepayRoutes(totalUsd: number, routes: ExecutionRoute[]): void {
2611
+ // Repay consumes VA USDC/asset; do not plan more than the budget VA holds after prior
2612
+ // routes (borrow→VA, Vesu→VA, wallet→VA, etc.). Otherwise repayVesuBorrowCapacity would
2613
+ // mutate Vesu debt while spendVaRawUsd could not fund it.
2614
+ const vaCap = this._budget.vaRawUsd;
2615
+ const repayUsd = Math.min(totalUsd, vaCap);
2616
+ if (repayUsd <= CASE_THRESHOLD_USD) return;
2617
+
2618
+ const { used, spendsByPool } = this._budget.repayVesuBorrowCapacity(repayUsd);
2619
+ for (const route of spendsByPool) {
2620
+ routes.push({ type: RouteType.VESU_REPAY as const, ...route, priority: routes.length });
2621
+ }
2622
+
2623
+ this._budget.spendVaRawUsd(used);
2624
+ }
2625
+
2626
+ private _buildVesuBorrowRoutes(
2627
+ totalUsd: number,
2628
+ routes: ExecutionRoute[],
2629
+ opts?: { maxBorrowUsd?: number },
2630
+ ): { routes: ExecutionRoute[], remaining: number } {
2631
+ let borrowable = this._budget.vesuBorrowCapacity;
2632
+ if (opts?.maxBorrowUsd !== undefined) {
2633
+ borrowable = Math.min(borrowable, Math.max(0, opts.maxBorrowUsd));
2634
+ }
2635
+ if (totalUsd <= CASE_THRESHOLD_USD) return { routes, remaining: totalUsd };
2636
+ if (borrowable <= CASE_THRESHOLD_USD) return { routes, remaining: totalUsd };
2637
+
2638
+ const borrowTarget = Math.min(totalUsd, borrowable);
2639
+ const { used, spendsByPool } = this._budget.spendVesuBorrowCapacity(borrowTarget);
2640
+ for (const route of spendsByPool) {
2641
+ routes.push({ type: RouteType.VESU_BORROW as const, ...route, priority: routes.length });
2642
+ }
2643
+
2644
+ this._budget.addToVA(used);
2645
+ return { routes, remaining: totalUsd - used };
2646
+ }
2647
+
2648
+ // private _buildExtendedDepositRoutes(totalUsd: number): TransferRoute[] {
2649
+ // const routes: TransferRoute[] = [];
2650
+ // let rem = totalUsd;
2651
+ // const walletBal = this._budget.walletBalance?.amount?.toNumber() ?? 0;
2652
+ // if (walletBal > 0 && rem > 0) {
2653
+ // const use = Math.min(walletBal, rem);
2654
+ // routes.push({ type: RouteType.WALLET_TO_EXTENDED, amount: safeUsdcWeb3Number(use), priority: routes.length });
2655
+ // this._budget.applyExtendedBalanceChange(use);
2656
+ // rem -= use;
2657
+ // }
2658
+ // if (rem > 0) {
2659
+ // routes.push({ type: RouteType.VA_TO_EXTENDED, amount: safeUsdcWeb3Number(rem), priority: routes.length });
2660
+ // this._budget.applyExtendedBalanceChange(rem);
2661
+ // }
2662
+ // return routes;
2663
+ // }
2664
+
2665
+ // private _buildVesuIncreaseLeverRoutes(vesuDeltas: VesuPoolDelta[]): VesuMultiplyRoute[] {
2666
+ // return vesuDeltas
2667
+ // .filter((d) => d.collateralDelta.greaterThan(0))
2668
+ // .map((d) => {
2669
+ // this._budget.applyVesuDelta(d.poolId, d.collateralDelta, d.debtDelta);
2670
+ // return {
2671
+ // type: RouteType.VESU_MULTIPLY_INCREASE_LEVER as const,
2672
+ // poolId: d.poolId,
2673
+ // collateralToken: d.collateralToken,
2674
+ // collateralAmount: d.collateralDelta,
2675
+ // debtToken: d.debtToken,
2676
+ // debtAmount: d.debtDelta,
2677
+ // priority: 0,
2678
+ // };
2679
+ // });
2680
+ // }
2681
+
2682
+ // private _buildVesuDecreaseLeverRoutes(vesuDeltas: Web3Number[]): VesuMultiplyRoute[] {
2683
+ // return vesuDeltas
2684
+ // .filter((d) => d.collateralDelta.isNegative())
2685
+ // .map((d) => {
2686
+ // this._budget.applyVesuDelta(d.poolId, d.collateralDelta, d.debtDelta);
2687
+ // return {
2688
+ // type: RouteType.VESU_MULTIPLY_DECREASE_LEVER as const,
2689
+ // poolId: d.poolId,
2690
+ // collateralToken: d.collateralToken,
2691
+ // collateralAmount: d.collateralDelta,
2692
+ // debtToken: d.debtToken,
2693
+ // debtAmount: d.debtDelta,
2694
+ // priority: 0,
2695
+ // };
2696
+ // });
2697
+ // }
2698
+
2699
+ private _getWalletToVARoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
2700
+ const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
2701
+ if (usableRaw > CASE_THRESHOLD_USD) {
2702
+ const walletUsed = this._budget.spendWallet(usableRaw);
2703
+ this._budget.addToVA(walletUsed);
2704
+ const route: TransferRoute = { type: RouteType.WALLET_TO_VA, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length };
2705
+ routes.push(route);
2706
+ return { routes, remaining: tryAmount - walletUsed };
2707
+ }
2708
+ return { routes, remaining: tryAmount };
2709
+ }
2710
+
2711
+ private _getWalletToEXTENDEDRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
2712
+ const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
2713
+ if (usableRaw > CASE_THRESHOLD_USD) {
2714
+ const walletUsed = this._budget.spendWallet(usableRaw);
2715
+ this._budget.addToExtAvailTrade(walletUsed);
2716
+ routes.push({ type: RouteType.WALLET_TO_EXTENDED, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length } as TransferRoute);
2717
+
2718
+ if (shouldAddWaitRoute) {
2719
+ routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
2720
+ }
2721
+ return { routes, remaining: tryAmount - walletUsed };
2722
+ }
2723
+ return { routes, remaining: tryAmount };
2724
+ }
2725
+
2726
+ private _getVAToEXTENDEDRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
2727
+ const usable = Math.min(tryAmount, this._budget.vaUsdcUsd);
2728
+ if (usable > CASE_THRESHOLD_USD) {
2729
+ const vaUsed = this._budget.spendVA(usable);
2730
+ this._budget.addToExtAvailTrade(vaUsed);
2731
+
2732
+ // add extended deposit route
2733
+ const route: TransferRoute = { type: RouteType.VA_TO_EXTENDED, amount: safeUsdcWeb3Number(vaUsed), priority: routes.length };
2734
+ routes.push(route);
2735
+
2736
+ // should follow up by a returning to wait route
2737
+ if (shouldAddWaitRoute) {
2738
+ routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
2739
+ }
2740
+ return { routes, remaining: tryAmount - vaUsed };
2741
+ }
2742
+ return { routes, remaining: tryAmount };
2743
+ }
2744
+
2745
+ private _getExtendedToWalletRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
2746
+ if (tryAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
2747
+ const rawCap = this._budget.extAvailWithdraw + Math.max(0, this._budget.extAvailUpnl);
2748
+ const rawSpend = Math.min(tryAmount, rawCap);
2749
+ if (rawSpend <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
2750
+ const rawOut = this._budget.spendExtAvailTrade(rawSpend);
2751
+ this._budget.addToWallet(Math.abs(rawOut));
2752
+ const route: TransferRoute = { type: RouteType.EXTENDED_TO_WALLET, amount: safeUsdcWeb3Number(rawSpend), priority: routes.length };
2753
+ routes.push(route);
2754
+
2755
+ if (shouldAddWaitRoute) {
2756
+ routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
2757
+ }
2758
+ return { routes, remaining: tryAmount - rawSpend };
2759
+ }
2760
+
2761
+ private _getWALLETToVARoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
2762
+ if (tryAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
2763
+ const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
2764
+ if (usableRaw <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
2765
+ const walletUsed = this._budget.spendWallet(usableRaw);
2766
+ this._budget.addToVA(walletUsed);
2767
+ const route: TransferRoute = { type: RouteType.WALLET_TO_VA, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length };
2768
+ routes.push(route);
2769
+ return { routes, remaining: tryAmount - walletUsed };
2770
+ }
2771
+
2772
+ private _getUpnlRoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
2773
+ const rawUpnl = this._budget.extAvailUpnl;
2774
+ const usableRaw = Math.min(tryAmount, rawUpnl);
2775
+ if (usableRaw <= 0) return { routes, remaining: tryAmount };
2776
+
2777
+ // if fails, ensure there is a way to choose the positio to use to create realised pnl
2778
+ // until then, only 1 position is supported
2779
+ this._budget.spendExtAvailUpnl(usableRaw);
2780
+ this._budget.addToExtAvailTrade(usableRaw);
2781
+ assert(this._budget.extendedPositionsView.length == 1, 'SolveBudget::_getUpnlRoute: extendedPositions length must be 1');
2782
+ routes.push({
2783
+ type: RouteType.REALISE_PNL,
2784
+ amount: safeUsdcWeb3Number(usableRaw),
2785
+ instrument: this._budget.extendedPositionsView[0].instrument,
2786
+ priority: routes.length,
2787
+ } as RealisePnlRoute);
2788
+ return { routes, remaining: tryAmount - usableRaw };
2789
+ }
2790
+
2791
+ // ── Sub-classifiers ────────────────────────────────────────────────────
2792
+ // Each sub-classifier builds routes directly from contextual data.
2793
+
2794
+ /**
2795
+ * 1. Withdrawal — source funds in priority order:
2796
+ * 1) VA balance 2) Wallet 3) Vesu borrow capacity
2797
+ * 4) Extended available-for-withdrawal + unrealised PnL
2798
+ * 5) Unwind positions on both sides (freed funds handled next cycle)
2799
+ */
2800
+ private _classifyWithdrawal(
2801
+ withdrawAmount: Web3Number
2802
+ ): SolveCaseEntry[] {
2803
+ if (!withdrawAmount.greaterThan(CASE_THRESHOLD_USD)) return [];
2804
+
2805
+ const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
2806
+ const routes: ExecutionRoute[] = [];
2807
+ let remaining = withdrawAmount.toNumber();
2808
+
2809
+ // ── Step 1: VA balance ────────────────────────────────────────────────
2810
+ // VA funds are already in the vault allocator — no transfer route needed.
2811
+ const vaUsed = this._budget.spendVA(remaining);
2812
+ remaining -= vaUsed;
2813
+
2814
+ let totalExtUsed = 0;
2815
+
2816
+ // ── Step 2: Wallet balance → VA ──────────────────────────────────────
2817
+ if (remaining > CASE_THRESHOLD_USD) {
2818
+ const { remaining: walletToVaRemaining } = this._getWALLETToVARoute(remaining, routes);
2819
+ remaining = walletToVaRemaining;
2820
+ }
2821
+
2822
+ // ── Step 3: Borrow from Vesu (positive debt delta = borrowable) ──────
2823
+ if (remaining > CASE_THRESHOLD_USD) {
2824
+ const { remaining: borrowRemaining } = this._buildVesuBorrowRoutes(remaining, routes);
2825
+ remaining = borrowRemaining;
2826
+ }
2827
+
2828
+ // ── Step 4: Extended available-for-withdrawal, then unrealised PnL ───
2829
+ if (remaining > CASE_THRESHOLD_USD) {
2830
+ const usableWithrawAmount = Math.min(remaining, this._budget.extAvailWithdraw);
2831
+ remaining -= usableWithrawAmount;
2832
+
2833
+ let upnlUsed = 0;
2834
+ if (remaining > CASE_THRESHOLD_USD) {
2835
+ const { remaining: upnlRemaining } = this._getUpnlRoute(remaining, routes);
2836
+ upnlUsed = remaining - upnlRemaining;
2837
+ remaining = upnlRemaining;
2838
+ }
2839
+
2840
+ // keep track of total extended used
2841
+ totalExtUsed = usableWithrawAmount + upnlUsed;
2842
+ }
2843
+
2844
+ // ── Step 5: Unwind positions ────────────────────────────────────────
2845
+ // Handle imbalance first (unwind from larger side), then equal unwind.
2846
+ // VESU_MULTIPLY_DECREASE_LEVER handles the BTC→USDC swap internally.
2847
+ if (remaining > CASE_THRESHOLD_USD) {
2848
+ assert(this._budget.vesuPools.length == 1, 'SolveBudget::_classifyWithdrawal: vesuPoolStates length must be 1');
2849
+ const vesuAdapter = this._config.vesuAdapters[0];
2850
+ const avgCollPrice = this._budget.vesuPools[0]?.collateralPrice ?? 1;
2851
+ const vesuLeverage = calculateVesuLeverage();
2852
+ const extLeverage = calculateExtendedLevergae();
2853
+ const freedPerBtcVesu = avgCollPrice / vesuLeverage;
2854
+ const freedPerBtcExt = avgCollPrice / extLeverage;
2855
+
2856
+ const vesuColBtc = this._budget.vesuPools[0].collateralAmount.toNumber();
2857
+ const extPosBtc = this._totalExtendedExposure().toNumber();
2858
+ let stillNeeded = remaining;
2859
+ let vesuBtcDelta = 0;
2860
+ let extBtcDelta = 0;
2861
+ let extFreed = 0;
2862
+
2863
+ const roundUpBtc = (x: number): number => {
2864
+ const factor = 10 ** COLLATERAL_PRECISION;
2865
+ return Math.ceil(x * factor) / factor;
2866
+ };
2867
+
2868
+ // Step 5a: Handle imbalance
2869
+ const diff = vesuColBtc - extPosBtc;
2870
+ let currentVesuBtc = vesuColBtc;
2871
+ let currentExtBtc = extPosBtc;
2872
+
2873
+ if (Math.abs(diff) > 1e-8) {
2874
+ if (diff > 0) {
2875
+ const btcRaw = stillNeeded / freedPerBtcVesu;
2876
+ const btc = Math.min(roundUpBtc(Math.min(Math.abs(diff), btcRaw, currentVesuBtc)), currentVesuBtc);
2877
+ vesuBtcDelta += btc;
2878
+ stillNeeded -= btc * freedPerBtcVesu;
2879
+ currentVesuBtc -= btc;
2880
+ } else {
2881
+ const btcRaw = stillNeeded / freedPerBtcExt;
2882
+ const btc = Math.min(roundUpBtc(Math.min(Math.abs(diff), btcRaw, currentExtBtc)), currentExtBtc);
2883
+ extBtcDelta += btc;
2884
+ extFreed += btc * freedPerBtcExt;
2885
+ stillNeeded -= btc * freedPerBtcExt;
2886
+ currentExtBtc -= btc;
2887
+ }
2888
+ }
2889
+
2890
+ // todo rereviewe logic on how neg upnl gets handled
2891
+ // esp when equity is below ideal margin, 0 ext withdraw,
2892
+ // and how does closing free capital.
2893
+
2894
+ // Step 5b: Equal unwind from both sides
2895
+ if (stillNeeded > CASE_THRESHOLD_USD) {
2896
+ const combinedFreed = freedPerBtcVesu + freedPerBtcExt;
2897
+ const maxBtc = Math.min(currentVesuBtc, currentExtBtc);
2898
+ const btcRaw = stillNeeded / combinedFreed;
2899
+ const btc = Math.min(roundUpBtc(Math.min(btcRaw, maxBtc)), maxBtc);
2900
+ vesuBtcDelta += btc;
2901
+ extBtcDelta += btc;
2902
+ extFreed += btc * freedPerBtcExt;
2903
+ }
2904
+
2905
+ const r6 = (n: number) => Number(n.toFixed(6));
2906
+
2907
+ // Emit VESU_MULTIPLY_DECREASE_LEVER (handles swap internally, freed funds go to VA)
2908
+ if (vesuBtcDelta > 0) {
2909
+ const totalVesuBtcSigned = -vesuBtcDelta;
2910
+ const targetLtv = 1 - 1 / vesuLeverage; // vesuLeverage = 1/(1-ltv), so ltv = 1-1/lev
2911
+ const debtDelta = r6(totalVesuBtcSigned * avgCollPrice * targetLtv);
2912
+ // Same convention as LTV rebalance: no separate margin BTC; full Vesu reduction is the swap leg.
2913
+ const marginBtc = 0;
2914
+ const swappedBtc = Number(vesuBtcDelta.toFixed(COLLATERAL_PRECISION));
2915
+
2916
+ routes.push({
2917
+ type: RouteType.VESU_MULTIPLY_DECREASE_LEVER,
2918
+ poolId: vesuAdapter.config.poolId,
2919
+ collateralToken: vesuAdapter.config.collateral,
2920
+ marginAmount: new Web3Number(marginBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
2921
+ swappedCollateralAmount: new Web3Number(swappedBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
2922
+ debtToken: vesuAdapter.config.debt,
2923
+ debtAmount: new Web3Number(debtDelta, USDC_TOKEN_DECIMALS),
2924
+ priority: routes.length,
2925
+ } as VesuMultiplyRoute);
2926
+ this._budget.applyVesuDelta(
2927
+ vesuAdapter.config.poolId, vesuAdapter.config.collateral, vesuAdapter.config.debt,
2928
+ new Web3Number(r6(totalVesuBtcSigned), USDC_TOKEN_DECIMALS),
2929
+ new Web3Number(debtDelta, USDC_TOKEN_DECIMALS),
2930
+ );
2931
+ this._budget.addToVA(vesuBtcDelta * freedPerBtcVesu);
2932
+ }
2933
+
2934
+ // Emit EXTENDED_DECREASE_LEVER
2935
+ if (extBtcDelta > 0) {
2936
+ routes.push({
2937
+ type: RouteType.EXTENDED_DECREASE_LEVER,
2938
+ amount: safeUsdcWeb3Number(-r6(extBtcDelta)),
2939
+ instrument,
2940
+ priority: routes.length,
2941
+ });
2942
+ this._budget.applyExtendedExposureDelta(
2943
+ instrument,
2944
+ safeUsdcWeb3Number(-r6(extBtcDelta)),
2945
+ avgCollPrice,
2946
+ );
2947
+ totalExtUsed += extFreed;
2948
+ }
2949
+ }
2950
+
2951
+ // Bridge extended funds to VA (availableForTrade → wallet → VA)
2952
+ if (totalExtUsed > CASE_THRESHOLD_USD) {
2953
+ this._getExtendedToWalletRoute(totalExtUsed, routes);
2954
+ this._getWALLETToVARoute(totalExtUsed, routes);
2955
+ }
2956
+
2957
+ // ── Bring liquidity for whatever was sourced in steps 1-4 ────────────
2958
+ routes.push({
2959
+ type: RouteType.BRING_LIQUIDITY,
2960
+ amount: withdrawAmount,
2961
+ priority: routes.length,
2962
+ });
2963
+ this._budget.spendVA(withdrawAmount.toNumber() - vaUsed);
2964
+
2965
+ routes.forEach((r, i) => { r.priority = i; });
2966
+
2967
+ return [{
2968
+ case: CASE_DEFINITIONS[CaseId.WITHDRAWAL_SIMPLE],
2969
+ additionalArgs: { amount: withdrawAmount },
2970
+ routes,
2971
+ }];
2972
+ }
2973
+
2974
+ /**
2975
+ * 2a. LTV Rebalance — Vesu side
2976
+ *
2977
+ * Triggered when vesuPerPoolDebtDeltasToBorrow sum is negative (high LTV, needs repayment).
2978
+ * Sources funds to VA so the subsequent deposit classifier can build the correct lever routes.
2979
+ *
2980
+ * Priority: 1) VA + Wallet (no routes) 2) Extended available-for-withdrawal
2981
+ * 3) Extended uPnL 4) Margin crisis (future)
2982
+ *
2983
+ * Design: accumulate all ext-to-wallet moves, add transfer routes at the end (principle #3).
2984
+ */
2985
+ /**
2986
+ * Unified LTV / exposure classifier: `rebalance()` drives both LTV (debt, margin,
2987
+ * funding) and Vesu↔Extended position alignment. There is no separate imbalance pass.
2988
+ * Computes both Vesu repay and Extended margin needs,
2989
+ * then builds all routes in a single pass with no duplicate transfers.
2990
+ *
2991
+ * Vesu repay priority: VA > Wallet > ExtAvl > ExtUpnl
2992
+ * Extended margin priority: Wallet > VA > VesuBorrow
2993
+ * Shared sources consumed by Vesu first (higher priority).
2994
+ */
2995
+ private _classifyLTV(): SolveCaseEntry[] {
2996
+ assert(this._budget.vesuPools.length === 1, `${this._tag}::_classifyLTV expects exactly one Vesu pool`);
2997
+ const d = rebalance(this._ltvRebalanceInputsFromBudget());
2998
+ if (this._isLtvRebalanceNoop(d)) return [];
2999
+
3000
+ logger.info(
3001
+ `${this._tag}::_classifyLTV deltas extPos=${d.dExtPosition} vesuPos=${d.dVesuPosition} `
3002
+ + `vesuDebt=${d.dVesuDebt} va=${d.dVaUsd} wallet=${d.dWalletUsd} borrow=${d.dVesuBorrowCapacity} `
3003
+ + `T=${d.dTransferVesuToExt}`,
3004
+ );
3005
+
3006
+ const routes = this._buildLtvRoutesFromRebalanceDeltas(d);
3007
+ if (routes.length === 0) return [];
3008
+
3009
+ const amountUsd =
3010
+ Math.abs(d.dVaUsd)
3011
+ + Math.abs(d.dWalletUsd)
3012
+ + Math.abs(d.dVesuBorrowCapacity)
3013
+ + Math.abs(d.dExtAvlWithdraw)
3014
+ + Math.abs(d.dExtUpnl)
3015
+ + Math.abs(d.dTransferVesuToExt);
3016
+
3017
+ routes.forEach((r, i) => { r.priority = i; });
3018
+ return [{
3019
+ case: CASE_DEFINITIONS[CaseId.MANAGE_LTV],
3020
+ additionalArgs: { amount: safeUsdcWeb3Number(amountUsd) },
3021
+ routes,
3022
+ }];
3023
+ }
3024
+
3025
+ private _ltvRebalanceInputsFromBudget() {
3026
+ const pool = this._budget.vesuPools[0];
3027
+ const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
3028
+ const extPosBtc = this._budget.extendedPositionsView
3029
+ .filter(p => p.instrument === instrument)
3030
+ .reduce((s, p) => s + Math.abs(p.size.toNumber()), 0);
3031
+ const targetHF = VesuConfig.maxLtv / VesuConfig.targetLtv;
3032
+
3033
+ return {
3034
+ ext: {
3035
+ positionBtc: extPosBtc,
3036
+ equity: this._budget.extendedBalanceView?.equity?.toNumber() ?? 0,
3037
+ avlWithdraw: this._budget.extAvailWithdraw,
3038
+ upnl: this._budget.extAvailUpnl,
3039
+ leverage: calculateExtendedLevergae(),
3040
+ },
3041
+ vesu: {
3042
+ positionBtc: pool.collateralAmount.toNumber(),
3043
+ debt: pool.debtAmount.toNumber(),
3044
+ debtPrice: pool.debtPrice,
3045
+ maxLTV: VesuConfig.maxLtv,
3046
+ targetHF,
3047
+ },
3048
+ btcPrice: pool.collateralPrice,
3049
+ funding: {
3050
+ vaUsd: this._budget.vaUsd,
3051
+ walletUsd: this._budget.walletUsd,
3052
+ vesuBorrowCapacity: this._budget.vesuBorrowCapacity,
3053
+ extAvlWithdraw: this._budget.extAvailWithdraw,
3054
+ extUpnl: this._budget.extAvailUpnl,
3055
+ },
3056
+ config: {
3057
+ positionPrecision: COLLATERAL_PRECISION,
3058
+ hfBuffer: 0.05,
3059
+ minRoutableUsd: CASE_THRESHOLD_USD,
3060
+ },
3061
+ };
3062
+ }
3063
+
3064
+ private _isLtvRebalanceNoop(d: RebalanceDeltas): boolean {
3065
+ const btcEps = 10 ** -COLLATERAL_PRECISION;
3066
+ const usdEps = CASE_THRESHOLD_USD;
3067
+ return (
3068
+ Math.abs(d.dExtPosition) < btcEps
3069
+ && Math.abs(d.dVesuPosition) < btcEps
3070
+ && Math.abs(d.dVesuDebt) < 1e-6
3071
+ && Math.abs(d.dExtAvlWithdraw) < usdEps
3072
+ && Math.abs(d.dExtUpnl) < usdEps
3073
+ && Math.abs(d.dVaUsd) < usdEps
3074
+ && Math.abs(d.dWalletUsd) < usdEps
3075
+ && Math.abs(d.dVesuBorrowCapacity) < usdEps
3076
+ && Math.abs(d.dTransferVesuToExt) < usdEps
3077
+ );
3078
+ }
3079
+
3080
+ /**
3081
+ * Turn pure rebalance() deltas into execution routes.
3082
+ * Order: Vesu multiply decrease → Extended decrease → aggregated transfers
3083
+ * (REALISE_PNL, EXTENDED_TO_WALLET + WAIT, WALLET_TO_VA, VESU_BORROW, VESU_REPAY, VA_TO_EXTENDED),
3084
+ * then Vesu multiply increase and Extended increase (need VA / Extended funded first).
3085
+ */
3086
+ private _buildLtvRoutesFromRebalanceDeltas(d: RebalanceDeltas): ExecutionRoute[] {
3087
+ const routes: ExecutionRoute[] = [];
3088
+ const pool = this._budget.vesuPools[0];
3089
+ const vesuAdapter = this._config.vesuAdapters[0];
3090
+ const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
3091
+ const price = pool.collateralPrice;
3092
+ const debtPrice = pool.debtPrice;
3093
+ const targetLtv = VesuConfig.targetLtv;
3094
+ const btcEps = 10 ** -COLLATERAL_PRECISION;
3095
+
3096
+ let multiplyDebtRepayUsd = 0;
3097
+
3098
+ logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas d=${JSON.stringify(d)}`);
3099
+
3100
+ // ── 1) Vesu multiply decrease (free collateral / repay) ─────────────
3101
+ if (d.dVesuPosition < -btcEps) {
3102
+ const xBtc = -d.dVesuPosition;
3103
+ // When Vesu sends USD to Extended (dTransferVesuToExt > 0), part of the BTC cut must be the
3104
+ // multiply "margin" leg so USDC lands in VA and can be routed VA_TO_EXTENDED (adapter uses sub_margin).
3105
+ const transferUsdFromVesu = Math.max(0, d.dTransferVesuToExt);
3106
+ let marginBtc = 0;
3107
+ let swappedBtc = Number(xBtc.toFixed(COLLATERAL_PRECISION));
3108
+ if (transferUsdFromVesu > CASE_THRESHOLD_USD && price > 0) {
3109
+ const marginCapFromTransfer = transferUsdFromVesu / price;
3110
+ marginBtc = Number(
3111
+ Math.min(xBtc, marginCapFromTransfer).toFixed(COLLATERAL_PRECISION),
3112
+ );
3113
+ swappedBtc = Number((xBtc - marginBtc).toFixed(COLLATERAL_PRECISION));
3114
+ }
3115
+ // Swap leg repays at most (swapped BTC notional in USD); any extra debt reduction
3116
+ // is a separate VESU_REPAY (see multiplyDebtRepayUsd / standaloneRepayUsd below).
3117
+ const swapLegMaxRepayUsd = swappedBtc * price * debtPrice;
3118
+ const debtUsdFallback = swappedBtc * price * targetLtv;
3119
+ let debtTokenDelta: number;
3120
+ if (d.dVesuDebt < 0) {
3121
+ const needRepayUsd = -d.dVesuDebt * debtPrice;
3122
+ const multiplyRepayUsd = Math.min(needRepayUsd, swapLegMaxRepayUsd);
3123
+ logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas ${JSON.stringify({
3124
+ needRepayUsd,
3125
+ multiplyRepayUsd,
3126
+ })}`);
3127
+ debtTokenDelta = -(multiplyRepayUsd / debtPrice);
3128
+ } else {
3129
+ debtTokenDelta = -debtUsdFallback;
3130
+ }
3131
+
3132
+ const debtAmtW3 = new Web3Number(debtTokenDelta.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
3133
+ multiplyDebtRepayUsd = Math.abs(debtTokenDelta) * debtPrice;
3134
+ logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas ${JSON.stringify({
3135
+ debtTokenDelta,
3136
+ debtUsdFallback,
3137
+ swapLegMaxRepayUsd,
3138
+ xBtc,
3139
+ marginBtc,
3140
+ swappedBtc,
3141
+ transferUsdFromVesu,
3142
+ debtPrice,
3143
+ targetLtv,
3144
+ multiplyDebtRepayUsd,
3145
+ })}`);
3146
+ routes.push({
3147
+ type: RouteType.VESU_MULTIPLY_DECREASE_LEVER,
3148
+ poolId: vesuAdapter.config.poolId,
3149
+ collateralToken: vesuAdapter.config.collateral,
3150
+ marginAmount: new Web3Number(marginBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
3151
+ swappedCollateralAmount: new Web3Number(swappedBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
3152
+ debtToken: vesuAdapter.config.debt,
3153
+ debtAmount: debtAmtW3,
3154
+ priority: routes.length,
3155
+ } as VesuMultiplyRoute);
3156
+ this._budget.applyVesuDelta(
3157
+ vesuAdapter.config.poolId,
3158
+ vesuAdapter.config.collateral,
3159
+ vesuAdapter.config.debt,
3160
+ new Web3Number((-xBtc).toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
3161
+ debtAmtW3,
3162
+ );
3163
+ if (transferUsdFromVesu > CASE_THRESHOLD_USD) {
3164
+ this._budget.addToVA(transferUsdFromVesu);
3165
+ }
3166
+ }
3167
+
3168
+ // ── 2) Extended decrease lever only (increase after VA→Ext funding) ─
3169
+ if (d.dExtPosition < -btcEps) {
3170
+ const amt = new Web3Number(d.dExtPosition.toFixed(COLLATERAL_PRECISION), 8);
3171
+ routes.push({
3172
+ type: RouteType.EXTENDED_DECREASE_LEVER,
3173
+ amount: amt,
3174
+ instrument,
3175
+ priority: routes.length,
3176
+ } as ExtendedLeverRoute);
3177
+ this._budget.applyExtendedExposureDelta(instrument, amt, price);
3178
+ }
3179
+
3180
+ // ── 3) Aggregated transfers (no WALLET_TO_EXTENDED; Ext only via VA) ─
3181
+ const negUpnl = Math.min(0, d.dExtUpnl);
3182
+ const negExtAvl = Math.min(0, d.dExtAvlWithdraw);
3183
+ let hadExtendedOut = false;
3184
+
3185
+ if (negUpnl < -CASE_THRESHOLD_USD) {
3186
+ this._getUpnlRoute(Math.abs(negUpnl), routes);
3187
+ hadExtendedOut = true;
3188
+ }
3189
+
3190
+ const extToWalletUsd = (negExtAvl < -CASE_THRESHOLD_USD ? Math.abs(negExtAvl) : 0)
3191
+ + (negUpnl < -CASE_THRESHOLD_USD ? Math.abs(negUpnl) : 0);
3192
+ logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas extToWalletUsd=${extToWalletUsd}, negExtAvl=${negExtAvl}, negUpnl=${negUpnl}`);
3193
+ if (extToWalletUsd > CASE_THRESHOLD_USD) {
3194
+ this._getExtendedToWalletRoute(extToWalletUsd, routes);
3195
+ hadExtendedOut = true;
3196
+ } else {
3197
+ // too small to withdraw from extended
3198
+ // reduce any vesu debt
3199
+ // else further repay from va will fail
3200
+ if (d.dVesuDebt < 0) {
3201
+ d.dVesuDebt += (negExtAvl < 0 && negExtAvl > -CASE_THRESHOLD_USD ? Math.abs(negExtAvl) : 0)
3202
+ + (negUpnl < 0 && negUpnl > -CASE_THRESHOLD_USD ? Math.abs(negUpnl) : 0);
3203
+ }
3204
+ }
3205
+
3206
+ logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas d=${JSON.stringify(d)}`);
3207
+
3208
+ const walletPull = Math.abs(Math.min(0, d.dWalletUsd));
3209
+ const walletToVaUsd = walletPull + extToWalletUsd;
3210
+ if (walletToVaUsd > CASE_THRESHOLD_USD) {
3211
+ this._getWALLETToVARoute(walletToVaUsd, routes);
3212
+ }
3213
+
3214
+ if (d.dVesuBorrowCapacity < -CASE_THRESHOLD_USD) {
3215
+ this._buildVesuBorrowRoutes(Math.abs(d.dVesuBorrowCapacity), routes);
3216
+ }
3217
+
3218
+ const totalDebtRepayUsd = d.dVesuDebt < 0 ? -d.dVesuDebt * debtPrice : 0;
3219
+ logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas totalDebtRepayUsd=${totalDebtRepayUsd}`);
3220
+ let standaloneRepayUsd = Math.max(0, totalDebtRepayUsd - multiplyDebtRepayUsd);
3221
+ logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas standaloneRepayUsd=${standaloneRepayUsd}`);
3222
+ if (standaloneRepayUsd > this._budget.vaRawUsd) {
3223
+ if (Math.abs(standaloneRepayUsd - this._budget.vaRawUsd) < CASE_THRESHOLD_USD) {
3224
+ standaloneRepayUsd = this._budget.vaRawUsd;
3225
+ } else {
3226
+ throw new Error(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas standaloneRepayUsd=${standaloneRepayUsd} > vaRawUsd=${this._budget.vaRawUsd}`);
3227
+ }
3228
+ }
3229
+ if (standaloneRepayUsd > CASE_THRESHOLD_USD) {
3230
+ this._buildVesuRepayRoutes(standaloneRepayUsd, routes);
3231
+ }
3232
+
3233
+ const posExtEq = Math.max(0, d.dExtAvlWithdraw);
3234
+ const vaToExtUsd = posExtEq > CASE_THRESHOLD_USD ? posExtEq : 0;
3235
+ if (vaToExtUsd > CASE_THRESHOLD_USD) {
3236
+ this._getVAToEXTENDEDRoute(vaToExtUsd, routes, hadExtendedOut);
3237
+ }
3238
+
3239
+ // ── Vesu / Extended increase lever: require prior VA funding & VA→Extended where applicable
3240
+ if (d.dVesuPosition > btcEps) {
3241
+ const vesuDepositAmount = new Web3Number(
3242
+ (d.dVesuPosition * price * (1 - targetLtv)).toFixed(USDC_TOKEN_DECIMALS),
3243
+ USDC_TOKEN_DECIMALS,
3244
+ );
3245
+ if (vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD) {
3246
+ // routes.push({
3247
+ // type: RouteType.AVNU_DEPOSIT_SWAP,
3248
+ // priority: routes.length,
3249
+ // fromToken: vesuAdapter.config.collateral.symbol,
3250
+ // fromAmount: vesuDepositAmount,
3251
+ // toToken: vesuAdapter.config.debt.symbol,
3252
+ // });
3253
+ }
3254
+ const collateralDelta = new Web3Number(
3255
+ d.dVesuPosition.toFixed(COLLATERAL_PRECISION),
3256
+ vesuAdapter.config.collateral.decimals,
3257
+ );
3258
+ const availableBorrowCapacity = this._budget.vesuBorrowCapacity;
3259
+ const externalDepositAmount = vesuDepositAmount.minus(
3260
+ new Web3Number(Math.min(availableBorrowCapacity, vesuDepositAmount.toNumber()).toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS),
3261
+ );
3262
+ const collPx = pool.collateralPrice || 1;
3263
+ const marginUsdAmount = externalDepositAmount.toNumber() * (pool.debtPrice ?? 1);
3264
+ const swappedAmount = new Web3Number(
3265
+ ((marginUsdAmount / collPx)).toFixed(6),
3266
+ vesuAdapter.config.collateral.decimals,
3267
+ );
3268
+ const debtDeltaTokens = new Web3Number(
3269
+ (d.dVesuDebt).toFixed(USDC_TOKEN_DECIMALS),
3270
+ USDC_TOKEN_DECIMALS,
3271
+ );
3272
+ routes.push({
3273
+ type: RouteType.VESU_MULTIPLY_INCREASE_LEVER,
3274
+ priority: routes.length,
3275
+ collateralToken: vesuAdapter.config.collateral,
3276
+ debtToken: vesuAdapter.config.debt,
3277
+ marginAmount: swappedAmount,
3278
+ swappedCollateralAmount: collateralDelta.minus(swappedAmount),
3279
+ debtAmount: debtDeltaTokens.plus(new Web3Number(Math.min(availableBorrowCapacity, vesuDepositAmount.toNumber()).toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS)),
3280
+ poolId: vesuAdapter.config.poolId,
3281
+ } as VesuMultiplyRoute);
3282
+ this._budget.applyVesuDelta(
3283
+ vesuAdapter.config.poolId,
3284
+ vesuAdapter.config.collateral,
3285
+ vesuAdapter.config.debt,
3286
+ collateralDelta,
3287
+ debtDeltaTokens,
3288
+ );
3289
+ this._budget.spendVA(marginUsdAmount);
3290
+ }
3291
+ if (d.dExtPosition > btcEps) {
3292
+ const amt = new Web3Number(d.dExtPosition.toFixed(COLLATERAL_PRECISION), 8);
3293
+ routes.push({
3294
+ type: RouteType.EXTENDED_INCREASE_LEVER,
3295
+ amount: amt,
3296
+ instrument,
3297
+ priority: routes.length,
3298
+ } as ExtendedLeverRoute);
3299
+ this._budget.applyExtendedExposureDelta(instrument, amt, price);
3300
+ }
3301
+
3302
+ return routes;
3303
+ }
3304
+
3305
+ // ── LTV Vesu route builders ───────────────────────────────────────────
3306
+
3307
+ /**
3308
+ * LTV_EXTENDED_PROFITABLE_AVAILABLE:
3309
+ * Extended has enough available-to-withdraw → withdraw, move to VA, repay Vesu.
3310
+ * Routes: [EXTENDED_TO_WALLET, WALLET_TO_VA, VESU_REPAY]
3311
+ */
3312
+ // private _buildLtvVesuRepayFromExtendedRoutes(amountUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
3313
+ // const routes: ExecutionRoute[] = [];
3314
+ // const amt = safeUsdcWeb3Number(amountUsd);
3315
+
3316
+ // // Withdraw from Extended → operator wallet
3317
+ // routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
3318
+ // this._budget.applyExtendedBalanceChange(-amountUsd);
3319
+
3320
+ // // Wait for Extended withdrawal to settle before using wallet funds
3321
+ // routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
3322
+
3323
+ // // Operator wallet → VA
3324
+ // routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
3325
+
3326
+ // // Repay Vesu debt from VA funds
3327
+ // routes.push(...this._buildVesuRepayRoutesss(amountUsd, vesuDeltas)); // wrong
3328
+
3329
+ // routes.forEach((r, i) => { r.priority = i; });
3330
+ // return routes;
3331
+ // }
3332
+
3333
+ /**
3334
+ * LTV_EXTENDED_PROFITABLE_REALIZE:
3335
+ * Extended has unrealised PnL → realise it first, then withdraw + repay.
3336
+ * Routes: [REALISE_PNL, EXTENDED_TO_WALLET, WALLET_TO_VA, VESU_REPAY]
3337
+ */
3338
+ // private _buildLtvVesuRepayRealiseRoutes(amountUsd: number, availWithdrawUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
3339
+ // const routes: ExecutionRoute[] = [];
3340
+ // const amt = safeUsdcWeb3Number(amountUsd);
3341
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
3342
+ // const upnlRequired = amountUsd - availWithdrawUsd;
3343
+
3344
+ // // Realise PnL to make funds available for withdrawal
3345
+ // routes.push({ type: RouteType.REALISE_PNL, amount: safeUsdcWeb3Number(upnlRequired), instrument, priority: routes.length });
3346
+
3347
+ // // Withdraw realised funds from Extended → operator wallet
3348
+ // routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
3349
+ // this._budget.applyExtendedBalanceChange(-amountUsd);
3350
+
3351
+ // // Wait for Extended withdrawal to settle
3352
+ // routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
3353
+
3354
+ // // Operator wallet → VA
3355
+ // routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
3356
+
3357
+ // // Repay Vesu debt from VA funds
3358
+ // routes.push(...this._buildVesuRepayRoutesss(amountUsd, vesuDeltas)); // wrong
3359
+
3360
+ // routes.forEach((r, i) => { r.priority = i; });
3361
+ // return routes;
3362
+ // }
3363
+
3364
+ /**
3365
+ * MARGIN_CRISIS_VESU:
3366
+ * Neither VA/Wallet nor Extended withdrawal covers the shortfall.
3367
+ * Temporarily increase Extended leverage to free margin, withdraw to VA, then
3368
+ * decrease Vesu lever to bring LTV back in range.
3369
+ * Routes: [CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE, EXTENDED_TO_WALLET, WALLET_TO_VA,
3370
+ * VESU_MULTIPLY_DECREASE_LEVER, EXTENDED_DECREASE_LEVER, CRISIS_UNDO_EXTENDED_MAX_LEVERAGE]
3371
+ */
3372
+ // private _buildMarginCrisisVesuRoutes(neededUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
3373
+ // const routes: ExecutionRoute[] = [];
3374
+ // const amt = safeUsdcWeb3Number(neededUsd);
3375
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
3376
+
3377
+ // // Temporarily boost Extended leverage to free margin
3378
+ // routes.push({ type: RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE, priority: routes.length });
3379
+
3380
+ // // Withdraw freed margin from Extended → wallet
3381
+ // routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
3382
+ // this._budget.applyExtendedBalanceChange(-neededUsd);
3383
+
3384
+ // // Wallet → VA
3385
+ // routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
3386
+
3387
+ // // Decrease Vesu leverage to reduce LTV
3388
+ // // todo to verify if this is correct.
3389
+ // routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
3390
+
3391
+ // // Decrease Extended exposure to match reduced Vesu position
3392
+ // // ! todo incorrect, to fix
3393
+ // routes.push({ type: RouteType.EXTENDED_DECREASE_LEVER, amount: safeUsdcWeb3Number(0), instrument, priority: routes.length });
3394
+ // this._budget.applyExtendedExposureDelta(instrument, safeUsdcWeb3Number(0));
3395
+
3396
+ // // Revert Extended leverage back to normal
3397
+ // routes.push({ type: RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE, priority: routes.length });
3398
+
3399
+ // routes.forEach((r, i) => { r.priority = i; });
3400
+ // return routes;
3401
+ // }
3402
+
3403
+ // _classifyLtvExtended has been merged into the unified _classifyLTV above.
3404
+
3405
+ // ── LTV Extended route builders ───────────────────────────────────────
3406
+
3407
+ /**
3408
+ * LTV_EXTENDED_HIGH_USE_VA_OR_WALLET:
3409
+ * VA/Wallet has funds → route them to Extended.
3410
+ * Routes: [VA_TO_EXTENDED, WALLET_TO_EXTENDED] (wallet-first, then VA for remainder)
3411
+ */
3412
+ // private _buildLtvExtendedDepositFromVARoutes(amountUsd: number): ExecutionRoute[] {
3413
+ // const routes: ExecutionRoute[] = this._buildExtendedDepositRoutes(amountUsd);
3414
+ // routes.forEach((r, i) => { r.priority = i; });
3415
+ // return routes;
3416
+ // }
3417
+
3418
+ /**
3419
+ * LTV_VESU_LOW_TO_EXTENDED:
3420
+ * Borrow USDC from Vesu, route through VA to Extended.
3421
+ * Routes: [VESU_BORROW, VA_TO_EXTENDED]
3422
+ */
3423
+ // private _buildLtvExtendedBorrowFromVesuRoutes(borrowUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
3424
+ // const routes: ExecutionRoute[] = [];
3425
+ // const amt = safeUsdcWeb3Number(borrowUsd);
3426
+
3427
+ // // Borrow USDC from Vesu (lands in VA)
3428
+ // const { routes: borrowRoutes } = this._buildVesuBorrowRoutes(borrowUsd, vesuDeltas);
3429
+ // routes.push(...borrowRoutes);
3430
+
3431
+ // // VA → Extended
3432
+ // routes.push({ type: RouteType.VA_TO_EXTENDED, amount: amt, priority: routes.length });
3433
+ // this._budget.applyExtendedBalanceChange(borrowUsd);
3434
+
3435
+ // routes.forEach((r, i) => { r.priority = i; });
3436
+ // return routes;
3437
+ // }
3438
+
3439
+ /**
3440
+ * MARGIN_CRISIS_EXTENDED:
3441
+ * Borrow beyond target HF on Vesu to free USDC, deposit to Extended,
3442
+ * then decrease Vesu/Extended lever, undo crisis leverage.
3443
+ * Routes: [CRISIS_BORROW_BEYOND_TARGET_HF, VA_TO_EXTENDED,
3444
+ * VESU_MULTIPLY_DECREASE_LEVER, EXTENDED_DECREASE_LEVER,
3445
+ * CRISIS_UNDO_EXTENDED_MAX_LEVERAGE]
3446
+ */
3447
+ // private _buildMarginCrisisExtendedRoutes(neededUsd: number, vesuDeltas: VesuPoolDelta[]): ExecutionRoute[] {
3448
+ // // ! todo need to verify this case properly
3449
+
3450
+ // const routes: ExecutionRoute[] = [];
3451
+ // const amt = safeUsdcWeb3Number(neededUsd);
3452
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
3453
+
3454
+ // // Borrow beyond normal target HF to free USDC
3455
+ // const pool = vesuDeltas.find((d) => d.debtDelta.greaterThan(0)) ?? vesuDeltas[0];
3456
+ // routes.push({
3457
+ // type: RouteType.CRISIS_BORROW_BEYOND_TARGET_HF,
3458
+ // poolId: pool?.poolId ?? ('' as unknown as ContractAddr),
3459
+ // token: pool?.debtToken?.symbol ?? this._config.assetToken.symbol,
3460
+ // amount: amt,
3461
+ // priority: routes.length,
3462
+ // });
3463
+
3464
+ // // Borrowed USDC (now in VA) → Extended
3465
+ // routes.push({ type: RouteType.VA_TO_EXTENDED, amount: amt, priority: routes.length });
3466
+ // this._budget.applyExtendedBalanceChange(neededUsd);
3467
+
3468
+ // // Wait for Extended deposit to be credited
3469
+ // routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
3470
+
3471
+ // // Decrease Vesu leverage
3472
+ // routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
3473
+
3474
+ // // Decrease Extended exposure to match
3475
+ // routes.push({ type: RouteType.EXTENDED_DECREASE_LEVER, amount: amt, instrument, priority: routes.length });
3476
+ // this._budget.applyExtendedExposureDelta(instrument, new Web3Number(amt.negated().toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS));
3477
+
3478
+ // // Revert crisis leverage
3479
+ // routes.push({ type: RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE, priority: routes.length });
3480
+
3481
+ // routes.forEach((r, i) => { r.priority = i; });
3482
+ // return routes;
3483
+ // }
3484
+
3485
+ // ! todo implement max lever amount per execution cycle
3486
+
3487
+ private _rebalanceFunds({
3488
+ extAvlWithdraw,
3489
+ extUpnl,
3490
+ vaUsd,
3491
+ walletUsd,
3492
+ vesuBorrowCapacity,
3493
+ vesuLeverage,
3494
+ extendedLeverage,
3495
+ }: Inputs): Deltas {
3496
+ const total = extAvlWithdraw + extUpnl + vaUsd + walletUsd + vesuBorrowCapacity;
3497
+
3498
+ // Target eco split
3499
+ const extendedTarget = (total) / (1 + extendedLeverage / vesuLeverage);
3500
+ const extendedInitial = extAvlWithdraw + extUpnl;
3501
+
3502
+ let delta = extendedTarget - extendedInitial;
3503
+
3504
+ // Initialize deltas (0 = no change)
3505
+ let dExtAvlWithdraw = 0,
3506
+ dExtUpnl = 0,
3507
+ dVaUsd = 0,
3508
+ dWalletUsd = 0,
3509
+ dVesuBorrowCapacity = 0,
3510
+ finalVaUsd = vaUsd,
3511
+ finalExtended = extAvlWithdraw + (Math.max(extUpnl, 0));
3512
+
3513
+ // --- Case 1: eco1 needs MORE funds (pull from eco2)
3514
+ if (delta > 0) {
3515
+ let need = delta;
3516
+ const eps = CASE_THRESHOLD_USD;
3517
+
3518
+ const takeWalletUsd = routableDrawAmount(walletUsd, need, eps);
3519
+ dWalletUsd -= takeWalletUsd;
3520
+ need -= takeWalletUsd;
3521
+
3522
+ const takeVaUsd = routableDrawAmount(vaUsd, need, eps);
3523
+ dVaUsd -= takeVaUsd;
3524
+ need -= takeVaUsd;
3525
+
3526
+ finalVaUsd -= takeVaUsd; // remove, bcz its sent to extended and wont be available for vesu multiply
3527
+ finalVaUsd += walletUsd - takeWalletUsd; // add remaining wallet usd to va
3528
+ // dWallet
3529
+
3530
+ const takeVesuBorrowCapacity = routableDrawAmount(vesuBorrowCapacity, need, eps);
3531
+ dVesuBorrowCapacity -= takeVesuBorrowCapacity;
3532
+ need -= takeVesuBorrowCapacity;
3533
+
3534
+ finalVaUsd += vesuBorrowCapacity - takeVesuBorrowCapacity; // add remaining vesu borrow capacity to va
3535
+
3536
+ // Received into eco1 → distribute proportionally into E1/E2
3537
+ const received = delta - need;
3538
+ const eco1Sum = extAvlWithdraw + extUpnl;
3539
+ finalExtended += received;
3540
+
3541
+ if (eco1Sum >= 0) {
3542
+ // any received amount is always given to extended avl withdaw only. upnl wont change
3543
+ dExtAvlWithdraw += received;
3544
+ } else {
3545
+ // Negative Extended "liquid" (withdraw + uPnL): cover uPnL deficit first, then avl withdraw.
3546
+ const hole = -eco1Sum;
3547
+ const fillUpnl = Math.min(received, hole);
3548
+ dExtUpnl += fillUpnl;
3549
+ dExtAvlWithdraw += received - fillUpnl;
3550
+ finalExtended -= fillUpnl;
3551
+ }
3552
+
3553
+ if (need > CASE_THRESHOLD_USD) {
3554
+ throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
3555
+ }
3556
+ }
3557
+
3558
+ // --- Case 2: eco2 needs MORE funds (push from eco1)
3559
+ else if (delta < 0) {
3560
+ let need = -delta;
3561
+
3562
+ const takeExtAvlWithdraw = Math.min(Math.max(extAvlWithdraw, 0), need);
3563
+ dExtAvlWithdraw -= takeExtAvlWithdraw;
3564
+
3565
+ const takeExtUpnl = Math.min(Math.max(extUpnl, 0), need);
3566
+ dExtUpnl -= takeExtUpnl;
3567
+
3568
+ const netDrawableAmount = takeExtAvlWithdraw + takeExtUpnl;
3569
+ if (netDrawableAmount > CASE_THRESHOLD_USD) {
3570
+ need -= netDrawableAmount;
3571
+ finalExtended -= netDrawableAmount;
3572
+ }
3573
+
3574
+ const sent = -delta - need;
3575
+
3576
+ // Distribute into eco2 proportionally (optional design choice)
3577
+ const eco2Sum = vaUsd + walletUsd + vesuBorrowCapacity;
3578
+
3579
+ const netWalletUsd = walletUsd < CASE_THRESHOLD_USD ? 0 : walletUsd;
3580
+ finalVaUsd += sent + netWalletUsd;
3581
+
3582
+ if (eco2Sum >= 0) {
3583
+ // all amount is sent to wallet only
3584
+ dWalletUsd += sent;
3585
+ } else {
3586
+ // dont think it can be negative
3587
+ throw new Error(`${this._tag}: Unexpected case`);
3588
+ }
3589
+
3590
+ if (need > CASE_THRESHOLD_USD) {
3591
+ throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
3592
+ }
3593
+ }
3594
+
3595
+ return { dExtAvlWithdraw, dExtUpnl, dVaUsd, dWalletUsd, dVesuBorrowCapacity, finalVaUsd, finalExtended, isExtendedToVesu: delta < 0 };
3596
+ }
3597
+
3598
+ private _scaleVesuPoolDeltasByFactor(deltas: VesuPoolDelta[], scale: number): VesuPoolDelta[] {
3599
+ if (scale >= 1 - 1e-15) return deltas;
3600
+ return deltas.map((d) => ({
3601
+ ...d,
3602
+ collateralDelta: new Web3Number(
3603
+ (d.collateralDelta.toNumber() * scale).toFixed(COLLATERAL_PRECISION),
3604
+ d.collateralToken.decimals,
3605
+ ),
3606
+ debtDelta: new Web3Number(
3607
+ (d.debtDelta.toNumber() * scale).toFixed(USDC_TOKEN_DECIMALS),
3608
+ USDC_TOKEN_DECIMALS,
3609
+ ),
3610
+ }));
3611
+ }
3612
+
3613
+ /**
3614
+ * 3. New Deposits / Excess Funds
3615
+ *
3616
+ * Computes how much exposure to create on Vesu and Extended, then
3617
+ * distributes available funds using shortest-path priority chains:
3618
+ * Extended margin: Wallet → VA → Vesu borrow
3619
+ * Vesu margin: VA → Wallet → Extended
3620
+ */
3621
+ /**
3622
+ * 3. New Deposits / Excess Funds
3623
+ *
3624
+ * Computes allocation split between Vesu and Extended, then sources
3625
+ * funds and creates lever-increase routes.
3626
+ *
3627
+ * Order: {@link _rebalanceFunds} first → project VA / Extended liquid after the same funding
3628
+ * routes (wallet→VA, borrow→VA, VA→Extended, Extended→wallet→VA) → ideal Vesu/Extended deltas
3629
+ * from distributable split → cap common BTC by min(Vesu fundable, Extended fundable) → scale
3630
+ * Vesu deltas and recompute Extended deltas so both sides stay matched.
3631
+ *
3632
+ * Fund flow (single pass — avoid VA→Extended then Extended→wallet round-trips):
3633
+ * 1) Treat Vesu borrow headroom that the multiply route will consume as covering
3634
+ * part of the Vesu USDC need (no standalone VESU_BORROW for that slice). Cap
3635
+ * standalone VESU_BORROW→VA→Extended by remaining headroom.
3636
+ * 2) Cover Extended deposit delta: REALISE_PNL first, then withdrawal→avail-trade,
3637
+ * then wallet→Extended, then VA→Extended only up to VA surplus above the USDC
3638
+ * that must remain for Vesu (after step 1), then borrow→VA→Extended.
3639
+ * 3) Cover Vesu VA shortfall: wallet→VA, Extended withdrawal→wallet→VA, REALISE_PNL,
3640
+ * then combined Extended→wallet→VA for the remainder.
3641
+ * 4) RETURN_TO_WAIT when needed; then AVNU + VESU_MULTIPLY + EXTENDED_INCREASE.
3642
+ */
3643
+ /**
3644
+ * @param skipAvnuDepositSwap Omit AVNU before Vesu multiply when LTV cases already ran this cycle
3645
+ * (matrix tests expect deposit routes without that step).
3646
+ */
3647
+ private _classifyDeposits(
3648
+ withdrawAmount: Web3Number,
3649
+ skipAvnuDepositSwap = false,
3650
+ ): SolveCaseEntry[] {
3651
+ if (withdrawAmount.toNumber() > CASE_THRESHOLD_USD) return [];
3652
+
3653
+ const distributableAmount = this._computeDistributableAmount(
3654
+ this._budget.vesuDebtDeltas, withdrawAmount,
3655
+ );
3656
+ if (distributableAmount.toNumber() <= CASE_THRESHOLD_USD) return [];
3657
+
3658
+ const vesuLeverage = calculateVesuLeverage();
3659
+ const extendedLeverage = calculateExtendedLevergae();
3660
+
3661
+ const { dExtAvlWithdraw, dExtUpnl, dVaUsd, dWalletUsd, dVesuBorrowCapacity, isExtendedToVesu, finalVaUsd, finalExtended } = this._rebalanceFunds({
3662
+ extAvlWithdraw: this._budget.extAvailWithdraw,
3663
+ extUpnl: this._budget.extAvailUpnl,
3664
+ vaUsd: this._budget.vaUsd,
3665
+ walletUsd: this._budget.walletUsd,
3666
+ vesuBorrowCapacity: this._budget.vesuBorrowCapacity,
3667
+ vesuLeverage,
3668
+ extendedLeverage,
3669
+ });
3670
+ logger.info(`${this._tag}::_classifyDeposits dExtAvlWithdraw=${dExtAvlWithdraw}, dExtUpnl=${dExtUpnl}, dVaUsd=${dVaUsd}, dWalletUsd=${dWalletUsd}, dVesuBorrowCapacity=${dVesuBorrowCapacity}, isExtendedToVesu=${isExtendedToVesu}, finalVaUsd=${finalVaUsd}`);
3671
+
3672
+ // const { vesuAllocationUsd } = this._computeAllocationSplit(distributableAmount);
3673
+
3674
+ let vesuDeltas = this._computePerPoolCollateralDeltas(new Web3Number(finalVaUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS));
3675
+ const collateralPrice = this._budget.vesuPools[0]?.collateralPrice ?? 0;
3676
+ const collateralDecimals = this._budget.vesuPools[0]?.collateralToken.decimals ?? 0;
3677
+ let _extendedPositionDelta = new Web3Number((finalExtended * extendedLeverage / collateralPrice).toFixed(USDC_TOKEN_DECIMALS), collateralDecimals).toFixedRoundDown(COLLATERAL_PRECISION);
3678
+ logger.info(`${this._tag}::_classifyDeposits extendedPositionDelta=${_extendedPositionDelta}`);
3679
+ logger.info(`${this._tag}::_classifyDeposits vesuDeltas=${JSON.stringify(vesuDeltas)}`);
3680
+ assert(vesuDeltas.length == 1, 'vesuDeltas should have only one delta');
3681
+ const minPositionDelta = Math.min(vesuDeltas[0].collateralDelta.toNumber(), Number(_extendedPositionDelta));
3682
+ logger.info(`${this._tag}::_classifyDeposits minPositionDelta=${minPositionDelta}`);
3683
+ vesuDeltas[0].collateralDelta = new Web3Number(minPositionDelta.toFixed(COLLATERAL_PRECISION), vesuDeltas[0].collateralDelta.decimals);
3684
+ const extendedPositionDeltas: ExtendedPositionDelta[] = [{
3685
+ instrument: this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD',
3686
+ delta: new Web3Number((minPositionDelta).toFixed(COLLATERAL_PRECISION), collateralDecimals),
3687
+ }];
3688
+ const vesuDepositAmount = this._computeVesuDepositAmount(vesuDeltas);
3689
+ logger.info(`${this._tag}::_classifyDeposits vesuDepositAmount=${vesuDepositAmount}`);
3690
+
3691
+ const routes: ExecutionRoute[] = [];
3692
+
3693
+ if (isExtendedToVesu) {
3694
+ // negative means to spend
3695
+ if (dExtUpnl < 0) {
3696
+ this._getUpnlRoute(Math.abs(dExtUpnl), routes);
3697
+ }
3698
+
3699
+ if (dExtUpnl < 0 || dExtAvlWithdraw < 0) {
3700
+ const netAmount = (dExtAvlWithdraw < 0 ? Math.abs(dExtAvlWithdraw) : 0) + (dExtUpnl < 0 ? Math.abs(dExtUpnl) : 0);
3701
+ const walletUsd = this._budget.walletUsd; // its important to put it here, to ensure below route's impact on walletUsd doesnt double count later
3702
+ this._getExtendedToWalletRoute(netAmount, routes);
3703
+ this._getWALLETToVARoute(netAmount + walletUsd, routes); // add wallet usd to send entire amount to va
3704
+ }
3705
+ } else {
3706
+ let netDVaUsd = dVaUsd;
3707
+ if (dWalletUsd < 0) {
3708
+ // always send entire amount to va (no use it being in wallet)
3709
+ this._getWalletToVARoute(this._budget.walletUsd, routes);
3710
+ netDVaUsd += dWalletUsd;
3711
+ }
3712
+
3713
+ if (dVesuBorrowCapacity < 0) {
3714
+ this._buildVesuBorrowRoutes(Math.abs(dVesuBorrowCapacity), routes);
3715
+ netDVaUsd += dVesuBorrowCapacity;
3716
+ }
3717
+
3718
+ if (netDVaUsd < 0) {
3719
+ this._getVAToEXTENDEDRoute(Math.abs(netDVaUsd), routes, true); // true means, add wait route always
3720
+ }
3721
+ }
3722
+
3723
+ // ── Vesu lever increase ─────────────────────────────────────────────
3724
+ for (const vesuDelta of vesuDeltas) {
3725
+ if (
3726
+ !skipAvnuDepositSwap &&
3727
+ vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD
3728
+ ) {
3729
+ // routes.push({
3730
+ // type: RouteType.AVNU_DEPOSIT_SWAP,
3731
+ // priority: routes.length,
3732
+ // fromToken: vesuDelta.collateralToken.symbol,
3733
+ // fromAmount: vesuDepositAmount,
3734
+ // toToken: vesuDelta.debtToken.symbol,
3735
+ // });
3736
+ }
3737
+ if (vesuDelta.collateralDelta.toNumber() > 0) {
3738
+ // removes borrowing capacity after excluding delta capacity that is being briddged out
3739
+ // this is bcz, since bnorrow capacity exists, no need for external margin amount
3740
+ const availableBorrowCapacity = this._budget.vesuBorrowCapacity;
3741
+ const externalDepositAmount = vesuDepositAmount.minus(availableBorrowCapacity);
3742
+ const swappedAmount = new Web3Number((externalDepositAmount.toNumber() * vesuDelta.debtPrice / (vesuDelta.collateralPrice ?? 0)).toFixed(6), vesuDelta.collateralToken.decimals);
3743
+ routes.push({
3744
+ type: RouteType.VESU_MULTIPLY_INCREASE_LEVER,
3745
+ priority: routes.length,
3746
+ collateralToken: vesuDelta.collateralToken,
3747
+ debtToken: vesuDelta.debtToken,
3748
+ marginAmount: swappedAmount, // should be the swapped amount as per vesu multiply adapter
3749
+ swappedCollateralAmount: vesuDelta.collateralDelta.minus(swappedAmount),
3750
+ debtAmount: vesuDelta.debtDelta.plus(availableBorrowCapacity),
3751
+ poolId: vesuDelta.poolId,
3752
+ } as VesuMultiplyRoute);
3753
+ }
3754
+ }
3755
+
3756
+ // ── Extended lever increase ────────────────────────────────────────
3757
+ for (const epDelta of extendedPositionDeltas) {
3758
+ if (epDelta.delta.toNumber() > 0) {
3759
+ routes.push({
3760
+ type: RouteType.EXTENDED_INCREASE_LEVER,
3761
+ priority: routes.length,
3762
+ instrument: epDelta.instrument,
3763
+ amount: epDelta.delta,
3764
+ } as ExtendedLeverRoute);
3765
+ }
3766
+ }
3767
+
3768
+ if (routes.length === 0) return [];
3769
+ routes.forEach((r, i) => { r.priority = i; });
3770
+ return [{
3771
+ case: CASE_DEFINITIONS[CaseId.DEPOSIT_FRESH_VAULT],
3772
+ additionalArgs: {
3773
+ amount: safeUsdcWeb3Number(distributableAmount.toNumber()),
3774
+ },
3775
+ routes,
3776
+ }];
3777
+ }
3778
+
3779
+ /** 4. Exposure Imbalance */
3780
+ // private _classifyImbalance(
3781
+ // vesuDeltas: VesuPoolDelta[],
3782
+ // ): SolveCaseEntry[] {
3783
+ // const extBtc = this._totalExtendedExposure().toNumber();
3784
+ // const vesuBtc = this._totalVesuCollateral().toNumber();
3785
+ // const maxBtc = Math.max(extBtc, vesuBtc);
3786
+ // if (maxBtc <= 0) return [];
3787
+ // const imb = extBtc - vesuBtc;
3788
+ // if (Math.abs(imb) / maxBtc <= IMBALANCE_THRESHOLD_FRACTION) return [];
3789
+ // const exposureDiffBtc = Math.abs(imb);
3790
+ // const rem = this._budget.totalUnused;
3791
+
3792
+ // if (imb > 0) {
3793
+ // // Extended excess short → need more Vesu collateral
3794
+ // if (rem > CASE_THRESHOLD_USD) {
3795
+ // return [{
3796
+ // case: CASE_DEFINITIONS[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS],
3797
+ // additionalArgs: {},
3798
+ // routes: this._buildImbalanceExtExcessShortHasFundsRoutes(rem, exposureDiffBtc, vesuDeltas),
3799
+ // }];
3800
+ // }
3801
+ // return [{
3802
+ // case: CASE_DEFINITIONS[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS],
3803
+ // additionalArgs: {},
3804
+ // routes: this._buildImbalanceExtExcessShortNoFundsRoutes(exposureDiffBtc),
3805
+ // }];
3806
+ // }
3807
+
3808
+ // // Vesu excess long → need more Extended exposure
3809
+ // if (rem > CASE_THRESHOLD_USD) {
3810
+ // return [{
3811
+ // case: CASE_DEFINITIONS[CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS],
3812
+ // additionalArgs: {},
3813
+ // routes: this._buildImbalanceVesuExcessLongHasFundsRoutes(rem, exposureDiffBtc, vesuDeltas),
3814
+ // }];
3815
+ // }
3816
+ // return [{
3817
+ // case: CASE_DEFINITIONS[CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS],
3818
+ // additionalArgs: {},
3819
+ // routes: this._buildImbalanceVesuExcessLongNoFundsRoutes(vesuDeltas),
3820
+ // }];
3821
+ // }
3822
+
3823
+ // ── Imbalance route builders ──────────────────────────────────────────
3824
+
3825
+ /**
3826
+ * IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS:
3827
+ * Extended has too much short exposure vs Vesu. Use VA/Wallet funds to
3828
+ * add Vesu collateral and reduce Extended exposure.
3829
+ *
3830
+ * The new Vesu collateral covers part of the imbalance; the remaining
3831
+ * gap is closed by reducing Extended short exposure.
3832
+ *
3833
+ * Routes: [AVNU_DEPOSIT_SWAP, VESU_MULTIPLY_INCREASE_LEVER, EXTENDED_DECREASE_LEVER]
3834
+ */
3835
+ // private _buildImbalanceExtExcessShortHasFundsRoutes(
3836
+ // fundsUsd: number, exposureDiffBtc: number, vesuDeltas: VesuPoolDelta[],
3837
+ // ): ExecutionRoute[] {
3838
+ // const routes: ExecutionRoute[] = [];
3839
+ // const collSym = this._config.collateralToken.symbol;
3840
+ // const debtSym = this._config.assetToken.symbol;
3841
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
3842
+
3843
+ // // Swap USDC → BTC for Vesu collateral
3844
+ // routes.push({
3845
+ // type: RouteType.AVNU_DEPOSIT_SWAP,
3846
+ // fromToken: debtSym,
3847
+ // fromAmount: safeUsdcWeb3Number(fundsUsd),
3848
+ // toToken: collSym,
3849
+ // priority: routes.length,
3850
+ // } as SwapRoute);
3851
+
3852
+ // // Increase Vesu lever (deposit collateral + borrow)
3853
+ // routes.push(...this._buildVesuIncreaseLeverRoutes(vesuDeltas));
3854
+
3855
+ // // New Vesu collateral covers part of the imbalance
3856
+ // const newVesuCollBtc = vesuDeltas
3857
+ // .filter((d) => d.collateralDelta.greaterThan(0))
3858
+ // .reduce((sum, d) => sum + d.collateralDelta.toNumber(), 0);
3859
+
3860
+ // // Remaining imbalance that needs to be closed on Extended
3861
+ // const extDecreaseExposureBtc = Math.max(0, exposureDiffBtc - newVesuCollBtc);
3862
+
3863
+ // if (extDecreaseExposureBtc > 0) {
3864
+ // const decDelta = new Web3Number(extDecreaseExposureBtc.toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
3865
+ // routes.push({
3866
+ // type: RouteType.EXTENDED_DECREASE_LEVER,
3867
+ // amount: decDelta,
3868
+ // instrument,
3869
+ // priority: routes.length,
3870
+ // });
3871
+ // this._budget.applyExtendedExposureDelta(instrument, new Web3Number(decDelta.negated().toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS));
3872
+
3873
+ // }
3874
+
3875
+ // routes.forEach((r, i) => { r.priority = i; });
3876
+ // return routes;
3877
+ // }
3878
+
3879
+ /**
3880
+ * IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS:
3881
+ * No available funds to add Vesu collateral → reduce Extended exposure
3882
+ * by the full imbalance amount.
3883
+ * Routes: [EXTENDED_DECREASE_LEVER]
3884
+ */
3885
+ private _buildImbalanceExtExcessShortNoFundsRoutes(exposureDiffBtc: number): ExecutionRoute[] {
3886
+ const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
3887
+ // hardcoding 8 decimals is ok here
3888
+ const decDelta = new Web3Number(Web3Number.fromNumber(exposureDiffBtc, 8).toFixedRoundDown(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
3889
+ const collPx = this._budget.vesuPools[0]?.collateralPrice ?? 1;
3890
+ this._budget.applyExtendedExposureDelta(
3891
+ instrument,
3892
+ new Web3Number(Web3Number.fromNumber(decDelta.negated().toNumber(), 8).toFixedRoundDown(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS),
3893
+ collPx,
3894
+ );
3895
+ return [{
3896
+ type: RouteType.EXTENDED_DECREASE_LEVER,
3897
+ amount: decDelta,
3898
+ instrument,
3899
+ priority: 0,
3900
+ }];
3901
+ }
3902
+
3903
+ /**
3904
+ * IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS:
3905
+ * Vesu has too much long exposure vs Extended. Deposit funds on Extended
3906
+ * to increase short exposure, and decrease Vesu lever for the remainder.
3907
+ *
3908
+ * Routes: [WALLET_TO_EXTENDED / VA_TO_EXTENDED, EXTENDED_INCREASE_LEVER,
3909
+ * VESU_MULTIPLY_DECREASE_LEVER]
3910
+ */
3911
+ // private _buildImbalanceVesuExcessLongHasFundsRoutes(
3912
+ // fundsUsd: number, exposureDiffBtc: number, vesuDeltas: VesuPoolDelta[],
3913
+ // ): ExecutionRoute[] {
3914
+ // const routes: ExecutionRoute[] = [];
3915
+ // const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
3916
+
3917
+ // // ExecutionRoute funds to Extended (wallet-first, then VA)
3918
+ // routes.push(...this._buildExtendedDepositRoutes(fundsUsd));
3919
+
3920
+ // // Compute how much new Extended exposure the deposited funds create
3921
+ // const extLeverage = calculateExtendedLevergae();
3922
+ // const newExtExposureUsd = fundsUsd * extLeverage;
3923
+ // const avgCollPrice = this._budget.vesuPoolStates[0]?.collateralPrice ?? 1;
3924
+ // const newExtExposureBtc = avgCollPrice > 0 ? newExtExposureUsd / avgCollPrice : 0;
3925
+
3926
+ // // Increase Extended lever by the new exposure (capped to imbalance)
3927
+ // const extIncreaseBtc = Math.min(newExtExposureBtc, exposureDiffBtc);
3928
+ // if (extIncreaseBtc > 0) {
3929
+ // const incDelta = new Web3Number(extIncreaseBtc.toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
3930
+ // routes.push({
3931
+ // type: RouteType.EXTENDED_INCREASE_LEVER,
3932
+ // amount: incDelta,
3933
+ // instrument,
3934
+ // priority: routes.length,
3935
+ // });
3936
+ // this._budget.applyExtendedExposureDelta(instrument, incDelta);
3937
+ // }
3938
+
3939
+ // // Remaining imbalance closed by decreasing Vesu lever
3940
+ // const vesuDecreaseBtc = Math.max(0, exposureDiffBtc - extIncreaseBtc);
3941
+ // if (vesuDecreaseBtc > 0) {
3942
+ // routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
3943
+ // }
3944
+
3945
+ // routes.forEach((r, i) => { r.priority = i; });
3946
+ // return routes;
3947
+ // }
3948
+
3949
+ /**
3950
+ * IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS:
3951
+ * No funds to increase Extended → decrease Vesu lever by the full imbalance.
3952
+ * Routes: [VESU_MULTIPLY_DECREASE_LEVER]
3953
+ */
3954
+ // private _buildImbalanceVesuExcessLongNoFundsRoutes(vesuDeltas: VesuPoolDelta[]): ExecutionRoute[] {
3955
+ // const routes: ExecutionRoute[] = this._buildVesuDecreaseLeverRoutes(vesuDeltas);
3956
+ // routes.forEach((r, i) => { r.priority = i; });
3957
+ // return routes;
3958
+ // }
3959
+
3960
+ // ── Main classifier (orchestrator) ─────────────────────────────────────
3961
+
3962
+ /**
3963
+ * Classifies the current state into actionable cases. Each case carries
3964
+ * its own execution routes with amounts and state info.
3965
+ */
3966
+ private _classifyCases(withdrawAmount: Web3Number): SolveCaseEntry[] {
3967
+ this._budget.initBudget();
3968
+
3969
+ // withdrawal uses full raw balances (no safety buffer)
3970
+ const withdrawalCases = this._classifyWithdrawal(withdrawAmount);
3971
+
3972
+ // apply safety buffer to remaining balances for subsequent classifiers
3973
+ this._budget.applyBuffer(this._config.limitBalanceBufferFactor);
3974
+
3975
+ // 2. LTV Rebalance — unified Vesu high LTV + Extended low margin
3976
+ const ltvCases = this._classifyLTV();
3977
+
3978
+ // 3. New Deposits — allocate remaining budget to lever operations
3979
+ const depositCases = this._classifyDeposits(
3980
+ withdrawAmount,
3981
+ ltvCases.length > 0,
3982
+ );
3983
+
3984
+ return [
3985
+ ...withdrawalCases,
3986
+ ...ltvCases,
3987
+ ...depositCases,
3988
+ ];
3989
+ }
3990
+
3991
+
3992
+
3993
+ // ═══════════════════════════════════════════════════════════════════════════
3994
+ // Private — aggregation helpers
3995
+ // ═══════════════════════════════════════════════════════════════════════════
3996
+
3997
+ private _totalVesuCollateral(): Web3Number {
3998
+ return this._budget.vesuPools.reduce(
3999
+ (acc, pool) =>
4000
+ acc.plus(
4001
+ pool.collateralAmount,
4002
+ ),
4003
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
4004
+ );
4005
+ }
4006
+
4007
+ private _totalVesuCollateralUsd(): Web3Number {
4008
+ return this._budget.vesuPools.reduce(
4009
+ (acc, pool) =>
4010
+ acc.plus(
4011
+ pool.collateralAmount.multipliedBy(pool.collateralPrice),
4012
+ ),
4013
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
4014
+ );
4015
+ }
4016
+
4017
+ private _totalExtendedExposure(): Web3Number {
4018
+ return this._budget.extendedPositionsView.reduce(
4019
+ (acc, position) => acc.plus(position.size),
4020
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
4021
+ );
4022
+ }
4023
+
4024
+ private _totalExtendedExposureUsd(): Web3Number {
4025
+ return this._budget.extendedPositionsView.reduce(
4026
+ (acc, position) => acc.plus(position.valueUsd),
4027
+ new Web3Number(0, USDC_TOKEN_DECIMALS),
4028
+ );
4029
+ }
4030
+
4031
+ // ═══════════════════════════════════════════════════════════════════════════
4032
+ // Private — logging
4033
+ // ═══════════════════════════════════════════════════════════════════════════
4034
+
4035
+ private _logSolveResult(result: SolveResult): void {
4036
+ // Log detected cases
4037
+ logger.info(
4038
+ `${this._tag}::solve detected ${result.cases.length} case(s): ` +
4039
+ result.cases
4040
+ .map((c) => `[${c.case.category}] ${c.case.id}`)
4041
+ .join(', '),
4042
+ );
4043
+ for (const entry of result.cases) {
4044
+ logger.info(
4045
+ `${this._tag}::solve case "${entry.case.title}" — ` +
4046
+ `steps: ${entry.case.steps.length}`,
4047
+ );
4048
+ }
4049
+
4050
+ logger.info(
4051
+ `${this._tag}::solve result — ` +
4052
+ `extendedDeposit: ${result.extendedDeposit.toNumber()}, ` +
4053
+ `vesuAllocUsd: ${result.vesuAllocationUsd.toNumber()}, ` +
4054
+ `extAllocUsd: ${result.extendedAllocationUsd.toNumber()}, ` +
4055
+ `bringLiquidity: ${result.bringLiquidityAmount.toNumber()}`,
4056
+ );
4057
+
4058
+ for (let i = 0; i < result.vesuDeltas.length; i++) {
4059
+ const delta = result.vesuDeltas[i];
4060
+ logger.info(
4061
+ `${this._tag}::solve vesu[${i}] ` +
4062
+ `pool=${delta.poolId.shortString()} ` +
4063
+ `debtDelta=${delta.debtDelta.toNumber()} ` +
4064
+ `collateralDelta=${delta.collateralDelta.toNumber()}`,
4065
+ );
4066
+ }
4067
+
4068
+ for (let i = 0; i < result.extendedPositionDeltas.length; i++) {
4069
+ const delta = result.extendedPositionDeltas[i];
4070
+ logger.info(
4071
+ `${this._tag}::solve extended[${i}] ` +
4072
+ `instrument=${delta.instrument} ` +
4073
+ `delta=${delta.delta.toNumber()}`,
4074
+ );
4075
+ }
4076
+
4077
+ for (const entry of result.cases) {
4078
+ const caseRoutes = entry.routes;
4079
+ logger.info(
4080
+ `${this._tag}::solve case "${entry.case.id}" routes (${caseRoutes.length}): ` +
4081
+ caseRoutes
4082
+ .map((r) => `[${r.priority}] ${r.type} ${routeSummary(r)}`)
4083
+ .join(' → '),
4084
+ );
4085
+ }
4086
+ }
4087
+ }