@defisaver/positions-sdk 2.1.127-midnight-2-dev → 2.1.127-midnight-4-dev

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.
@@ -1,6 +1,6 @@
1
1
  import Dec from 'decimal.js';
2
- import { MMUsedAssets } from '../../types/common';
3
- import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketInfo } from '../../types';
2
+ import { MMUsedAssets, NetworkNumber } from '../../types/common';
3
+ import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightBookSide, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
4
4
  /**
5
5
  * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
6
6
  * the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
@@ -32,6 +32,17 @@ export interface MorphoMidnightBorrowQuote {
32
32
  availableUnits: string;
33
33
  takeableOffers: any[];
34
34
  }
35
+ export interface MorphoMidnightPaybackQuote {
36
+ bestPrice: string;
37
+ worstPrice: string;
38
+ estPaybackRate: string;
39
+ minRate: string;
40
+ newUnits: string;
41
+ minUnits: string;
42
+ availableAssets: string;
43
+ availableUnits: string;
44
+ takeableOffers: any[];
45
+ }
35
46
  export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
36
47
  export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
37
48
  /**
@@ -52,12 +63,29 @@ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec
52
63
  */
53
64
  export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
54
65
  /**
55
- * Current borrower rate + debt breakdown from the Midnight transactions API. On-chain we can only read the
56
- * total debt at maturity (`units`); the base-vs-interest split and the effective borrow rate require the
57
- * fill history. Per fill the rate is (units / seller_assets)^(365 / ttmAtFill) 1, weighted by base amount.
66
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
67
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
68
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... the fill history
69
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
70
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
71
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
58
72
  * The caller swallows errors — a missing rate must never block position rendering.
59
73
  */
60
- export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, maturity: number, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
74
+ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
75
+ /**
76
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
77
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
78
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
79
+ *
80
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
81
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
82
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
83
+ *
84
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
85
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
86
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
87
+ */
88
+ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
61
89
  /**
62
90
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
63
91
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -76,3 +104,22 @@ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId
76
104
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
77
105
  */
78
106
  export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
107
+ /**
108
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
109
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
110
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
111
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
112
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
113
+ * Throws if the book can't fill the amount (caller handles).
114
+ *
115
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
116
+ * so the guard is a floor rather than a ceiling:
117
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
118
+ * this when a user pins a min rate.
119
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
120
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
121
+ *
122
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
123
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
124
+ */
125
+ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.getMorphoMidnightBorrowQuote = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.midnightPriceFromApy = exports.midnightApyFromPrice = exports.midnightTimeToMaturityDays = exports.getMorphoMidnightAggregatedPositionData = void 0;
15
+ exports.getMorphoMidnightPaybackQuote = exports.getMorphoMidnightBorrowQuote = exports.getMorphoMidnightMarketBook = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.midnightPriceFromApy = exports.midnightApyFromPrice = exports.midnightTimeToMaturityDays = exports.getMorphoMidnightAggregatedPositionData = void 0;
16
16
  const decimal_js_1 = __importDefault(require("decimal.js"));
17
17
  const tokens_1 = require("@defisaver/tokens");
18
18
  const moneymarket_1 = require("../../moneymarket");
@@ -86,6 +86,9 @@ exports.getMorphoMidnightAggregatedPositionData = getMorphoMidnightAggregatedPos
86
86
  // loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
87
87
  const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
88
88
  const nowInSeconds = () => Math.floor(Date.now() / 1000);
89
+ // The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
90
+ // enough that markets drop out of the list for no reason.
91
+ const MIDNIGHT_BOOK_TIMEOUT = 30000;
89
92
  // The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
90
93
  // place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
91
94
  const MIDNIGHT_SLIPPAGE_MIN = 0.1;
@@ -131,39 +134,65 @@ exports.midnightPriceFromApy = midnightPriceFromApy;
131
134
  const midnightSlippageParam = (slippagePercent) => decimal_js_1.default.min(decimal_js_1.default.max(new decimal_js_1.default(slippagePercent), MIDNIGHT_SLIPPAGE_MIN), MIDNIGHT_SLIPPAGE_MAX).toDP(1, decimal_js_1.default.ROUND_DOWN).toString();
132
135
  exports.midnightSlippageParam = midnightSlippageParam;
133
136
  /**
134
- * Current borrower rate + debt breakdown from the Midnight transactions API. On-chain we can only read the
135
- * total debt at maturity (`units`); the base-vs-interest split and the effective borrow rate require the
136
- * fill history. Per fill the rate is (units / seller_assets)^(365 / ttmAtFill) 1, weighted by base amount.
137
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
138
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
139
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... the fill history
140
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
141
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
142
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
137
143
  * The caller swallows errors — a missing rate must never block position rendering.
138
144
  */
