@defisaver/positions-sdk 2.1.127-midnight-3-dev → 2.1.127-midnight-5-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
2
  import { MMUsedAssets, NetworkNumber } from '../../types/common';
3
- import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
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,28 @@ 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
+ }
46
+ export interface MorphoMidnightPaybackUnitsQuote {
47
+ bestPrice: string;
48
+ worstPrice: string;
49
+ estPaybackRate: string;
50
+ minRate: string;
51
+ newAssets: string;
52
+ maxAssets: string;
53
+ availableAssets: string;
54
+ availableUnits: string;
55
+ takeableOffers: any[];
56
+ }
35
57
  export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
36
58
  export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
37
59
  /**
@@ -52,22 +74,29 @@ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec
52
74
  */
53
75
  export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
54
76
  /**
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.
77
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
78
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
79
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... the fill history
80
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
81
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
82
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
58
83
  * The caller swallows errors — a missing rate must never block position rendering.
59
84
  */
60
- export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, maturity: number, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
85
+ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
61
86
  /**
62
- * A market's resting bids, as rates rather than the API's WAD-scaled loan-per-unit prices. Annualizing
63
- * each price against time-to-maturity gives the rate a borrower filling that offer pays — verified
64
- * against Morpho's fixed-market UI, where per-offer rates match to the cent.
87
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
88
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
89
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
90
+ *
91
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
92
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
93
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
65
94
  *
66
- * Returns `null` for an empty book: there is nothing to borrow against, so a market listing should skip
67
- * the market rather than advertise it at a 0% rate. Throws when the request fails — an error response is
68
- * rarely JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
95
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
96
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
97
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
69
98
  */
70
- export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber) => Promise<MorphoMidnightParsedBook | null>;
99
+ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
71
100
  /**
72
101
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
73
102
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -86,3 +115,38 @@ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketD
86
115
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
87
116
  */
88
117
  export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
118
+ /**
119
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
120
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
121
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
122
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
123
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
124
+ * Throws if the book can't fill the amount (caller handles).
125
+ *
126
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
127
+ * so the guard is a floor rather than a ceiling:
128
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
129
+ * this when a user pins a min rate.
130
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
131
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
132
+ *
133
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
134
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
135
+ */
136
+ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
137
+ /**
138
+ * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
139
+ * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
140
+ * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
141
+ * a ceiling on assets spent rather than a floor on units bought.
142
+ *
143
+ * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
144
+ * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
145
+ * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
146
+ * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
147
+ * directly, rather than through an action contract that sweeps the remainder back — need this quote.
148
+ *
149
+ * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
150
+ * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
151
+ */
152
+ export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackUnitsQuote>;
@@ -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.getMorphoMidnightMarketBook = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.midnightPriceFromApy = exports.midnightApyFromPrice = exports.midnightTimeToMaturityDays = exports.getMorphoMidnightAggregatedPositionData = void 0;
15
+ exports.getMorphoMidnightPaybackUnitsQuote = 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");
@@ -134,49 +134,42 @@ exports.midnightPriceFromApy = midnightPriceFromApy;
134
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();
135
135
  exports.midnightSlippageParam = midnightSlippageParam;
136
136
  /**
137
- * Current borrower rate + debt breakdown from the Midnight transactions API. On-chain we can only read the
138
- * total debt at maturity (`units`); the base-vs-interest split and the effective borrow rate require the
139
- * 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.
140
143
  * The caller swallows errors — a missing rate must never block position rendering.
141
144
  */
142
- const getMorphoMidnightUserBorrowInfo = (account, marketId, maturity, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
143
- 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) });
144
147
  const json = yield res.json();
145
- 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(); });
146
- let sumSeller = new decimal_js_1.default(0); // Σ seller_assets (base), raw
147
- let sumUnits = new decimal_js_1.default(0); // Σ units (debt at maturity), raw
148
- let weightedApy = new decimal_js_1.default(0); // Σ seller_assets × APYᵢ
149
- borrows.forEach((t) => {
150
- var _a, _b;
151
- const sellerAssets = new decimal_js_1.default(((_a = t.data) === null || _a === void 0 ? void 0 : _a.seller_assets) || 0);
152
- const units = new decimal_js_1.default(((_b = t.data) === null || _b === void 0 ? void 0 : _b.units) || 0);
153
- if (sellerAssets.lte(0) || units.lte(0))
154
- return;
155
- const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity, t.created_at);
156
- const apy = (0, exports.midnightApyFromPrice)(sellerAssets.div(units), ttmDays); // price = seller_assets / units
157
- sumSeller = sumSeller.add(sellerAssets);
158
- sumUnits = sumUnits.add(units);
159
- weightedApy = weightedApy.add(sellerAssets.mul(apy));
160
- });
161
- const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
162
- const debtBase = (0, tokens_1.assetAmountInEth)(sumSeller.toFixed(0), loanTokenSymbol);
163
- 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();
164
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();
165
154
  return {
166
155
  borrowRate, debtBase, debtInterest, debtTotal,
167
156
  };
