@defisaver/positions-sdk 2.1.144-dev → 2.1.145

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 (96) hide show
  1. package/cjs/config/contracts.d.ts +249 -0
  2. package/cjs/config/contracts.js +11 -1
  3. package/cjs/constants/index.d.ts +1 -0
  4. package/cjs/constants/index.js +2 -1
  5. package/cjs/contracts.d.ts +1510 -0
  6. package/cjs/contracts.js +3 -2
  7. package/cjs/fluid/index.js +6 -2
  8. package/cjs/helpers/fluidHelpers/index.js +5 -4
  9. package/cjs/helpers/index.d.ts +1 -0
  10. package/cjs/helpers/index.js +2 -1
  11. package/cjs/helpers/morphoMidnightHelpers/headroom.d.ts +48 -0
  12. package/cjs/helpers/morphoMidnightHelpers/headroom.js +52 -0
  13. package/cjs/helpers/morphoMidnightHelpers/index.d.ts +113 -0
  14. package/cjs/helpers/morphoMidnightHelpers/index.js +298 -0
  15. package/cjs/helpers/morphoMidnightHelpers/rate.d.ts +15 -0
  16. package/cjs/helpers/morphoMidnightHelpers/rate.js +62 -0
  17. package/cjs/helpers/morphoMidnightHelpers/tenor.d.ts +129 -0
  18. package/cjs/helpers/morphoMidnightHelpers/tenor.js +254 -0
  19. package/cjs/index.d.ts +2 -1
  20. package/cjs/index.js +3 -1
  21. package/cjs/markets/index.d.ts +1 -0
  22. package/cjs/markets/index.js +6 -1
  23. package/cjs/markets/morphoMidnight/index.d.ts +60 -0
  24. package/cjs/markets/morphoMidnight/index.js +659 -0
  25. package/cjs/morphoMidnight/index.d.ts +14 -0
  26. package/cjs/morphoMidnight/index.js +261 -0
  27. package/cjs/portfolio/discovery.js +4 -0
  28. package/cjs/portfolio/index.js +44 -0
  29. package/cjs/services/viem.d.ts +11 -11
  30. package/cjs/staking/eligibility.d.ts +15 -1
  31. package/cjs/staking/eligibility.js +12 -1
  32. package/cjs/staking/staking.js +18 -22
  33. package/cjs/types/index.d.ts +1 -0
  34. package/cjs/types/index.js +1 -0
  35. package/cjs/types/morphoMidnight.d.ts +141 -0
  36. package/cjs/types/morphoMidnight.js +47 -0
  37. package/cjs/types/portfolio.d.ts +4 -0
  38. package/esm/config/contracts.d.ts +249 -0
  39. package/esm/config/contracts.js +9 -0
  40. package/esm/constants/index.d.ts +1 -0
  41. package/esm/constants/index.js +1 -0
  42. package/esm/contracts.d.ts +1510 -0
  43. package/esm/contracts.js +1 -0
  44. package/esm/fluid/index.js +6 -2
  45. package/esm/helpers/fluidHelpers/index.js +6 -5
  46. package/esm/helpers/index.d.ts +1 -0
  47. package/esm/helpers/index.js +1 -0
  48. package/esm/helpers/morphoMidnightHelpers/headroom.d.ts +48 -0
  49. package/esm/helpers/morphoMidnightHelpers/headroom.js +45 -0
  50. package/esm/helpers/morphoMidnightHelpers/index.d.ts +113 -0
  51. package/esm/helpers/morphoMidnightHelpers/index.js +271 -0
  52. package/esm/helpers/morphoMidnightHelpers/rate.d.ts +15 -0
  53. package/esm/helpers/morphoMidnightHelpers/rate.js +50 -0
  54. package/esm/helpers/morphoMidnightHelpers/tenor.d.ts +129 -0
  55. package/esm/helpers/morphoMidnightHelpers/tenor.js +238 -0
  56. package/esm/index.d.ts +2 -1
  57. package/esm/index.js +2 -1
  58. package/esm/markets/index.d.ts +1 -0
  59. package/esm/markets/index.js +1 -0
  60. package/esm/markets/morphoMidnight/index.d.ts +60 -0
  61. package/esm/markets/morphoMidnight/index.js +631 -0
  62. package/esm/morphoMidnight/index.d.ts +14 -0
  63. package/esm/morphoMidnight/index.js +248 -0
  64. package/esm/portfolio/discovery.js +5 -1
  65. package/esm/portfolio/index.js +45 -1
  66. package/esm/services/viem.d.ts +11 -11
  67. package/esm/staking/eligibility.d.ts +15 -1
  68. package/esm/staking/eligibility.js +10 -0
  69. package/esm/staking/staking.js +19 -23
  70. package/esm/types/index.d.ts +1 -0
  71. package/esm/types/index.js +1 -0
  72. package/esm/types/morphoMidnight.d.ts +141 -0
  73. package/esm/types/morphoMidnight.js +44 -0
  74. package/esm/types/portfolio.d.ts +4 -0
  75. package/package.json +1 -1
  76. package/src/config/contracts.ts +9 -0
  77. package/src/constants/index.ts +1 -0
  78. package/src/contracts.ts +1 -0
  79. package/src/fluid/index.ts +6 -2
  80. package/src/helpers/fluidHelpers/index.ts +6 -5
  81. package/src/helpers/index.ts +1 -0
  82. package/src/helpers/morphoMidnightHelpers/headroom.ts +82 -0
  83. package/src/helpers/morphoMidnightHelpers/index.ts +485 -0
  84. package/src/helpers/morphoMidnightHelpers/rate.ts +71 -0
  85. package/src/helpers/morphoMidnightHelpers/tenor.ts +407 -0
  86. package/src/index.ts +2 -0
  87. package/src/markets/index.ts +3 -0
  88. package/src/markets/morphoMidnight/index.ts +675 -0
  89. package/src/morphoMidnight/index.ts +276 -0
  90. package/src/portfolio/discovery.ts +6 -0
  91. package/src/portfolio/index.ts +44 -0
  92. package/src/staking/eligibility.ts +29 -2
  93. package/src/staking/staking.ts +20 -22
  94. package/src/types/index.ts +1 -0
  95. package/src/types/morphoMidnight.ts +163 -0
  96. package/src/types/portfolio.ts +4 -0