139
- const getMorphoMidnightUserBorrowInfo = (account, marketId, maturity, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
140
- const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/transactions`, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
145
+ const getMorphoMidnightUserBorrowInfo = (account, marketId, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
146
+ const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
141
147
  const json = yield res.json();
142
- const borrows = ((json === null || json === void 0 ? void 0 : json.data) || []).filter((t) => { var _a; return t.event_type === 'borrow' && ((_a = t.market_id) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === marketId.toLowerCase(); });
143
- let sumSeller = new decimal_js_1.default(0); // Σ seller_assets (base), raw
144
- let sumUnits = new decimal_js_1.default(0); // Σ units (debt at maturity), raw
145
- let weightedApy = new decimal_js_1.default(0); // Σ seller_assets × APYᵢ
146
- borrows.forEach((t) => {
147
- var _a, _b;
148
- const sellerAssets = new decimal_js_1.default(((_a = t.data) === null || _a === void 0 ? void 0 : _a.seller_assets) || 0);
149
- const units = new decimal_js_1.default(((_b = t.data) === null || _b === void 0 ? void 0 : _b.units) || 0);
150
- if (sellerAssets.lte(0) || units.lte(0))
151
- return;
152
- const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity, t.created_at);
153
- const apy = (0, exports.midnightApyFromPrice)(sellerAssets.div(units), ttmDays); // price = seller_assets / units
154
- sumSeller = sumSeller.add(sellerAssets);
155
- sumUnits = sumUnits.add(units);
156
- weightedApy = weightedApy.add(sellerAssets.mul(apy));
157
- });
158
- const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
159
- const debtBase = (0, tokens_1.assetAmountInEth)(sumSeller.toFixed(0), loanTokenSymbol);
160
- const debtTotal = (0, tokens_1.assetAmountInEth)(sumUnits.toFixed(0), loanTokenSymbol);
148
+ const position = ((json === null || json === void 0 ? void 0 : json.data) || []).find((p) => { var _a; return p.type === 'borrow' && ((_a = p.market_id) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === marketId.toLowerCase(); });
149
+ const debtTotal = (0, tokens_1.assetAmountInEth)((position === null || position === void 0 ? void 0 : position.debt) || '0', loanTokenSymbol);
150
+ const costBasisRaw = new decimal_js_1.default((position === null || position === void 0 ? void 0 : position.cost_basis) || 0).div(constants_1.WAD); // WAD-scaled raw base units
151
+ const debtBase = decimal_js_1.default.min((0, tokens_1.assetAmountInEth)(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
161
152
  const debtInterest = decimal_js_1.default.max(new decimal_js_1.default(debtTotal).sub(debtBase), 0).toString();
153
+ const borrowRate = new decimal_js_1.default((position === null || position === void 0 ? void 0 : position.effective_rate_wad) || 0).div(constants_1.WAD).mul(100).toString();
162
154
  return {
163
155
  borrowRate, debtBase, debtInterest, debtTotal,
164
156
  };
165
157
  });
166
158
  exports.getMorphoMidnightUserBorrowInfo = getMorphoMidnightUserBorrowInfo;
159
+ /**
160
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
161
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
162
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
163
+ *
164
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
165
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
166
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
167
+ *
168
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
169
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
170
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
171
+ */
172
+ const getMorphoMidnightMarketBook = (market_1, network_1, ...args_1) => __awaiter(void 0, [market_1, network_1, ...args_1], void 0, function* (market, network, side = 'bids') {
173
+ var _a;
174
+ const loanSymbol = (0, tokens_1.getAssetInfoByAddress)(market.loanToken, network).symbol;
175
+ const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
176
+ if (!res.ok)
177
+ throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
178
+ const json = yield res.json();
179
+ const ttmDays = (0, exports.midnightTimeToMaturityDays)(market.maturity);
180
+ const bestFirst = side === 'asks' ? -1 : 1;
181
+ const offers = (((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a[side]) || [])
182
+ .map((offer) => ({
183
+ rate: (0, exports.midnightApyFromPrice)(new decimal_js_1.default(offer.price).div(constants_1.WAD), ttmDays),
184
+ liquidity: (0, tokens_1.assetAmountInEth)(offer.assets, loanSymbol),
185
+ }))
186
+ .sort((a, b) => new decimal_js_1.default(a.rate).minus(b.rate).mul(bestFirst).toNumber());
187
+ if (offers.length === 0)
188
+ return null;
189
+ return {
190
+ bestRate: offers[0].rate,
191
+ totalLiquidity: offers.reduce((sum, offer) => sum.add(offer.liquidity), new decimal_js_1.default(0)).toString(),
192
+ offers,
193
+ };
194
+ });
195
+ exports.getMorphoMidnightMarketBook = getMorphoMidnightMarketBook;
167
196
  // The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
168
197
  // (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
169
198
  // Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
@@ -172,6 +201,22 @@ const midnightQuoteError = (error) => {
172
201
  const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
173
202
  return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
174
203
  };
204
+ // The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
205
+ const fetchMorphoMidnightQuote = (marketId, side, assetsRaw, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
206
+ const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?assets=${assetsRaw}&slippage=${(0, exports.midnightSlippageParam)(slippagePercent)}`;
207
+ const res = yield fetch(url, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
208
+ const json = yield res.json();
209
+ const d = json === null || json === void 0 ? void 0 : json.data;
210
+ if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
211
+ throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
212
+ return {
213
+ bestPrice: new decimal_js_1.default(d.average_best_price).div(constants_1.WAD).toString(),
214
+ worstPrice: new decimal_js_1.default(d.average_worst_price || 0).div(constants_1.WAD).toString(),
215
+ availableAssets: d.available_assets || '0',
216
+ availableUnits: d.available_units || '0',
217
+ takeableOffers: d.takeable_offers || [],
218
+ };
219
+ });
175
220
  /**
176
221
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
177
222
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -190,14 +235,8 @@ const midnightQuoteError = (error) => {
190
235
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
191
236
  */
