@defisaver/positions-sdk 2.1.127-midnight-3-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
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,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,22 +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>;
61
75
  /**
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.
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`.
65
83
  *
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.
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.
69
87
  */
70
- export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber) => Promise<MorphoMidnightParsedBook | null>;
88
+ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
71
89
  /**
72
90
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
73
91
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -86,3 +104,22 @@ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketD
86
104
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
87
105
  */
88
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.getMorphoMidnightMarketBook = 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");
@@ -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,22 @@ 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, 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
+ });
210
220
  /**
211
221
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
212
222
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -225,14 +235,8 @@ const midnightQuoteError = (error) => {
225
235
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
226
236
  */
227
237
  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();
238
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
239
+ const { bestPrice, worstPrice } = quote;
236
240
  const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
237
241
  const estBorrowRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
238
242
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
@@ -243,16 +247,46 @@ const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, matu
243
247
  const maxRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
244
248
  const newUnits = new decimal_js_1.default(bestPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(bestPrice).toFixed(0);
245
249
  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,
250
+ return Object.assign(Object.assign({}, quote), { estBorrowRate,
250
251
  maxRate,
251
252
  newUnits,
252
- maxUnits,
253
- availableAssets: d.available_assets || '0',
254
- availableUnits: d.available_units || '0',
255
- takeableOffers: d.takeable_offers || [],
256
- };
253
+ maxUnits });
257
254
  });
258
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,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,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,22 +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>;
61
75
  /**
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.
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`.
65
83
  *
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.
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.
69
87
  */
70
- export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber) => Promise<MorphoMidnightParsedBook | null>;
88
+ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
71
89
  /**
72
90
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
73
91
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -86,3 +104,22 @@ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketD
86
104
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
87
105
  */
88
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>;
@@ -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,22 @@ 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, 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
+ });
197
207
  /**
198
208
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
199
209
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -212,14 +222,8 @@ const midnightQuoteError = (error) => {
212
222
  * Compare the two before submitting and tell the user their ceiling is under the market rate.
213
223
  */
214
224
  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();
225
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
226
+ const { bestPrice, worstPrice } = quote;
223
227
  const ttmDays = midnightTimeToMaturityDays(maturity);
224
228
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
225
229
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
@@ -230,15 +234,44 @@ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercen
230
234
  const maxRate = midnightApyFromPrice(capPrice, ttmDays);
231
235
  const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
232
236
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
233
- return {
234
- bestPrice,
235
- worstPrice,
236
- estBorrowRate,
237
+ return Object.assign(Object.assign({}, quote), { estBorrowRate,
237
238
  maxRate,
238
239
  newUnits,
239
- maxUnits,
240
- availableAssets: d.available_assets || '0',
241
- availableUnits: d.available_units || '0',
242
- takeableOffers: d.takeable_offers || [],
243
- };
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 });
244
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,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-4-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,18 @@ 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
+
162
176
  // Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
163
177
  export const midnightTimeToMaturityDays = (maturity: number, atSeconds: number = nowInSeconds()): number => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
164
178
 
@@ -201,40 +215,28 @@ export const midnightSlippageParam = (slippagePercent: Dec.Value): string => Dec
201
215
  ).toDP(1, Dec.ROUND_DOWN).toString();
202
216
 
203
217
  /**
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.
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.
207
224
  * The caller swallows errors — a missing rate must never block position rendering.
208
225
  */
209
226
  export const getMorphoMidnightUserBorrowInfo = async (
210
227
  account: string,
211
228
  marketId: string,
212
- maturity: number,
213
229
  loanTokenSymbol: string,
214
230
  ): 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);
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();
237
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();
238
240
 
239
241
  return {
240
242
  borrowRate, debtBase, debtInterest, debtTotal,
@@ -242,31 +244,37 @@ export const getMorphoMidnightUserBorrowInfo = async (
242
244
  };
243
245
 
244
246
  /**
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.
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.
248
250
  *
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.
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.
252
258
  */
253
259
  export const getMorphoMidnightMarketBook = async (
254
260
  market: MorphoMidnightMarketData,
255
261
  network: NetworkNumber,
262
+ side: MorphoMidnightBookSide = 'bids',
256
263
  ): Promise<MorphoMidnightParsedBook | null> => {
257
264
  const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
258
265
  const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
259
266
  if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
260
267
 
261
- const json: { data?: { bids?: MidnightRawBid[] } } = await res.json();
268
+ const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
262
269
  const ttmDays = midnightTimeToMaturityDays(market.maturity);
270
+ const bestFirst = side === 'asks' ? -1 : 1;
263
271
 
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),
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),
268
276
  }))
269
- .sort((a, b) => new Dec(a.rate).minus(b.rate).toNumber());
277
+ .sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
270
278
 
271
279
  if (offers.length === 0) return null;
272
280
 
@@ -286,6 +294,36 @@ const midnightQuoteError = (error?: MidnightApiError): string => {
286
294
  return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
287
295
  };
288
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
+
289
327
  /**
290
328
  * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
291
329
  * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
@@ -310,14 +348,8 @@ export const getMorphoMidnightBorrowQuote = async (
310
348
  maturity: number,
311
349
  maxBorrowRate?: Dec.Value,
312
350
  ): 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();
351
+ const quote = await fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
352
+ const { bestPrice, worstPrice } = quote;
321
353
  const ttmDays = midnightTimeToMaturityDays(maturity);
322
354
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
323
355
 
@@ -331,14 +363,58 @@ export const getMorphoMidnightBorrowQuote = async (
331
363
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
332
364
 
333
365
  return {
334
- bestPrice,
335
- worstPrice,
366
+ ...quote,
336
367
  estBorrowRate,
337
368
  maxRate,
338
369
  newUnits,
339
370
  maxUnits,
340
- availableAssets: d.available_assets || '0',
341
- availableUnits: d.available_units || '0',
342
- 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,
343
419
  };
344
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,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 {