168
157
  });
169
158
  exports.getMorphoMidnightUserBorrowInfo = getMorphoMidnightUserBorrowInfo;
170
159
  /**
171
- * A market's resting bids, as rates rather than the API's WAD-scaled loan-per-unit prices. Annualizing
172
- * each price against time-to-maturity gives the rate a borrower filling that offer pays — verified
173
- * against Morpho's fixed-market UI, where per-offer rates match to the cent.
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`.
174
167
  *
175
- * Returns `null` for an empty book: there is nothing to borrow against, so a market listing should skip
176
- * the market rather than advertise it at a 0% rate. Throws when the request fails — an error response is
177
- * rarely JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
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.
178
171
  */
179
- const getMorphoMidnightMarketBook = (market, network) => __awaiter(void 0, void 0, void 0, function* () {
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') {
180
173
  var _a;
181
174
  const loanSymbol = (0, tokens_1.getAssetInfoByAddress)(market.loanToken, network).symbol;
182
175
  const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
@@ -184,12 +177,13 @@ const getMorphoMidnightMarketBook = (market, network) => __awaiter(void 0, void
184
177
  throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
185
178
  const json = yield res.json();
186
179
  const ttmDays = (0, exports.midnightTimeToMaturityDays)(market.maturity);
187
- const offers = (((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a.bids) || [])
188
- .map((bid) => ({
189
- rate: (0, exports.midnightApyFromPrice)(new decimal_js_1.default(bid.price).div(constants_1.WAD), ttmDays),
190
- liquidity: (0, tokens_1.assetAmountInEth)(bid.assets, loanSymbol),
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),
191
185
  }))
192
- .sort((a, b) => new decimal_js_1.default(a.rate).minus(b.rate).toNumber());
186
+ .sort((a, b) => new decimal_js_1.default(a.rate).minus(b.rate).mul(bestFirst).toNumber());
193
187
  if (offers.length === 0)
194
188
  return null;
195
189
  return {
@@ -207,6 +201,23 @@ const midnightQuoteError = (error) => {
207
201
  const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
208
202
  return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
209
203
  };
204
+ // The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
205
+ const fetchMorphoMidnightQuote = (marketId, side, size, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
206
+ const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
207
+ const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${(0, exports.midnightSlippageParam)(slippagePercent)}`;
208
+ const res = yield fetch(url, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
209
+ const json = yield res.json();
210
+ const d = json === null || json === void 0 ? void 0 : json.data;
211
+ if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
212
+ throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
213
+ return {
214
+ bestPrice: new decimal_js_1.default(d.average_best_price).div(constants_1.WAD).toString(),
215
+ worstPrice: new decimal_js_1.default(d.average_worst_price || 0).div(constants_1.WAD).toString(),
216
+ availableAssets: d.available_assets || '0',
217
+ availableUnits: d.available_units || '0',
218
+ takeableOffers: d.takeable_offers || [],
219
+ };
220
+ });
210
221
  /**
211
222
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
212
223
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -225,14 +236,8 @@ const midnightQuoteError = (error) => {
225
236
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
226
237
  */
227
238
  const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
228
- const url = `${MIDNIGHT_API_BASE}/books/${marketId}/bids/quote?assets=${assetsRaw}&slippage=${(0, exports.midnightSlippageParam)(slippagePercent)}`;
229
- const res = yield fetch(url, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
230
- const json = yield res.json();
231
- const d = json === null || json === void 0 ? void 0 : json.data;
232
- if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
233
- throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
234
- const bestPrice = new decimal_js_1.default(d.average_best_price).div(constants_1.WAD).toString();
235
- const worstPrice = new decimal_js_1.default(d.average_worst_price || 0).div(constants_1.WAD).toString();
239
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
240
+ const { bestPrice, worstPrice } = quote;
236
241
  const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
237
242
  const estBorrowRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
238
243
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
@@ -243,16 +248,81 @@ const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, matu
243
248
  const maxRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
244
249
  const newUnits = new decimal_js_1.default(bestPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(bestPrice).toFixed(0);
245
250
  const maxUnits = new decimal_js_1.default(capPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(capPrice).toFixed(0);
246
- return {
247
- bestPrice,
248
- worstPrice,
249
- estBorrowRate,
251
+ return Object.assign(Object.assign({}, quote), { estBorrowRate,
250
252
  maxRate,
251
253
  newUnits,
252
- maxUnits,
253
- availableAssets: d.available_assets || '0',
254
- availableUnits: d.available_units || '0',
255
- takeableOffers: d.takeable_offers || [],
256
- };
254
+ maxUnits });
257
255
  });
258
256
  exports.getMorphoMidnightBorrowQuote = getMorphoMidnightBorrowQuote;
257
+ /**
258
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
259
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
260
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
261
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
262
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
263
+ * Throws if the book can't fill the amount (caller handles).
264
+ *
265
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
266
+ * so the guard is a floor rather than a ceiling:
267
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
268
+ * this when a user pins a min rate.
269
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
270
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
271
+ *
272
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
273
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
274
+ */
275
+ const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
276
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
277
+ const { bestPrice, worstPrice } = quote;
278
+ const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
279
+ const estPaybackRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
280
+ const capPrice = minPaybackRate !== undefined && new decimal_js_1.default(minPaybackRate).gt(0)
281
+ ? (0, exports.midnightPriceFromApy)(minPaybackRate, ttmDays)
282
+ : worstPrice;
283
+ const minRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
284
+ // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
285
+ // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
286
+ 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);
287
+ 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);
288
+ return Object.assign(Object.assign({}, quote), { estPaybackRate,
289
+ minRate,
290
+ newUnits,
291
+ minUnits });
292
+ });
293
+ exports.getMorphoMidnightPaybackQuote = getMorphoMidnightPaybackQuote;
294
+ /**
295
+ * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
296
+ * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
297
+ * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
298
+ * a ceiling on assets spent rather than a floor on units bought.
299
+ *
300
+ * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
301
+ * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
302
+ * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
303
+ * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
304
+ * directly, rather than through an action contract that sweeps the remainder back — need this quote.
305
+ *
306
+ * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
307
+ * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
308
+ */
309
+ const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
310
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
311
+ const { bestPrice, worstPrice } = quote;
312
+ const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
313
+ const estPaybackRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
314
+ const capPrice = minPaybackRate !== undefined && new decimal_js_1.default(minPaybackRate).gt(0)
315
+ ? (0, exports.midnightPriceFromApy)(minPaybackRate, ttmDays)
316
+ : worstPrice;
317
+ const minRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
318
+ // Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
319
+ // what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
320
+ // buy short of the units it was asked for.
321
+ const newAssets = new decimal_js_1.default(unitsRaw).mul(bestPrice).toFixed(0, decimal_js_1.default.ROUND_UP);
322
+ const maxAssets = new decimal_js_1.default(unitsRaw).mul(capPrice).toFixed(0, decimal_js_1.default.ROUND_UP);
323
+ return Object.assign(Object.assign({}, quote), { estPaybackRate,
324
+ minRate,
325
+ newAssets,
326
+ maxAssets });
327
+ });
328
+ exports.getMorphoMidnightPaybackUnitsQuote = getMorphoMidnightPaybackUnitsQuote;
@@ -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,7 @@ export interface MorphoMidnightMarketInfo {
60
60
  utillization: string;
61
61
  assetsData: MorphoMidnightAssetsData;
62
62
  }
63
+ export type MorphoMidnightBookSide = 'bids' | 'asks';
63
64
  export interface MorphoMidnightBookOffer {
64
65
  rate: string;
65
66
  liquidity: string;
@@ -1,6 +1,6 @@
1
1
  import Dec from 'decimal.js';
2
2
  import { MMUsedAssets, NetworkNumber } from '../../types/common';
3
- import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
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,28 @@ 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
+ }
46
+ export interface MorphoMidnightPaybackUnitsQuote {
47
+ bestPrice: string;
48
+ worstPrice: string;
49
+ estPaybackRate: string;
50
+ minRate: string;
51
+ newAssets: string;
52
+ maxAssets: string;
53
+ availableAssets: string;
54
+ availableUnits: string;
55
+ takeableOffers: any[];
56
+ }
35
57
  export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
36
58
  export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
37
59
  /**
@@ -52,22 +74,29 @@ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec
52
74
  */
53
75
  export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
54
76
  /**
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.
77
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
78
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
79
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... the fill history
80
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
81
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
82
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
58
83
  * The caller swallows errors — a missing rate must never block position rendering.
59
84
  */
60
- export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, maturity: number, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
85
+ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
61
86
  /**
62
- * A market's resting bids, as rates rather than the API's WAD-scaled loan-per-unit prices. Annualizing
63
- * each price against time-to-maturity gives the rate a borrower filling that offer pays — verified
64
- * against Morpho's fixed-market UI, where per-offer rates match to the cent.
87
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
88
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
89
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
90
+ *
91
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
92
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
93
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
65
94
  *
66
- * Returns `null` for an empty book: there is nothing to borrow against, so a market listing should skip
67
- * the market rather than advertise it at a 0% rate. Throws when the request fails — an error response is
68
- * rarely JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
95
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
96
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
97
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
69
98
  */
70
- export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber) => Promise<MorphoMidnightParsedBook | null>;
99
+ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
71
100
  /**
72
101
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
73
102
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -86,3 +115,38 @@ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketD
86
115
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
87
116
  */
88
117
  export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
118
+ /**
119
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
120
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
121
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
122
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
123
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
124
+ * Throws if the book can't fill the amount (caller handles).
125
+ *
126
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
127
+ * so the guard is a floor rather than a ceiling:
128
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
129
+ * this when a user pins a min rate.
130
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
131
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
132
+ *
133
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
134
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
135
+ */
136
+ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
137
+ /**
138
+ * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
139
+ * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
140
+ * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
141
+ * a ceiling on assets spent rather than a floor on units bought.
142
+ *
143
+ * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
144
+ * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
145
+ * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
146
+ * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
147
+ * directly, rather than through an action contract that sweeps the remainder back — need this quote.
148
+ *
149
+ * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
150
+ * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
151
+ */
152
+ export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackUnitsQuote>;
@@ -123,48 +123,41 @@ export const midnightPriceFromApy = (ratePercent, ttmDays) => {
123
123
  */
124
124
  export const midnightSlippageParam = (slippagePercent) => Dec.min(Dec.max(new Dec(slippagePercent), MIDNIGHT_SLIPPAGE_MIN), MIDNIGHT_SLIPPAGE_MAX).toDP(1, Dec.ROUND_DOWN).toString();
125
125
  /**
126
- * Current borrower rate + debt breakdown from the Midnight transactions API. On-chain we can only read the
127
- * total debt at maturity (`units`); the base-vs-interest split and the effective borrow rate require the
128
- * 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.
129
132
  * The caller swallows errors — a missing rate must never block position rendering.
130
133
  */
131
- export const getMorphoMidnightUserBorrowInfo = (account, marketId, maturity, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
132
- 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) });
133
136
  const json = yield res.json();
134
- 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(); });
135
- let sumSeller = new Dec(0); // Σ seller_assets (base), raw
136
- let sumUnits = new Dec(0); // Σ units (debt at maturity), raw
137
- let weightedApy = new Dec(0); // Σ seller_assets × APYᵢ
138
- borrows.forEach((t) => {
139
- var _a, _b;
140
- const sellerAssets = new Dec(((_a = t.data) === null || _a === void 0 ? void 0 : _a.seller_assets) || 0);
141
- const units = new Dec(((_b = t.data) === null || _b === void 0 ? void 0 : _b.units) || 0);
142
- if (sellerAssets.lte(0) || units.lte(0))
143
- return;
144
- const ttmDays = midnightTimeToMaturityDays(maturity, t.created_at);
145
- const apy = midnightApyFromPrice(sellerAssets.div(units), ttmDays); // price = seller_assets / units
146
- sumSeller = sumSeller.add(sellerAssets);
147
- sumUnits = sumUnits.add(units);
148
- weightedApy = weightedApy.add(sellerAssets.mul(apy));
149
- });
150
- const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
151
- const debtBase = assetAmountInEth(sumSeller.toFixed(0), loanTokenSymbol);
152
- 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();
153
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();
154
143
  return {
155
144
  borrowRate, debtBase, debtInterest, debtTotal,
156
145
  };
157
146
  });
158
147
  /**
159
- * A market's resting bids, as rates rather than the API's WAD-scaled loan-per-unit prices. Annualizing
160
- * each price against time-to-maturity gives the rate a borrower filling that offer pays — verified
161
- * against Morpho's fixed-market UI, where per-offer rates match to the cent.
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`.
162
155
  *
163
- * Returns `null` for an empty book: there is nothing to borrow against, so a market listing should skip
164
- * the market rather than advertise it at a 0% rate. Throws when the request fails — an error response is
165
- * rarely JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
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.
166
159
  */
167
- export const getMorphoMidnightMarketBook = (market, network) => __awaiter(void 0, void 0, void 0, function* () {
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') {
168
161
  var _a;
169
162
  const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
170
163
  const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
@@ -172,12 +165,13 @@ export const getMorphoMidnightMarketBook = (market, network) => __awaiter(void 0
172
165
  throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
173
166
  const json = yield res.json();
174
167
  const ttmDays = midnightTimeToMaturityDays(market.maturity);
175
- const offers = (((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a.bids) || [])
176
- .map((bid) => ({
177
- rate: midnightApyFromPrice(new Dec(bid.price).div(WAD), ttmDays),
178
- liquidity: assetAmountInEth(bid.assets, loanSymbol),
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),
179
173
  }))
180
- .sort((a, b) => new Dec(a.rate).minus(b.rate).toNumber());
174
+ .sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
181
175
  if (offers.length === 0)
182
176
  return null;
183
177
  return {
@@ -194,6 +188,23 @@ const midnightQuoteError = (error) => {
194
188
  const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
195
189
  return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
196
190
  };
191
+ // The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
192
+ const fetchMorphoMidnightQuote = (marketId, side, size, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
193
+ const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
194
+ const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${midnightSlippageParam(slippagePercent)}`;
195
+ const res = yield fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
196
+ const json = yield res.json();
197
+ const d = json === null || json === void 0 ? void 0 : json.data;
198
+ if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
199
+ throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
200
+ return {
201
+ bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
202
+ worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
203
+ availableAssets: d.available_assets || '0',
204
+ availableUnits: d.available_units || '0',
205
+ takeableOffers: d.takeable_offers || [],
206
+ };
207
+ });
197
208
  /**
198
209
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
199
210
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -212,14 +223,8 @@ const midnightQuoteError = (error) => {
212
223
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
213
224
  */
214
225
  export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
215
- const url = `${MIDNIGHT_API_BASE}/books/${marketId}/bids/quote?assets=${assetsRaw}&slippage=${midnightSlippageParam(slippagePercent)}`;
216
- const res = yield fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
217
- const json = yield res.json();
218
- const d = json === null || json === void 0 ? void 0 : json.data;
219
- if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
220
- throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
221
- const bestPrice = new Dec(d.average_best_price).div(WAD).toString();
222
- const worstPrice = new Dec(d.average_worst_price || 0).div(WAD).toString();
226
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
227
+ const { bestPrice, worstPrice } = quote;
223
228
  const ttmDays = midnightTimeToMaturityDays(maturity);
224
229
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
225
230
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
@@ -230,15 +235,78 @@ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercen
230
235
  const maxRate = midnightApyFromPrice(capPrice, ttmDays);
231
236
  const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
232
237
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
233
- return {
234
- bestPrice,
235
- worstPrice,
236
- estBorrowRate,
238
+ return Object.assign(Object.assign({}, quote), { estBorrowRate,
237
239
  maxRate,
238
240
  newUnits,
239
- maxUnits,
240
- availableAssets: d.available_assets || '0',
241
- availableUnits: d.available_units || '0',
242
- takeableOffers: d.takeable_offers || [],
243
- };
241
+ maxUnits });
242
+ });
243
+ /**
244
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
245
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
246
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
247
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
248
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
249
+ * Throws if the book can't fill the amount (caller handles).
250
+ *
251
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
252
+ * so the guard is a floor rather than a ceiling:
253
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
254
+ * this when a user pins a min rate.
255
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
256
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
257
+ *
258
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
259
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
260
+ */
261
+ export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
262
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
263
+ const { bestPrice, worstPrice } = quote;
264
+ const ttmDays = midnightTimeToMaturityDays(maturity);
265
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
266
+ const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
267
+ ? midnightPriceFromApy(minPaybackRate, ttmDays)
268
+ : worstPrice;
269
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
270
+ // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
271
+ // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
272
+ const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
273
+ const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
274
+ return Object.assign(Object.assign({}, quote), { estPaybackRate,
275
+ minRate,
276
+ newUnits,
277
+ minUnits });
278
+ });
279
+ /**
280
+ * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
281
+ * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
282
+ * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
283
+ * a ceiling on assets spent rather than a floor on units bought.
284
+ *
285
+ * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
286
+ * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
287
+ * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
288
+ * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
289
+ * directly, rather than through an action contract that sweeps the remainder back — need this quote.
290
+ *
291
+ * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
292
+ * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
293
+ */
294
+ export const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
295
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
296
+ const { bestPrice, worstPrice } = quote;
297
+ const ttmDays = midnightTimeToMaturityDays(maturity);
298
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
299
+ const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
300
+ ? midnightPriceFromApy(minPaybackRate, ttmDays)
301
+ : worstPrice;
302
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
303
+ // Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
304
+ // what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
305
+ // buy short of the units it was asked for.
306
+ const newAssets = new Dec(unitsRaw).mul(bestPrice).toFixed(0, Dec.ROUND_UP);
307
+ const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
308
+ return Object.assign(Object.assign({}, quote), { estPaybackRate,
309
+ minRate,
310
+ newAssets,
311
+ maxAssets });
244
312
  });
@@ -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,7 @@ export interface MorphoMidnightMarketInfo {
60
60
  utillization: string;
61
61
  assetsData: MorphoMidnightAssetsData;
62
62
  }
63
+ export type MorphoMidnightBookSide = 'bids' | 'asks';
63
64
  export interface MorphoMidnightBookOffer {
64
65
  rate: string;
65
66
  liquidity: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.127-midnight-3-dev",
3
+ "version": "2.1.127-midnight-5-dev",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -11,6 +11,7 @@ import {
11
11
  MorphoMidnightAggregatedPositionData,
12
12
  MorphoMidnightAssetsData,
13
13
  MorphoMidnightBookOffer,
14
+ MorphoMidnightBookSide,
14
15
  MorphoMidnightMarketData,
15
16
  MorphoMidnightMarketInfo,
16
17
  MorphoMidnightParsedBook,
@@ -114,11 +115,12 @@ const MIDNIGHT_BOOK_TIMEOUT = 30000;
114
115
  const MIDNIGHT_SLIPPAGE_MIN = 0.1;
115
116
  const MIDNIGHT_SLIPPAGE_MAX = 100;
116
117
 
117
- interface MidnightTransaction {
118
- event_type: string,
118
+ interface MidnightPosition {
119
119
  market_id: string,
120
- created_at: number,
121
- 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%
122
124
  }
123
125
 
124
126
  interface MidnightApiError {
@@ -127,7 +129,7 @@ interface MidnightApiError {
127
129
  details?: ({ field?: string, issue?: string })[] | null,
128
130
  }
129
131
 
130
- interface MidnightRawBid {
132
+ interface MidnightRawOffer {
131
133
  price: string, // WAD-scaled loan-per-unit
132
134
  assets: string, // loan-token base units available at this offer
133
135
  }
@@ -141,10 +143,10 @@ interface MidnightQuoteResponse {
141
143
  }
142
144
 
143
145
  export interface MorphoMidnightBorrowInfo {
144
- borrowRate: string, // weighted-average borrow APY as a percent
145
- 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
146
148
  debtInterest: string, // debtTotal − debtBase (interest owed at maturity), loan-token units
147
- debtTotal: string, // Σ units = on-chain debt at maturity, loan-token units
149
+ debtTotal: string, // on-chain debt at maturity, loan-token units
148
150
  }
149
151
 
150
152
  export interface MorphoMidnightBorrowQuote {
@@ -159,6 +161,32 @@ export interface MorphoMidnightBorrowQuote {
159
161
  takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
160
162
  }
161
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
+
176
+ // Payback quoted the other way round: the caller names the debt units to retire, and the quote prices
177
+ // what buying them costs. Same rate guard as the assets-target quote, expressed as a spend ceiling.
178
+ export interface MorphoMidnightPaybackUnitsQuote {
179
+ bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
180
+ worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
181
+ estPaybackRate: string, // APY the repayment retires debt at, as a percent
182
+ minRate: string, // APY the on-chain ceiling permits, i.e. `maxAssets` annualized (display only)
183
+ newAssets: string, // assets the target units cost at best price, raw loan-token base units
184
+ maxAssets: string, // ceiling on assets spent (on-chain guard), raw loan-token base units
185
+ availableAssets: string,
186
+ availableUnits: string,
187
+ takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
188
+ }
189
+
162
190
  // Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
163
191
  export const midnightTimeToMaturityDays = (maturity: number, atSeconds: number = nowInSeconds()): number => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
164
192
 
@@ -201,40 +229,28 @@ export const midnightSlippageParam = (slippagePercent: Dec.Value): string => Dec
201
229
  ).toDP(1, Dec.ROUND_DOWN).toString();
202
230
 
203
231
  /**
204
- * Current borrower rate + debt breakdown from the Midnight transactions API. On-chain we can only read the
205
- * total debt at maturity (`units`); the base-vs-interest split and the effective borrow rate require the
206
- * fill history. Per fill the rate is (units / seller_assets)^(365 / ttmAtFill) 1, weighted by base amount.
232
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
233
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
234
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... the fill history
235
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
236
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
237
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
207
238
  * The caller swallows errors — a missing rate must never block position rendering.
208
239
  */
209
240
  export const getMorphoMidnightUserBorrowInfo = async (
210
241
  account: string,
211
242
  marketId: string,
212
- maturity: number,
213
243
  loanTokenSymbol: string,
214
244
  ): Promise<MorphoMidnightBorrowInfo> => {
215
- const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/transactions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
216
- const json: { data?: MidnightTransaction[] } = await res.json();
217
- const borrows = (json?.data || []).filter((t) => t.event_type === 'borrow' && t.market_id?.toLowerCase() === marketId.toLowerCase());
218
-
219
- let sumSeller = new Dec(0); // Σ seller_assets (base), raw
220
- let sumUnits = new Dec(0); // Σ units (debt at maturity), raw
221
- let weightedApy = new Dec(0); // Σ seller_assets × APYᵢ
222
-
223
- borrows.forEach((t) => {
224
- const sellerAssets = new Dec(t.data?.seller_assets || 0);
225
- const units = new Dec(t.data?.units || 0);
226
- if (sellerAssets.lte(0) || units.lte(0)) return;
227
- const ttmDays = midnightTimeToMaturityDays(maturity, t.created_at);
228
- const apy = midnightApyFromPrice(sellerAssets.div(units), ttmDays); // price = seller_assets / units
229
- sumSeller = sumSeller.add(sellerAssets);
230
- sumUnits = sumUnits.add(units);
231
- weightedApy = weightedApy.add(sellerAssets.mul(apy));
232
- });
233
-
234
- const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
235
- const debtBase = assetAmountInEth(sumSeller.toFixed(0), loanTokenSymbol);
236
- const debtTotal = assetAmountInEth(sumUnits.toFixed(0), loanTokenSymbol);
245
+ const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
246
+ const json: { data?: MidnightPosition[] } = await res.json();
247
+ const position = (json?.data || []).find((p) => p.type === 'borrow' && p.market_id?.toLowerCase() === marketId.toLowerCase());
248
+
249
+ const debtTotal = assetAmountInEth(position?.debt || '0', loanTokenSymbol);
250
+ const costBasisRaw = new Dec(position?.cost_basis || 0).div(WAD); // WAD-scaled raw base units
251
+ const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
237
252
  const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
253
+ const borrowRate = new Dec(position?.effective_rate_wad || 0).div(WAD).mul(100).toString();
238
254
 
239
255
  return {
240
256
  borrowRate, debtBase, debtInterest, debtTotal,
@@ -242,31 +258,37 @@ export const getMorphoMidnightUserBorrowInfo = async (
242
258
  };
243
259
 
244
260
  /**
245
- * A market's resting bids, as rates rather than the API's WAD-scaled loan-per-unit prices. Annualizing
246
- * each price against time-to-maturity gives the rate a borrower filling that offer pays — verified
247
- * against Morpho's fixed-market UI, where per-offer rates match to the cent.
261
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
262
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
263
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
264
+ *
265
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
266
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
267
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
248
268
  *
249
- * Returns `null` for an empty book: there is nothing to borrow against, so a market listing should skip
250
- * the market rather than advertise it at a 0% rate. Throws when the request fails — an error response is
251
- * rarely JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
269
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
270
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
271
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
252
272
  */
253
273
  export const getMorphoMidnightMarketBook = async (
254
274
  market: MorphoMidnightMarketData,
255
275
  network: NetworkNumber,
276
+ side: MorphoMidnightBookSide = 'bids',
256
277
  ): Promise<MorphoMidnightParsedBook | null> => {
257
278
  const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
258
279
  const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
259
280
  if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
260
281
 
261
- const json: { data?: { bids?: MidnightRawBid[] } } = await res.json();
282
+ const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
262
283
  const ttmDays = midnightTimeToMaturityDays(market.maturity);
284
+ const bestFirst = side === 'asks' ? -1 : 1;
263
285
 
264
- const offers: MorphoMidnightBookOffer[] = (json?.data?.bids || [])
265
- .map((bid) => ({
266
- rate: midnightApyFromPrice(new Dec(bid.price).div(WAD), ttmDays),
267
- liquidity: assetAmountInEth(bid.assets, loanSymbol),
286
+ const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || [])
287
+ .map((offer) => ({
288
+ rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
289
+ liquidity: assetAmountInEth(offer.assets, loanSymbol),
268
290
  }))
269
- .sort((a, b) => new Dec(a.rate).minus(b.rate).toNumber());
291
+ .sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
270
292
 
271
293
  if (offers.length === 0) return null;
272
294
 
@@ -286,6 +308,44 @@ const midnightQuoteError = (error?: MidnightApiError): string => {
286
308
  return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
287
309
  };
288
310
 
311
+ interface MidnightParsedQuote {
312
+ bestPrice: string, // loan-per-unit
313
+ worstPrice: string, // slippage-adjusted; below best on `bids`, above it on `asks`
314
+ availableAssets: string,
315
+ availableUnits: string,
316
+ takeableOffers: any[],
317
+ }
318
+
319
+ /**
320
+ * How much of the book to quote. The endpoint takes exactly one of the two — it rejects a request with
321
+ * neither ("Either assets or units must be provided") — and answers the same `takeable_offers` list either
322
+ * way, since that list is the whole in-band depth rather than the slice this size consumes.
323
+ */
324
+ type MidnightQuoteSize = { assets: string } | { units: string };
325
+
326
+ // The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
327
+ const fetchMorphoMidnightQuote = async (
328
+ marketId: string,
329
+ side: MorphoMidnightBookSide,
330
+ size: MidnightQuoteSize,
331
+ slippagePercent: Dec.Value,
332
+ ): Promise<MidnightParsedQuote> => {
333
+ const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
334
+ const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${midnightSlippageParam(slippagePercent)}`;
335
+ const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
336
+ const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
337
+ const d = json?.data;
338
+ if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
339
+
340
+ return {
341
+ bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
342
+ worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
343
+ availableAssets: d.available_assets || '0',
344
+ availableUnits: d.available_units || '0',
345
+ takeableOffers: d.takeable_offers || [],
346
+ };
347
+ };
348
+
289
349
  /**
290
350
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
291
351
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -310,14 +370,8 @@ export const getMorphoMidnightBorrowQuote = async (
310
370
  maturity: number,
311
371
  maxBorrowRate?: Dec.Value,
312
372
  ): Promise<MorphoMidnightBorrowQuote> => {
313
- const url = `${MIDNIGHT_API_BASE}/books/${marketId}/bids/quote?assets=${assetsRaw}&slippage=${midnightSlippageParam(slippagePercent)}`;
314
- const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
315
- const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
316
- const d = json?.data;
317
- if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
318
-
319
- const bestPrice = new Dec(d.average_best_price).div(WAD).toString();
320
- const worstPrice = new Dec(d.average_worst_price || 0).div(WAD).toString();
373
+ const quote = await fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
374
+ const { bestPrice, worstPrice } = quote;
321
375
  const ttmDays = midnightTimeToMaturityDays(maturity);
322
376
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
323
377
 
@@ -331,14 +385,104 @@ export const getMorphoMidnightBorrowQuote = async (
331
385
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
332
386
 
333
387
  return {
334
- bestPrice,
335
- worstPrice,
388
+ ...quote,
336
389
  estBorrowRate,
337
390
  maxRate,
338
391
  newUnits,
339
392
  maxUnits,
340
- availableAssets: d.available_assets || '0',
341
- availableUnits: d.available_units || '0',
342
- takeableOffers: d.takeable_offers || [],
393
+ };
394
+ };
395
+
396
+ /**
397
+ * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
398
+ * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
399
+ * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
400
+ * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
401
+ * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
402
+ * Throws if the book can't fill the amount (caller handles).
403
+ *
404
+ * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
405
+ * so the guard is a floor rather than a ceiling:
406
+ * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
407
+ * this when a user pins a min rate.
408
+ * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
409
+ * annualisation factor near maturity. `minRate` reports what the floor actually permits.
410
+ *
411
+ * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
412
+ * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
413
+ */
414
+ export const getMorphoMidnightPaybackQuote = async (
415
+ marketId: string,
416
+ assetsRaw: string,
417
+ slippagePercent: Dec.Value,
418
+ maturity: number,
419
+ minPaybackRate?: Dec.Value,
420
+ ): Promise<MorphoMidnightPaybackQuote> => {
421
+ const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
422
+ const { bestPrice, worstPrice } = quote;
423
+ const ttmDays = midnightTimeToMaturityDays(maturity);
424
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
425
+
426
+ const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
427
+ ? midnightPriceFromApy(minPaybackRate, ttmDays)
428
+ : worstPrice;
429
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
430
+ // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
431
+ // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
432
+ const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
433
+ const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
434
+
435
+ return {
436
+ ...quote,
437
+ estPaybackRate,
438
+ minRate,
439
+ newUnits,
440
+ minUnits,
441
+ };
442
+ };
443
+
444
+ /**
445
+ * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
446
+ * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
447
+ * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
448
+ * a ceiling on assets spent rather than a floor on units bought.
449
+ *
450
+ * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
451
+ * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
452
+ * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
453
+ * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
454
+ * directly, rather than through an action contract that sweeps the remainder back — need this quote.
455
+ *
456
+ * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
457
+ * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
458
+ */
459
+ export const getMorphoMidnightPaybackUnitsQuote = async (
460
+ marketId: string,
461
+ unitsRaw: string,
462
+ slippagePercent: Dec.Value,
463
+ maturity: number,
464
+ minPaybackRate?: Dec.Value,
465
+ ): Promise<MorphoMidnightPaybackUnitsQuote> => {
466
+ const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
467
+ const { bestPrice, worstPrice } = quote;
468
+ const ttmDays = midnightTimeToMaturityDays(maturity);
469
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
470
+
471
+ const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
472
+ ? midnightPriceFromApy(minPaybackRate, ttmDays)
473
+ : worstPrice;
474
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
475
+ // Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
476
+ // what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
477
+ // buy short of the units it was asked for.
478
+ const newAssets = new Dec(unitsRaw).mul(bestPrice).toFixed(0, Dec.ROUND_UP);
479
+ const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
480
+
481
+ return {
482
+ ...quote,
483
+ estPaybackRate,
484
+ minRate,
485
+ newAssets,
486
+ maxAssets,
343
487
  };
344
488
  };
@@ -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,16 +70,20 @@ export interface MorphoMidnightMarketInfo {
70
70
  assetsData: MorphoMidnightAssetsData,
71
71
  }
72
72
 
73
- // One resting bid on a market's order book, as an annualized rate rather than the API's raw WAD price.
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.
74
78
  export interface MorphoMidnightBookOffer {
75
- rate: string, // fixed borrow APY, percent
79
+ rate: string, // fixed APY, percent
76
80
  liquidity: string, // loan-token amount available at this rate
77
81
  }
78
82
 
79
83
  export interface MorphoMidnightParsedBook {
80
- bestRate: string, // cheapest rate on the book (= offers[0].rate)
84
+ bestRate: string, // best rate for the taker of this side (= offers[0].rate)
81
85
  totalLiquidity: string, // Σ offers[].liquidity, loan-token units
82
- offers: MorphoMidnightBookOffer[], // ascending by rate
86
+ offers: MorphoMidnightBookOffer[], // best-first: bids ascending by rate, asks descending
83
87
  }
84
88
 
85
89
  export interface MorphoMidnightAggregatedPositionData {