@@ -0,0 +1,485 @@
1
+ import Dec from 'decimal.js';
2
+ import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
3
+ import {
4
+ calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos,
5
+ } from '../../moneymarket';
6
+ import { calculateNetApy } from '../../staking';
7
+ import {
8
+ LeverageType, MMAssetsData, MMUsedAsset, MMUsedAssets, NetworkNumber,
9
+ } from '../../types/common';
10
+ import {
11
+ MorphoMidnightAggregatedPositionData,
12
+ MorphoMidnightAssetsData,
13
+ MorphoMidnightBookOffer,
14
+ MorphoMidnightBookSide,
15
+ MorphoMidnightMarketData,
16
+ MorphoMidnightMarketInfo,
17
+ MorphoMidnightParsedBook,
18
+ } from '../../types';
19
+ import { WAD } from '../../constants';
20
+ import { LONGER_TIMEOUT } from '../../services/utils';
21
+ import { isTenorMidnightMarket } from '../../markets/morphoMidnight';
22
+ import {
23
+ buildMidnightParsedBook, midnightApyFromPrice, midnightBoundPrice, midnightTimeToMaturityDays,
24
+ } from './rate';
25
+ import {
26
+ getTenorBorrowQuote, getTenorMarketBook, getTenorPaybackQuote, getTenorPaybackUnitsQuote,
27
+ } from './tenor';
28
+
29
+ export {
30
+ buildMidnightParsedBook,
31
+ midnightApyFromPrice,
32
+ midnightBookBestFirst,
33
+ midnightBoundPrice,
34
+ midnightPriceFromApy,
35
+ midnightTimeToMaturityDays,
36
+ MIDNIGHT_DEFAULT_RATE_SLIPPAGE,
37
+ } from './rate';
38
+ export {
39
+ getMorphoMidnightBorrowHeadroom,
40
+ MIDNIGHT_MIN_HEALTH_RATIO,
41
+ } from './headroom';
42
+ export type { MidnightBorrowHeadroom, MidnightBorrowHeadroomParams } from './headroom';
43
+ export {
44
+ tenorBookKeyFor,
45
+ tenorBookRateToApyPercent,
46
+ tenorOfferFillToApiFill,
47
+ tenorOfferToApiOffer,
48
+ } from './tenor';
49
+
50
+ /**
51
+ * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
52
+ * the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
53
+ *
54
+ * Note on amounts: `borrowedUsd` is derived from the position's `debt`, which is the face value owed at
55
+ * maturity (principal + fixed interest). Health is therefore measured against the full maturity debt,
56
+ * matching how MidnightView computes `ratio`. Fixed-rate APY is not derived on-chain in MVP, so
57
+ * `netApy` reflects the `'0'` rates in `assetsData` (see the module getter).
58
+ */
59
+ export const getMorphoMidnightAggregatedPositionData = ({
60
+ usedAssets,
61
+ assetsData,
62
+ marketInfo,
63
+ }: {
64
+ usedAssets: MMUsedAssets,
65
+ assetsData: MorphoMidnightAssetsData,
66
+ marketInfo: MorphoMidnightMarketInfo,
67
+ }): MorphoMidnightAggregatedPositionData => {
68
+ const payload = {} as MorphoMidnightAggregatedPositionData;
69
+
70
+ payload.suppliedUsd = getAssetsTotal(usedAssets, ({ isSupplied }: { isSupplied: boolean }) => isSupplied, ({ suppliedUsd }: { suppliedUsd: string }) => suppliedUsd);
71
+ payload.suppliedCollateralUsd = getAssetsTotal(usedAssets, ({ isSupplied, collateral }: { isSupplied: boolean, collateral: boolean }) => isSupplied && collateral, ({ suppliedUsd }: { suppliedUsd: string }) => suppliedUsd);
72
+ payload.borrowedUsd = getAssetsTotal(usedAssets, ({ isBorrowed }: { isBorrowed: boolean }) => isBorrowed, ({ borrowedUsd }: { borrowedUsd: string }) => borrowedUsd);
73
+
74
+ // borrowLimit = Σ collateralUsd_i * lltv_i (per-collateral lltv carried on assetsData)
75
+ payload.borrowLimitUsd = getAssetsTotal(
76
+ usedAssets,
77
+ ({ isSupplied, collateral }: { isSupplied: boolean, collateral: boolean }) => isSupplied && collateral,
78
+ ({ symbol, suppliedUsd }: { symbol: string, suppliedUsd: string }) => new Dec(suppliedUsd).mul(assetsData[symbol]?.lltv || 0),
79
+ );
80
+ payload.liquidationLimitUsd = payload.borrowLimitUsd;
81
+
82
+ // Same subtraction every other money market uses, but it does NOT mean the same thing here. Elsewhere
83
+ // `borrowedUsd` is debt at present value, so the remainder is what a borrow would pay out. Midnight
84
+ // records debt at its face value at maturity, so this is face-value headroom: borrowing it would add
85
+ // more debt than the number says, by the market's discount. It also reserves no safety margin, and this
86
+ // aggregate is synchronous so it has no book rate to discount by.
87
+ //
88
+ // DO NOT SURFACE THIS AS "what you can borrow" — use `getMorphoMidnightBorrowHeadroom` instead, which
89
+ // applies both corrections (the margin, and the loan-per-unit price off a book rate) and caps on the
90
+ // depth the book can fill. The discount alone grows past 7% on a one-year market.
91
+ const leftToBorrowUsd = new Dec(payload.borrowLimitUsd).sub(payload.borrowedUsd);
92
+ payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
93
+
94
+ const loanTokenPrice = assetsData[marketInfo.loanToken]?.price || '0';
95
+ payload.leftToBorrow = new Dec(loanTokenPrice).eq(0) ? '0' : new Dec(payload.leftToBorrowUsd).div(loanTokenPrice).toString();
96
+
97
+ const { netApy, incentiveUsd, totalInterestUsd } = calculateNetApy({ usedAssets, assetsData: assetsData as unknown as MMAssetsData });
98
+ payload.netApy = netApy;
99
+ payload.incentiveUsd = incentiveUsd;
100
+ payload.totalInterestUsd = totalInterestUsd;
101
+
102
+ payload.ltv = new Dec(payload.suppliedCollateralUsd).eq(0) ? '0' : new Dec(payload.borrowedUsd).div(payload.suppliedCollateralUsd).toString();
103
+ payload.ratio = new Dec(payload.borrowedUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.borrowedUsd).mul(100).toString();
104
+ payload.healthRatio = new Dec(payload.borrowedUsd).eq(0) ? 'Infinity' : new Dec(payload.liquidationLimitUsd).div(payload.borrowedUsd).toDP(4).toString();
105
+
106
+ const { leveragedType, leveragedAsset } = isLeveragedPos(usedAssets);
107
+ payload.leveragedType = leveragedType;
108
+ payload.liquidationPrice = '';
109
+ if (leveragedType !== '') {
110
+ payload.leveragedAsset = leveragedAsset;
111
+ let assetPrice = assetsData[leveragedAsset].price;
112
+ if (leveragedType === LeverageType.VolatilePair) {
113
+ const borrowedAsset = (Object.values(usedAssets) as MMUsedAsset[]).find(({ borrowedUsd }: { borrowedUsd: string }) => +borrowedUsd > 0);
114
+ const borrowedAssetPrice = assetsData[borrowedAsset!.symbol].price;
115
+ const leveragedAssetPrice = assetsData[leveragedAsset].price;
116
+ const isReverse = new Dec(leveragedAssetPrice).lt(borrowedAssetPrice);
117
+ if (isReverse) {
118
+ payload.leveragedType = LeverageType.VolatilePairReverse;
119
+ payload.currentVolatilePairRatio = new Dec(borrowedAssetPrice).div(leveragedAssetPrice).toDP(18).toString();
120
+ assetPrice = new Dec(borrowedAssetPrice).div(assetPrice).toString();
121
+ } else {
122
+ assetPrice = new Dec(assetPrice).div(borrowedAssetPrice).toString();
123
+ payload.currentVolatilePairRatio = new Dec(leveragedAssetPrice).div(borrowedAssetPrice).toDP(18).toString();
124
+ }
125
+ }
126
+ payload.liquidationPrice = calcLeverageLiqPrice(payload.leveragedType, assetPrice, payload.borrowedUsd, payload.liquidationLimitUsd);
127
+ }
128
+
129
+ payload.minCollRatio = new Dec(payload.borrowLimitUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.borrowLimitUsd).mul(100).toString();
130
+ payload.collLiquidationRatio = new Dec(payload.liquidationLimitUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.liquidationLimitUsd).mul(100).toString();
131
+ payload.exposure = getExposure(payload.borrowedUsd, payload.suppliedUsd);
132
+
133
+ return payload;
134
+ };
135
+
136
+ // ── Off-chain order-book rate helpers ──────────────────────────────────────────────────────────────
137
+ // notion: https://app.notion.com/p/defisaver/Estimate-borrow-rate-and-slippage-before-execution-3a70be682adc80c783c8c11fdb761dd2
138
+ // the borrow rate is not exposed on-chain (MidnightView only stores total debt at maturity in `units`).
139
+ // derive the rate + interest from Morpho's public keyless Midnight API
140
+ // Quote prices are WAD-scaled
141
+ // loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
142
+
143
+ const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
144
+
145
+ // The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
146
+ // enough that markets drop out of the list for no reason.
147
+ const MIDNIGHT_BOOK_TIMEOUT = 30000;
148
+
149
+ // The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
150
+ // place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
151
+ const MIDNIGHT_SLIPPAGE_MIN = 0.1;
152
+ const MIDNIGHT_SLIPPAGE_MAX = 100;
153
+
154
+ interface MidnightPosition {
155
+ market_id: string,
156
+ type: string, // 'borrow' | 'lend'
157
+ debt: string, // raw loan-token base units — matches MidnightView.getPositionInfo exactly
158
+ cost_basis: string, // WAD-scaled raw base units — outstanding principal, net of exits/liquidations
159
+ effective_rate_wad: string, // WAD-scaled borrow APY, e.g. 0.05e18 = 5%
160
+ }
161
+
162
+ interface MidnightApiError {
163
+ code?: string,
164
+ message?: string,
165
+ details?: ({ field?: string, issue?: string })[] | null,
166
+ }
167
+
168
+ interface MidnightRawOffer {
169
+ price: string, // WAD-scaled loan-per-unit
170
+ assets: string, // loan-token base units available at this offer
171
+ }
172
+
173
+ interface MidnightQuoteResponse {
174
+ average_best_price?: string,
175
+ average_worst_price?: string,
176
+ available_assets?: string,
177
+ available_units?: string,
178
+ takeable_offers?: unknown[],
179
+ }
180
+
181
+ export interface MorphoMidnightBorrowInfo {
182
+ borrowRate: string, // effective borrow APY as a percent
183
+ debtBase: string, // outstanding principal (cost_basis), loan-token units
184
+ debtInterest: string, // debtTotal − debtBase (interest owed at maturity), loan-token units
185
+ debtTotal: string, // on-chain debt at maturity, loan-token units
186
+ }
187
+
188
+ export interface MorphoMidnightBorrowQuote {
189
+ bestPrice: string, // average_best_price, loan-per-unit
190
+ worstPrice: string, // average_worst_price, slippage-adjusted
191
+ estBorrowRate: string, // estimated borrow APY as a percent
192
+ maxRate: string, // borrow APY the on-chain cap permits, i.e. `maxUnits` annualized (display only)
193
+ newUnits: string, // debt added at best price, raw loan-token base units
194
+ maxUnits: string, // capped debt (on-chain cap), raw loan-token base units
195
+ availableAssets: string,
196
+ availableUnits: string,
197
+ takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
198
+ }
199
+
200
+ export interface MorphoMidnightPaybackQuote {
201
+ bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
202
+ worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
203
+ estPaybackRate: string, // APY the repayment retires debt at, as a percent
204
+ minRate: string, // APY the on-chain floor permits, i.e. `minUnits` annualized (display only)
205
+ newUnits: string, // debt retired at best price, raw loan-token base units
206
+ minUnits: string, // floor on debt retired (on-chain guard), raw loan-token base units
207
+ availableAssets: string,
208
+ availableUnits: string,
209
+ takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
210
+ }
211
+
212
+ // Payback quoted the other way round: the caller names the debt units to retire, and the quote prices
213
+ // what buying them costs. Same rate guard as the assets-target quote, expressed as a spend ceiling.
214
+ export interface MorphoMidnightPaybackUnitsQuote {
215
+ bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
216
+ worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
217
+ estPaybackRate: string, // APY the repayment retires debt at, as a percent
218
+ minRate: string, // APY the on-chain ceiling permits, i.e. `maxAssets` annualized (display only)
219
+ newAssets: string, // assets the target units cost at best price, raw loan-token base units
220
+ maxAssets: string, // ceiling on assets spent (on-chain guard), raw loan-token base units
221
+ availableAssets: string,
222
+ availableUnits: string,
223
+ takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
224
+ }
225
+
226
+ /**
227
+ * Coerce a slippage into what the quote endpoint accepts: 0.1–100 with at most one decimal place. The
228
+ * validation is lexical, so a computed value (`4.15066671050631467`) is rejected outright — without this
229
+ * the request 400s and the quote looks unavailable.
230
+ *
231
+ * Rounded **down**, since a wider slippage is a looser cap than the caller asked for.
232
+ */
233
+ export const midnightSlippageParam = (slippagePercent: Dec.Value): string => Dec.min(
234
+ Dec.max(new Dec(slippagePercent), MIDNIGHT_SLIPPAGE_MIN),
235
+ MIDNIGHT_SLIPPAGE_MAX,
236
+ ).toDP(1, Dec.ROUND_DOWN).toString();
237
+
238
+ /**
239
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
240
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
241
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
242
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
243
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
244
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
245
+ * The caller swallows errors — a missing rate must never block position rendering.
246
+ */
247
+ export const getMorphoMidnightUserBorrowInfo = async (
248
+ account: string,
249
+ marketId: string,
250
+ loanTokenSymbol: string,
251
+ ): Promise<MorphoMidnightBorrowInfo> => {
252
+ const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
253
+ const json: { data?: MidnightPosition[] } = await res.json();
254
+ const position = (json?.data || []).find((p) => p.type === 'borrow' && p.market_id?.toLowerCase() === marketId.toLowerCase());
255
+
256
+ const debtTotal = assetAmountInEth(position?.debt || '0', loanTokenSymbol);
257
+ const costBasisRaw = new Dec(position?.cost_basis || 0).div(WAD); // WAD-scaled → raw base units
258
+ const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
259
+ const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
260
+ const borrowRate = new Dec(position?.effective_rate_wad || 0).div(WAD).mul(100).toString();
261
+
262
+ return {
263
+ borrowRate, debtBase, debtInterest, debtTotal,
264
+ };
265
+ };
266
+
267
+ /**
268
+ * The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
269
+ * interest off the indexer, this carries that same split forward through a payback, from a debt of
270
+ * `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
271
+ * so both scale by the same fraction.
272
+ *
273
+ * Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
274
+ * `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
275
+ * in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
276
+ * payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
277
+ * payback it goes negative on a position that still owes. Callers detect that window as
278
+ * `debtBase + debtInterest !== debt`.
279
+ *
280
+ * Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
281
+ * it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
282
+ * rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
283
+ */
284
+ export const scaleMorphoMidnightDebtSplit = (
285
+ { debtBase, debtInterest }: Pick<MorphoMidnightBorrowInfo, 'debtBase' | 'debtInterest'>,
286
+ borrowedBefore: Dec.Value,
287
+ borrowedAfter: Dec.Value,
288
+ ): Pick<MorphoMidnightBorrowInfo, 'debtBase' | 'debtInterest'> => {
289
+ const fractionRemaining = new Dec(borrowedBefore).lte(0)
290
+ ? new Dec(0)
291
+ : Dec.max(0, new Dec(borrowedAfter)).div(borrowedBefore);
292
+
293
+ return {
294
+ debtBase: new Dec(debtBase).mul(fractionRemaining).toString(),
295
+ debtInterest: new Dec(debtInterest).mul(fractionRemaining).toString(),
296
+ };
297
+ };
298
+
299
+ /**
300
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
301
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
302
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
303
+ *
304
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
305
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
306
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
307
+ *
308
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
309
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
310
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
311
+ */
312
+ export const getMorphoMidnightMarketBook = async (
313
+ market: MorphoMidnightMarketData,
314
+ network: NetworkNumber,
315
+ side: MorphoMidnightBookSide = 'bids',
316
+ ): Promise<MorphoMidnightParsedBook | null> => {
317
+ if (isTenorMidnightMarket(market)) {
318
+ return getTenorMarketBook(market, network, side);
319
+ }
320
+
321
+ const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
322
+ const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
323
+ if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
324
+
325
+ const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
326
+ const ttmDays = midnightTimeToMaturityDays(market.maturity);
327
+
328
+ const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || []).map((offer) => ({
329
+ rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
330
+ liquidity: assetAmountInEth(offer.assets, loanSymbol),
331
+ }));
332
+
333
+ return buildMidnightParsedBook(offers, side);
334
+ };
335
+
336
+ // The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
337
+ // (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
338
+ // Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
339
+ const midnightQuoteError = (error?: MidnightApiError): string => {
340
+ const detail = (error?.details || []).map(({ issue }) => issue).filter(Boolean).join('; ');
341
+ const reason = detail || error?.message || error?.code;
342
+ return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
343
+ };
344
+
345
+ interface MidnightParsedQuote {
346
+ bestPrice: string, // loan-per-unit
347
+ worstPrice: string, // slippage-adjusted; below best on `bids`, above it on `asks`
348
+ availableAssets: string,
349
+ availableUnits: string,
350
+ takeableOffers: any[],
351
+ }
352
+
353
+ /**
354
+ * How much of the book to quote. The endpoint takes exactly one of the two — it rejects a request with
355
+ * neither ("Either assets or units must be provided") — and answers the same `takeable_offers` list either
356
+ * way, since that list is the whole in-band depth rather than the slice this size consumes.
357
+ */
358
+ type MidnightQuoteSize = { assets: string } | { units: string };
359
+
360
+ // The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
361
+ const fetchMorphoMidnightQuote = async (
362
+ marketId: string,
363
+ side: MorphoMidnightBookSide,
364
+ size: MidnightQuoteSize,
365
+ slippagePercent: Dec.Value,
366
+ ): Promise<MidnightParsedQuote> => {
367
+ const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
368
+ const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${midnightSlippageParam(slippagePercent)}`;
369
+ const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
370
+ const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
371
+ const d = json?.data;
372
+ if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
373
+
374
+ return {
375
+ bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
376
+ worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
377
+ availableAssets: d.available_assets || '0',
378
+ availableUnits: d.available_units || '0',
379
+ takeableOffers: d.takeable_offers || [],
380
+ };
381
+ };
382
+
383
+ export const getMorphoMidnightBorrowQuote = async (
384
+ marketId: string,
385
+ assetsRaw: string,
386
+ slippagePercent: Dec.Value,
387
+ maturity: number,
388
+ maxBorrowRate?: Dec.Value,
389
+ taker?: string,
390
+ rateSlippagePercent?: Dec.Value,
391
+ ): Promise<MorphoMidnightBorrowQuote> => {
392
+ if (isTenorMidnightMarket(marketId)) {
393
+ return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent);
394
+ }
395
+
396
+ const quote = await fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
397
+ const { bestPrice } = quote;
398
+ const ttmDays = midnightTimeToMaturityDays(maturity);
399
+ const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
400
+
401
+ // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
402
+ // `maxUnits` can never disagree about what the user is protected at.
403
+ const capPrice = midnightBoundPrice(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
404
+ const maxRate = midnightApyFromPrice(capPrice, ttmDays);
405
+ const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
406
+ const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
407
+
408
+ return {
409
+ ...quote,
410
+ estBorrowRate,
411
+ maxRate,
412
+ newUnits,
413
+ maxUnits,
414
+ };
415
+ };
416
+
417
+ export const getMorphoMidnightPaybackQuote = async (
418
+ marketId: string,
419
+ assetsRaw: string,
420
+ slippagePercent: Dec.Value,
421
+ maturity: number,
422
+ minPaybackRate?: Dec.Value,
423
+ taker?: string,
424
+ rateSlippagePercent?: Dec.Value,
425
+ ): Promise<MorphoMidnightPaybackQuote> => {
426
+ if (isTenorMidnightMarket(marketId)) {
427
+ return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
428
+ }
429
+
430
+ const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
431
+ const { bestPrice } = quote;
432
+ const ttmDays = midnightTimeToMaturityDays(maturity);
433
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
434
+
435
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
436
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
437
+ // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
438
+ // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
439
+ const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
440
+ const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
441
+
442
+ return {
443
+ ...quote,
444
+ estPaybackRate,
445
+ minRate,
446
+ newUnits,
447
+ minUnits,
448
+ };
449
+ };
450
+
451
+
452
+ export const getMorphoMidnightPaybackUnitsQuote = async (
453
+ marketId: string,
454
+ unitsRaw: string,
455
+ slippagePercent: Dec.Value,
456
+ maturity: number,
457
+ minPaybackRate?: Dec.Value,
458
+ taker?: string,
459
+ rateSlippagePercent?: Dec.Value,
460
+ ): Promise<MorphoMidnightPaybackUnitsQuote> => {
461
+ if (isTenorMidnightMarket(marketId)) {
462
+ return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
463
+ }
464
+
465
+ const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
466
+ const { bestPrice } = quote;
467
+ const ttmDays = midnightTimeToMaturityDays(maturity);
468
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
469
+
470
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
471
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
472
+ // Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
473
+ // what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
474
+ // buy short of the units it was asked for.
475
+ const newAssets = new Dec(unitsRaw).mul(bestPrice).toFixed(0, Dec.ROUND_UP);
476
+ const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
477
+
478
+ return {
479
+ ...quote,
480
+ estPaybackRate,
481
+ minRate,
482
+ newAssets,
483
+ maxAssets,
484
+ };
485
+ };
@@ -0,0 +1,71 @@
1
+ import Dec from 'decimal.js';
2
+ import { SECONDS_PER_DAY } from '../../constants';
3
+ import { MorphoMidnightBookOffer, MorphoMidnightBookSide, MorphoMidnightParsedBook } from '../../types';
4
+
5
+ const nowInSeconds = () => Math.floor(Date.now() / 1000);
6
+
7
+ // Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
8
+ export const midnightTimeToMaturityDays = (maturity: number, atSeconds: number = nowInSeconds()): number => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
9
+
10
+ // Annualize a fixed-term discount price into an APY percent: (1 / price)^(365 / ttmDays) − 1.
11
+ // `price` is loan-per-unit (assets received / units owed), so 1/price ≥ 1.
12
+ export const midnightApyFromPrice = (price: Dec.Value, ttmDays: Dec.Value): string => {
13
+ const p = new Dec(price);
14
+ const ttm = new Dec(ttmDays);
15
+ if (p.lte(0) || ttm.lte(0)) return '0';
16
+ return new Dec(1).div(p).pow(new Dec(365).div(ttm)).sub(1)
17
+ .mul(100)
18
+ .toString();
19
+ };
20
+
21
+ /**
22
+ * Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
23
+ * price = (1 + rate)^(−ttmDays / 365).
24
+ */
25
+ export const midnightPriceFromApy = (ratePercent: Dec.Value, ttmDays: Dec.Value): string => {
26
+ const rate = new Dec(ratePercent);
27
+ const ttm = new Dec(ttmDays);
28
+ if (rate.lte(0) || ttm.lte(0)) return '1';
29
+ return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
30
+ };
31
+
32
+ export const MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
33
+
34
+ const MIDNIGHT_MIN_FLOOR_RATIO = 0.5;
35
+
36
+ /** Which side of the estimate a guard sits on: a ceiling for borrows, a floor for paybacks. */
37
+ export type MidnightRateBoundKind = 'ceiling' | 'floor';
38
+
39
+ export const midnightBoundPrice = (
40
+ estRate: Dec.Value,
41
+ ttmDays: Dec.Value,
42
+ kind: MidnightRateBoundKind,
43
+ boundRate?: Dec.Value,
44
+ rateSlippagePercent: Dec.Value = MIDNIGHT_DEFAULT_RATE_SLIPPAGE,
45
+ ): string => {
46
+ if (boundRate !== undefined && new Dec(boundRate).gt(0)) return midnightPriceFromApy(boundRate, ttmDays);
47
+
48
+ const est = new Dec(estRate);
49
+ const slippage = new Dec(rateSlippagePercent);
50
+ const bound = kind === 'ceiling'
51
+ ? est.add(slippage)
52
+ : Dec.max(est.sub(slippage), est.mul(MIDNIGHT_MIN_FLOOR_RATIO));
53
+
54
+ return midnightPriceFromApy(bound, ttmDays);
55
+ };
56
+
57
+ export const midnightBookBestFirst = (side: MorphoMidnightBookSide): 1 | -1 => (side === 'asks' ? -1 : 1);
58
+
59
+ export const buildMidnightParsedBook = (
60
+ offers: MorphoMidnightBookOffer[],
61
+ side: MorphoMidnightBookSide,
62
+ ): MorphoMidnightParsedBook | null => {
63
+ const bestFirst = [...offers].sort((a, b) => new Dec(a.rate).minus(b.rate).mul(midnightBookBestFirst(side)).toNumber());
64
+ if (bestFirst.length === 0) return null;
65
+
66
+ return {
67
+ bestRate: bestFirst[0].rate,
68
+ totalLiquidity: bestFirst.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
69
+ offers: bestFirst,
70
+ };
71
+ };