192
237
  const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
193
- const url = `${MIDNIGHT_API_BASE}/books/${marketId}/bids/quote?assets=${assetsRaw}&slippage=${(0, exports.midnightSlippageParam)(slippagePercent)}`;
194
- const res = yield fetch(url, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
195
- const json = yield res.json();
196
- const d = json === null || json === void 0 ? void 0 : json.data;
197
- if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
198
- throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
199
- const bestPrice = new decimal_js_1.default(d.average_best_price).div(constants_1.WAD).toString();
200
- const worstPrice = new decimal_js_1.default(d.average_worst_price || 0).div(constants_1.WAD).toString();
238
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
239
+ const { bestPrice, worstPrice } = quote;
201
240
  const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
202
241
  const estBorrowRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
203
242
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
@@ -208,16 +247,46 @@ const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, matu
208
247
  const maxRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
209
248
  const newUnits = new decimal_js_1.default(bestPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(bestPrice).toFixed(0);
210
249
  const maxUnits = new decimal_js_1.default(capPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(capPrice).toFixed(0);
211
- return {
212
- bestPrice,
213
- worstPrice,
214
- estBorrowRate,
250
+ return Object.assign(Object.assign({}, quote), { estBorrowRate,
215
251
  maxRate,
216
252
  newUnits,
217
- maxUnits,
218
- availableAssets: d.available_assets || '0',
219
- availableUnits: d.available_units || '0',
220
- takeableOffers: d.takeable_offers || [],
221
- };
253
+ maxUnits });
222
254
  });
223
255
  exports.getMorphoMidnightBorrowQuote = getMorphoMidnightBorrowQuote;
256
+ /**
257
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
258
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
259
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
260
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
261
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
262
+ * Throws if the book can't fill the amount (caller handles).
263
+ *
264
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
265
+ * so the guard is a floor rather than a ceiling:
266
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
267
+ * this when a user pins a min rate.
268
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
269
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
270
+ *
271
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
272
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
273
+ */
274
+ const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
275
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', assetsRaw, slippagePercent);
276
+ const { bestPrice, worstPrice } = quote;
277
+ const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
278
+ const estPaybackRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
279
+ const capPrice = minPaybackRate !== undefined && new decimal_js_1.default(minPaybackRate).gt(0)
280
+ ? (0, exports.midnightPriceFromApy)(minPaybackRate, ttmDays)
281
+ : worstPrice;
282
+ const minRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
283
+ // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
284
+ // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
285
+ const newUnits = new decimal_js_1.default(bestPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(bestPrice).toFixed(0, decimal_js_1.default.ROUND_DOWN);
286
+ const minUnits = new decimal_js_1.default(capPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(capPrice).toFixed(0, decimal_js_1.default.ROUND_DOWN);
287
+ return Object.assign(Object.assign({}, quote), { estPaybackRate,
288
+ minRate,
289
+ newUnits,
290
+ minUnits });
291
+ });
292
+ exports.getMorphoMidnightPaybackQuote = getMorphoMidnightPaybackQuote;
@@ -168,7 +168,7 @@ function _getMorphoMidnightAccountData(provider, network, account, selectedMarke
168
168
  let assetsDataForApy = marketInfo.assetsData;
169
169
  if (new decimal_js_1.default(positionInfo.debt.toString()).gt(0)) {
170
170
  try {
171
- const borrowInfo = yield (0, morphoMidnightHelpers_1.getMorphoMidnightUserBorrowInfo)(account, marketId, marketInfo.maturity, marketInfo.loanToken);
171
+ const borrowInfo = yield (0, morphoMidnightHelpers_1.getMorphoMidnightUserBorrowInfo)(account, marketId, marketInfo.loanToken);
172
172
  borrowRate = borrowInfo.borrowRate;
173
173
  debtBase = borrowInfo.debtBase;
174
174
  debtInterest = borrowInfo.debtInterest;
@@ -60,6 +60,16 @@ export interface MorphoMidnightMarketInfo {
60
60
  utillization: string;
61
61
  assetsData: MorphoMidnightAssetsData;
62
62
  }
63
+ export type MorphoMidnightBookSide = 'bids' | 'asks';
64
+ export interface MorphoMidnightBookOffer {
65
+ rate: string;
66
+ liquidity: string;
67
+ }
68
+ export interface MorphoMidnightParsedBook {
69
+ bestRate: string;
70
+ totalLiquidity: string;
71
+ offers: MorphoMidnightBookOffer[];
72
+ }
63
73
  export interface MorphoMidnightAggregatedPositionData {
64
74
  suppliedUsd: string;
65
75
  suppliedCollateralUsd: string;
@@ -1,6 +1,6 @@
1
1
  import Dec from 'decimal.js';
2
- import { MMUsedAssets } from '../../types/common';
3
- import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketInfo } from '../../types';
2
+ import { MMUsedAssets, NetworkNumber } from '../../types/common';
3
+ import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightBookSide, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
4
4
  /**
5
5
  * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
6
6
  * the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
@@ -32,6 +32,17 @@ export interface MorphoMidnightBorrowQuote {
32
32
  availableUnits: string;
33
33
  takeableOffers: any[];
34
34
  }
35
+ export interface MorphoMidnightPaybackQuote {
36
+ bestPrice: string;
37
+ worstPrice: string;
38
+ estPaybackRate: string;
39
+ minRate: string;
40
+ newUnits: string;
41
+ minUnits: string;
42
+ availableAssets: string;
43
+ availableUnits: string;
44
+ takeableOffers: any[];
45
+ }
35
46
  export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
36
47
  export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
37
48
  /**
@@ -52,12 +63,29 @@ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec
52
63
  */
53
64
  export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
54
65
  /**
55
- * Current borrower rate + debt breakdown from the Midnight transactions API. On-chain we can only read the
56
- * total debt at maturity (`units`); the base-vs-interest split and the effective borrow rate require the
57
- * fill history. Per fill the rate is (units / seller_assets)^(365 / ttmAtFill) 1, weighted by base amount.
66
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
67
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
68
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... the fill history
69
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
70
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
71
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
58
72
  * The caller swallows errors — a missing rate must never block position rendering.
59
73
  */
60
- export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, maturity: number, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
74
+ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
75
+ /**
76
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
77
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
78
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
79
+ *
80
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
81
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
82
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
83
+ *
84
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
85
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
86
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
87
+ */
88
+ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
61
89
  /**
62
90
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
63
91
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -76,3 +104,22 @@ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId
76
104
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
77
105
  */
78
106
  export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
107
+ /**
108
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
109
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
110
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
111
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
112
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
113
+ * Throws if the book can't fill the amount (caller handles).
114
+ *
115
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
116
+ * so the guard is a floor rather than a ceiling:
117
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
118
+ * this when a user pins a min rate.
119
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
120
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
121
+ *
122
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
123
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
124
+ */
125
+ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import Dec from 'decimal.js';
11
- import { assetAmountInEth } from '@defisaver/tokens';
11
+ import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
12
12
  import { calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos, } from '../../moneymarket';
13
13
  import { calculateNetApy } from '../../staking';
14
14
  import { LeverageType, } from '../../types/common';
@@ -79,6 +79,9 @@ export const getMorphoMidnightAggregatedPositionData = ({ usedAssets, assetsData
79
79
  // loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
80
80
  const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
81
81
  const nowInSeconds = () => Math.floor(Date.now() / 1000);
82
+ // The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
83
+ // enough that markets drop out of the list for no reason.
84
+ const MIDNIGHT_BOOK_TIMEOUT = 30000;
82
85
  // The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
83
86
  // place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
84
87
  const MIDNIGHT_SLIPPAGE_MIN = 0.1;
@@ -120,38 +123,63 @@ export const midnightPriceFromApy = (ratePercent, ttmDays) => {
120
123
  */
121
124
  export const midnightSlippageParam = (slippagePercent) => Dec.min(Dec.max(new Dec(slippagePercent), MIDNIGHT_SLIPPAGE_MIN), MIDNIGHT_SLIPPAGE_MAX).toDP(1, Dec.ROUND_DOWN).toString();
122
125
  /**
123
- * Current borrower rate + debt breakdown from the Midnight transactions API. On-chain we can only read the
124
- * total debt at maturity (`units`); the base-vs-interest split and the effective borrow rate require the
125
- * fill history. Per fill the rate is (units / seller_assets)^(365 / ttmAtFill) 1, weighted by base amount.
126
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
127
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
128
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... the fill history
129
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
130
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
131
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
126
132
  * The caller swallows errors — a missing rate must never block position rendering.
127
133
  */
128
- export const getMorphoMidnightUserBorrowInfo = (account, marketId, maturity, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
129
- const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/transactions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
134
+ export const getMorphoMidnightUserBorrowInfo = (account, marketId, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
135
+ const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
130
136
  const json = yield res.json();
131
- const borrows = ((json === null || json === void 0 ? void 0 : json.data) || []).filter((t) => { var _a; return t.event_type === 'borrow' && ((_a = t.market_id) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === marketId.toLowerCase(); });
132
- let sumSeller = new Dec(0); // Σ seller_assets (base), raw
133
- let sumUnits = new Dec(0); // Σ units (debt at maturity), raw
134
- let weightedApy = new Dec(0); // Σ seller_assets × APYᵢ
135
- borrows.forEach((t) => {
136
- var _a, _b;
137
- const sellerAssets = new Dec(((_a = t.data) === null || _a === void 0 ? void 0 : _a.seller_assets) || 0);
138
- const units = new Dec(((_b = t.data) === null || _b === void 0 ? void 0 : _b.units) || 0);
139
- if (sellerAssets.lte(0) || units.lte(0))
140
- return;
141
- const ttmDays = midnightTimeToMaturityDays(maturity, t.created_at);
142
- const apy = midnightApyFromPrice(sellerAssets.div(units), ttmDays); // price = seller_assets / units
143
- sumSeller = sumSeller.add(sellerAssets);
144
- sumUnits = sumUnits.add(units);
145
- weightedApy = weightedApy.add(sellerAssets.mul(apy));
146
- });
147
- const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
148
- const debtBase = assetAmountInEth(sumSeller.toFixed(0), loanTokenSymbol);
149
- const debtTotal = assetAmountInEth(sumUnits.toFixed(0), loanTokenSymbol);
137
+ const position = ((json === null || json === void 0 ? void 0 : json.data) || []).find((p) => { var _a; return p.type === 'borrow' && ((_a = p.market_id) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === marketId.toLowerCase(); });
138
+ const debtTotal = assetAmountInEth((position === null || position === void 0 ? void 0 : position.debt) || '0', loanTokenSymbol);
139
+ const costBasisRaw = new Dec((position === null || position === void 0 ? void 0 : position.cost_basis) || 0).div(WAD); // WAD-scaled raw base units
140
+ const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
150
141
  const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
142
+ const borrowRate = new Dec((position === null || position === void 0 ? void 0 : position.effective_rate_wad) || 0).div(WAD).mul(100).toString();
151
143
  return {
152
144
  borrowRate, debtBase, debtInterest, debtTotal,
153
145
  };
154
146
  });
147
+ /**
148
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
149
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
150
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
151
+ *
152
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
153
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
154
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
155
+ *
156
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
157
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
158
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
159
+ */
160
+ export const getMorphoMidnightMarketBook = (market_1, network_1, ...args_1) => __awaiter(void 0, [market_1, network_1, ...args_1], void 0, function* (market, network, side = 'bids') {
161
+ var _a;
162
+ const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
163
+ const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
164
+ if (!res.ok)
165
+ throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
166
+ const json = yield res.json();
167
+ const ttmDays = midnightTimeToMaturityDays(market.maturity);
168
+ const bestFirst = side === 'asks' ? -1 : 1;
169
+ const offers = (((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a[side]) || [])
170
+ .map((offer) => ({
171
+ rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
172
+ liquidity: assetAmountInEth(offer.assets, loanSymbol),
173
+ }))
174
+ .sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
175
+ if (offers.length === 0)
176
+ return null;
177
+ return {
178
+ bestRate: offers[0].rate,
179
+ totalLiquidity: offers.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
180
+ offers,
181
+ };
182
+ });
155
183
  // The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
156
184
  // (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
157
185
  // Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
@@ -160,6 +188,22 @@ const midnightQuoteError = (error) => {
160
188
  const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
161
189
  return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
162
190
  };
191
+ // The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
192
+ const fetchMorphoMidnightQuote = (marketId, side, assetsRaw, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
193
+ const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?assets=${assetsRaw}&slippage=${midnightSlippageParam(slippagePercent)}`;
194
+ const res = yield fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
195
+ const json = yield res.json();
196
+ const d = json === null || json === void 0 ? void 0 : json.data;
197
+ if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
198
+ throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
199
+ return {
200
+ bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
201
+ worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
202
+ availableAssets: d.available_assets || '0',
203
+ availableUnits: d.available_units || '0',
204
+ takeableOffers: d.takeable_offers || [],
205
+ };
206
+ });
163
207
  /**
164
208
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
165
209
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -178,14 +222,8 @@ const midnightQuoteError = (error) => {
178
222
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
179
223
  */
180
224
  export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
181
- const url = `${MIDNIGHT_API_BASE}/books/${marketId}/bids/quote?assets=${assetsRaw}&slippage=${midnightSlippageParam(slippagePercent)}`;
182
- const res = yield fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
183
- const json = yield res.json();
184
- const d = json === null || json === void 0 ? void 0 : json.data;
185
- if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
186
- throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
187
- const bestPrice = new Dec(d.average_best_price).div(WAD).toString();
188
- const worstPrice = new Dec(d.average_worst_price || 0).div(WAD).toString();
225
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
226
+ const { bestPrice, worstPrice } = quote;
189
227
  const ttmDays = midnightTimeToMaturityDays(maturity);
190
228
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
191
229
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
@@ -196,15 +234,44 @@ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercen
196
234
  const maxRate = midnightApyFromPrice(capPrice, ttmDays);
197
235
  const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
198
236
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
199
- return {
200
- bestPrice,
201
- worstPrice,
202
- estBorrowRate,
237
+ return Object.assign(Object.assign({}, quote), { estBorrowRate,
203
238
  maxRate,
204
239
  newUnits,
205
- maxUnits,
206
- availableAssets: d.available_assets || '0',
207
- availableUnits: d.available_units || '0',
208
- takeableOffers: d.takeable_offers || [],
209
- };
240
+ maxUnits });
241
+ });
242
+ /**
243
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
244
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
245
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
246
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
247
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
248
+ * Throws if the book can't fill the amount (caller handles).
249
+ *
250
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
251
+ * so the guard is a floor rather than a ceiling:
252
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
253
+ * this when a user pins a min rate.
254
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
255
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
256
+ *
257
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
258
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
259
+ */
260
+ export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
261
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', assetsRaw, slippagePercent);
262
+ const { bestPrice, worstPrice } = quote;
263
+ const ttmDays = midnightTimeToMaturityDays(maturity);
264
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
265
+ const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
266
+ ? midnightPriceFromApy(minPaybackRate, ttmDays)
267
+ : worstPrice;
268
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
269
+ // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
270
+ // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
271
+ const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
272
+ const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
273
+ return Object.assign(Object.assign({}, quote), { estPaybackRate,
274
+ minRate,
275
+ newUnits,
276
+ minUnits });
210
277
  });
@@ -157,7 +157,7 @@ export function _getMorphoMidnightAccountData(provider, network, account, select
157
157
  let assetsDataForApy = marketInfo.assetsData;
158
158
  if (new Dec(positionInfo.debt.toString()).gt(0)) {
159
159
  try {
160
- const borrowInfo = yield getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.maturity, marketInfo.loanToken);
160
+ const borrowInfo = yield getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
161
161
  borrowRate = borrowInfo.borrowRate;
162
162
  debtBase = borrowInfo.debtBase;
163
163
  debtInterest = borrowInfo.debtInterest;
@@ -60,6 +60,16 @@ export interface MorphoMidnightMarketInfo {
60
60
  utillization: string;
61
61
  assetsData: MorphoMidnightAssetsData;
62
62
  }
63
+ export type MorphoMidnightBookSide = 'bids' | 'asks';
64
+ export interface MorphoMidnightBookOffer {
65
+ rate: string;
66
+ liquidity: string;
67
+ }
68
+ export interface MorphoMidnightParsedBook {
69
+ bestRate: string;
70
+ totalLiquidity: string;
71
+ offers: MorphoMidnightBookOffer[];
72
+ }
63
73
  export interface MorphoMidnightAggregatedPositionData {
64
74
  suppliedUsd: string;
65
75
  suppliedCollateralUsd: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.127-midnight-2-dev",
3
+ "version": "2.1.127-midnight-4-dev",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -1,13 +1,21 @@
1
1
  import Dec from 'decimal.js';
2
- import { assetAmountInEth } from '@defisaver/tokens';
2
+ import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
3
3
  import {
4
4
  calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos,
5
5
  } from '../../moneymarket';
6
6
  import { calculateNetApy } from '../../staking';
7
7
  import {
8
- LeverageType, MMAssetsData, MMUsedAsset, MMUsedAssets,
8
+ LeverageType, MMAssetsData, MMUsedAsset, MMUsedAssets, NetworkNumber,
9
9
  } from '../../types/common';
10
- import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketInfo } from '../../types';
10
+ import {
11
+ MorphoMidnightAggregatedPositionData,
12
+ MorphoMidnightAssetsData,
13
+ MorphoMidnightBookOffer,
14
+ MorphoMidnightBookSide,
15
+ MorphoMidnightMarketData,
16
+ MorphoMidnightMarketInfo,
17
+ MorphoMidnightParsedBook,
18
+ } from '../../types';
11
19
  import { SECONDS_PER_DAY, WAD } from '../../constants';
12
20
  import { LONGER_TIMEOUT } from '../../services/utils';
13
21
 
@@ -98,16 +106,21 @@ export const getMorphoMidnightAggregatedPositionData = ({
98
106
  const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
99
107
  const nowInSeconds = () => Math.floor(Date.now() / 1000);
100
108
 
109
+ // The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
110
+ // enough that markets drop out of the list for no reason.
111
+ const MIDNIGHT_BOOK_TIMEOUT = 30000;
112
+
101
113
  // The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
102
114
  // place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
103
115
  const MIDNIGHT_SLIPPAGE_MIN = 0.1;
104
116
  const MIDNIGHT_SLIPPAGE_MAX = 100;
105
117
 
106
- interface MidnightTransaction {
107
- event_type: string,
118
+ interface MidnightPosition {
108
119
  market_id: string,
109
- created_at: number,
110
- data: { seller_assets?: string, units?: string },
120
+ type: string, // 'borrow' | 'lend'
121
+ debt: string, // raw loan-token base units matches MidnightView.getPositionInfo exactly
122
+ cost_basis: string, // WAD-scaled raw base units — outstanding principal, net of exits/liquidations
123
+ effective_rate_wad: string, // WAD-scaled borrow APY, e.g. 0.05e18 = 5%
111
124
  }
112
125
 
113
126
  interface MidnightApiError {
@@ -116,6 +129,11 @@ interface MidnightApiError {
116
129
  details?: ({ field?: string, issue?: string })[] | null,
117
130
  }
118
131
 
132
+ interface MidnightRawOffer {
133
+ price: string, // WAD-scaled loan-per-unit
134
+ assets: string, // loan-token base units available at this offer
135
+ }
136
+
119
137
  interface MidnightQuoteResponse {
120
138
  average_best_price?: string,
121
139
  average_worst_price?: string,
@@ -125,10 +143,10 @@ interface MidnightQuoteResponse {
125
143
  }
126
144
 
127
145
  export interface MorphoMidnightBorrowInfo {
128
- borrowRate: string, // weighted-average borrow APY as a percent
129
- debtBase: string, // base borrowed (Σ seller_assets), loan-token units
146
+ borrowRate: string, // effective borrow APY as a percent
147
+ debtBase: string, // outstanding principal (cost_basis), loan-token units
130
148
  debtInterest: string, // debtTotal − debtBase (interest owed at maturity), loan-token units
131
- debtTotal: string, // Σ units = on-chain debt at maturity, loan-token units
149
+ debtTotal: string, // on-chain debt at maturity, loan-token units
132
150
  }
133
151
 
134
152
  export interface MorphoMidnightBorrowQuote {
@@ -143,6 +161,18 @@ export interface MorphoMidnightBorrowQuote {
143
161
  takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
144
162
  }
145
163
 
164
+ export interface MorphoMidnightPaybackQuote {
165
+ bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
166
+ worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
167
+ estPaybackRate: string, // APY the repayment retires debt at, as a percent
168
+ minRate: string, // APY the on-chain floor permits, i.e. `minUnits` annualized (display only)
169
+ newUnits: string, // debt retired at best price, raw loan-token base units
170
+ minUnits: string, // floor on debt retired (on-chain guard), raw loan-token base units
171
+ availableAssets: string,
172
+ availableUnits: string,
173
+ takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
174
+ }
175
+
146
176
  // Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
147
177
  export const midnightTimeToMaturityDays = (maturity: number, atSeconds: number = nowInSeconds()): number => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
148
178
 
@@ -185,46 +215,76 @@ export const midnightSlippageParam = (slippagePercent: Dec.Value): string => Dec
185
215
  ).toDP(1, Dec.ROUND_DOWN).toString();
186
216
 
187
217
  /**
188
- * Current borrower rate + debt breakdown from the Midnight transactions API. On-chain we can only read the
189
- * total debt at maturity (`units`); the base-vs-interest split and the effective borrow rate require the
190
- * fill history. Per fill the rate is (units / seller_assets)^(365 / ttmAtFill) 1, weighted by base amount.
218
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
219
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
220
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... the fill history
221
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
222
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
223
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
191
224
  * The caller swallows errors — a missing rate must never block position rendering.
192
225
  */
193
226
  export const getMorphoMidnightUserBorrowInfo = async (
194
227
  account: string,
195
228
  marketId: string,
196
- maturity: number,
197
229
  loanTokenSymbol: string,
198
230
  ): Promise<MorphoMidnightBorrowInfo> => {
199
- const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/transactions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
200
- const json: { data?: MidnightTransaction[] } = await res.json();
201
- const borrows = (json?.data || []).filter((t) => t.event_type === 'borrow' && t.market_id?.toLowerCase() === marketId.toLowerCase());
202
-
203
- let sumSeller = new Dec(0); // Σ seller_assets (base), raw
204
- let sumUnits = new Dec(0); // Σ units (debt at maturity), raw
205
- let weightedApy = new Dec(0); // Σ seller_assets × APYᵢ
206
-
207
- borrows.forEach((t) => {
208
- const sellerAssets = new Dec(t.data?.seller_assets || 0);
209
- const units = new Dec(t.data?.units || 0);
210
- if (sellerAssets.lte(0) || units.lte(0)) return;
211
- const ttmDays = midnightTimeToMaturityDays(maturity, t.created_at);
212
- const apy = midnightApyFromPrice(sellerAssets.div(units), ttmDays); // price = seller_assets / units
213
- sumSeller = sumSeller.add(sellerAssets);
214
- sumUnits = sumUnits.add(units);
215
- weightedApy = weightedApy.add(sellerAssets.mul(apy));
216
- });
217
-
218
- const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
219
- const debtBase = assetAmountInEth(sumSeller.toFixed(0), loanTokenSymbol);
220
- const debtTotal = assetAmountInEth(sumUnits.toFixed(0), loanTokenSymbol);
231
+ const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
232
+ const json: { data?: MidnightPosition[] } = await res.json();
233
+ const position = (json?.data || []).find((p) => p.type === 'borrow' && p.market_id?.toLowerCase() === marketId.toLowerCase());
234
+
235
+ const debtTotal = assetAmountInEth(position?.debt || '0', loanTokenSymbol);
236
+ const costBasisRaw = new Dec(position?.cost_basis || 0).div(WAD); // WAD-scaled raw base units
237
+ const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
221
238
  const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
239
+ const borrowRate = new Dec(position?.effective_rate_wad || 0).div(WAD).mul(100).toString();
222
240
 
223
241
  return {
224
242
  borrowRate, debtBase, debtInterest, debtTotal,
225
243
  };
226
244
  };
227
245
 
246
+ /**
247
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
248
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
249
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
250
+ *
251
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
252
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
253
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
254
+ *
255
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
256
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
257
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
258
+ */
259
+ export const getMorphoMidnightMarketBook = async (
260
+ market: MorphoMidnightMarketData,
261
+ network: NetworkNumber,
262
+ side: MorphoMidnightBookSide = 'bids',
263
+ ): Promise<MorphoMidnightParsedBook | null> => {
264
+ const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
265
+ const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
266
+ if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
267
+
268
+ const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
269
+ const ttmDays = midnightTimeToMaturityDays(market.maturity);
270
+ const bestFirst = side === 'asks' ? -1 : 1;
271
+
272
+ const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || [])
273
+ .map((offer) => ({
274
+ rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
275
+ liquidity: assetAmountInEth(offer.assets, loanSymbol),
276
+ }))
277
+ .sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
278
+
279
+ if (offers.length === 0) return null;
280
+
281
+ return {
282
+ bestRate: offers[0].rate,
283
+ totalLiquidity: offers.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
284
+ offers,
285
+ };
286
+ };
287
+
228
288
  // The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
229
289
  // (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
230
290
  // Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
@@ -234,6 +294,36 @@ const midnightQuoteError = (error?: MidnightApiError): string => {
234
294
  return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
235
295
  };
236
296
 
297
+ interface MidnightParsedQuote {
298
+ bestPrice: string, // loan-per-unit
299
+ worstPrice: string, // slippage-adjusted; below best on `bids`, above it on `asks`
300
+ availableAssets: string,
301
+ availableUnits: string,
302
+ takeableOffers: any[],
303
+ }
304
+
305
+ // The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
306
+ const fetchMorphoMidnightQuote = async (
307
+ marketId: string,
308
+ side: MorphoMidnightBookSide,
309
+ assetsRaw: string,
310
+ slippagePercent: Dec.Value,
311
+ ): Promise<MidnightParsedQuote> => {
312
+ const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?assets=${assetsRaw}&slippage=${midnightSlippageParam(slippagePercent)}`;
313
+ const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
314
+ const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
315
+ const d = json?.data;
316
+ if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
317
+
318
+ return {
319
+ bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
320
+ worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
321
+ availableAssets: d.available_assets || '0',
322
+ availableUnits: d.available_units || '0',
323
+ takeableOffers: d.takeable_offers || [],
324
+ };
325
+ };
326
+
237
327
  /**
238
328
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
239
329
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -258,14 +348,8 @@ export const getMorphoMidnightBorrowQuote = async (
258
348
  maturity: number,
259
349
  maxBorrowRate?: Dec.Value,
260
350
  ): Promise<MorphoMidnightBorrowQuote> => {
261
- const url = `${MIDNIGHT_API_BASE}/books/${marketId}/bids/quote?assets=${assetsRaw}&slippage=${midnightSlippageParam(slippagePercent)}`;
262
- const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
263
- const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
264
- const d = json?.data;
265
- if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
266
-
267
- const bestPrice = new Dec(d.average_best_price).div(WAD).toString();
268
- const worstPrice = new Dec(d.average_worst_price || 0).div(WAD).toString();
351
+ const quote = await fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
352
+ const { bestPrice, worstPrice } = quote;
269
353
  const ttmDays = midnightTimeToMaturityDays(maturity);
270
354
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
271
355
 
@@ -279,14 +363,58 @@ export const getMorphoMidnightBorrowQuote = async (
279
363
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
280
364
 
281
365
  return {
282
- bestPrice,
283
- worstPrice,
366
+ ...quote,
284
367
  estBorrowRate,
285
368
  maxRate,
286
369
  newUnits,
287
370
  maxUnits,
288
- availableAssets: d.available_assets || '0',
289
- availableUnits: d.available_units || '0',
290
- takeableOffers: d.takeable_offers || [],
371
+ };
372
+ };
373
+
374
+ /**
375
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
376
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
377
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
378
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
379
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
380
+ * Throws if the book can't fill the amount (caller handles).
381
+ *
382
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
383
+ * so the guard is a floor rather than a ceiling:
384
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
385
+ * this when a user pins a min rate.
386
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
387
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
388
+ *
389
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
390
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
391
+ */
392
+ export const getMorphoMidnightPaybackQuote = async (
393
+ marketId: string,
394
+ assetsRaw: string,
395
+ slippagePercent: Dec.Value,
396
+ maturity: number,
397
+ minPaybackRate?: Dec.Value,
398
+ ): Promise<MorphoMidnightPaybackQuote> => {
399
+ const quote = await fetchMorphoMidnightQuote(marketId, 'asks', assetsRaw, slippagePercent);
400
+ const { bestPrice, worstPrice } = quote;
401
+ const ttmDays = midnightTimeToMaturityDays(maturity);
402
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
403
+
404
+ const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
405
+ ? midnightPriceFromApy(minPaybackRate, ttmDays)
406
+ : worstPrice;
407
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
408
+ // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
409
+ // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
410
+ const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
411
+ const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
412
+
413
+ return {
414
+ ...quote,
415
+ estPaybackRate,
416
+ minRate,
417
+ newUnits,
418
+ minUnits,
291
419
  };
292
420
  };
@@ -166,7 +166,7 @@ export async function _getMorphoMidnightAccountData(provider: Client, network: N
166
166
  let assetsDataForApy = marketInfo.assetsData;
167
167
  if (new Dec(positionInfo.debt.toString()).gt(0)) {
168
168
  try {
169
- const borrowInfo = await getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.maturity, marketInfo.loanToken);
169
+ const borrowInfo = await getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
170
170
  borrowRate = borrowInfo.borrowRate;
171
171
  debtBase = borrowInfo.debtBase;
172
172
  debtInterest = borrowInfo.debtInterest;
@@ -70,6 +70,22 @@ export interface MorphoMidnightMarketInfo {
70
70
  assetsData: MorphoMidnightAssetsData,
71
71
  }
72
72
 
73
+ // Which half of the order book a caller is taking from: `bids` are the lend offers a borrower fills,
74
+ // `asks` the sell offers a repayer buys debt units from.
75
+ export type MorphoMidnightBookSide = 'bids' | 'asks';
76
+
77
+ // One resting offer on a market's order book, as an annualized rate rather than the API's raw WAD price.
78
+ export interface MorphoMidnightBookOffer {
79
+ rate: string, // fixed APY, percent
80
+ liquidity: string, // loan-token amount available at this rate
81
+ }
82
+
83
+ export interface MorphoMidnightParsedBook {
84
+ bestRate: string, // best rate for the taker of this side (= offers[0].rate)
85
+ totalLiquidity: string, // Σ offers[].liquidity, loan-token units
86
+ offers: MorphoMidnightBookOffer[], // best-first: bids ascending by rate, asks descending
87
+ }
88
+
73
89
  export interface MorphoMidnightAggregatedPositionData {
74
90
  suppliedUsd: string,
75
91
  suppliedCollateralUsd: string,