@defisaver/positions-sdk 2.1.127-midnight-9-dev → 2.1.127-midnight-11-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,7 +1,7 @@
1
1
  import Dec from 'decimal.js';
2
2
  import { MMUsedAssets, NetworkNumber } from '../../types/common';
3
3
  import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightBookSide, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
4
- export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightPriceFromApy, midnightTimeToMaturityDays, } from './rate';
4
+ export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightBoundPrice, midnightPriceFromApy, midnightTimeToMaturityDays, MIDNIGHT_DEFAULT_RATE_SLIPPAGE, } from './rate';
5
5
  export { tenorBookKeyFor, tenorBookRateToApyPercent, tenorOfferFillToApiFill, tenorOfferToApiOffer, } from './tenor';
6
6
  /**
7
7
  * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
@@ -88,59 +88,6 @@ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId
88
88
  * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
89
89
  */
90
90
  export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
91
- /**
92
- * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
93
- * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
94
- * (and the returned `newUnits`/`maxUnits`) are raw loan-token base units — callers convert to/from human
95
- * amounts. Throws if the book can't fill the amount (caller handles).
96
- *
97
- * Two ways to set the cap:
98
- * - `maxBorrowRate` — an absolute APY ceiling, honoured **exactly**: the cap price is derived locally via
99
- * `midnightPriceFromApy`. Prefer this when a user pins a max rate.
100
- * - otherwise `slippagePercent`, the API's own knob. Note it is a **price**-level slippage, not APY points:
101
- * near maturity the annualisation factor (365 / ttmDays) multiplies it heavily, so on a 22-day market a
102
- * slippage of 0.5 permitted an APY ~9pp above the estimate, not 0.5pp. It also saturates at the book's
103
- * cheapest bid. `maxRate` therefore reports what the cap actually permits, derived from the cap price.
104
- *
105
- * A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
106
- * Compare the two before submitting and tell the user their ceiling is under the market rate.
107
- *
108
- * Tenor-hosted markets (see `isTenorMidnightMarket`) are quoted against Tenor's router instead of Morpho's
109
- * book API. Slippage is applied locally to `maxUnits`; `taker` is the position owner (the smart wallet).
110
- */
111
- export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightBorrowQuote>;
112
- /**
113
- * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
114
- * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
115
- * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
116
- * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
117
- * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
118
- * Throws if the book can't fill the amount (caller handles).
119
- *
120
- * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
121
- * so the guard is a floor rather than a ceiling:
122
- * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
123
- * this when a user pins a min rate.
124
- * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
125
- * annualisation factor near maturity. `minRate` reports what the floor actually permits.
126
- *
127
- * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
128
- * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
129
- */
130
- export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightPaybackQuote>;
131
- /**
132
- * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
133
- * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
134
- * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
135
- * a ceiling on assets spent rather than a floor on units bought.
136
- *
137
- * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
138
- * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
139
- * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
140
- * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
141
- * directly, rather than through an action contract that sweeps the remainder back — need this quote.
142
- *
143
- * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
144
- * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
145
- */
146
- export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightPaybackUnitsQuote>;
91
+ export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
92
+ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
93
+ export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: 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.getMorphoMidnightPaybackUnitsQuote = exports.getMorphoMidnightPaybackQuote = exports.getMorphoMidnightBorrowQuote = exports.getMorphoMidnightMarketBook = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.getMorphoMidnightAggregatedPositionData = exports.tenorOfferToApiOffer = exports.tenorOfferFillToApiFill = exports.tenorBookRateToApyPercent = exports.tenorBookKeyFor = exports.midnightTimeToMaturityDays = exports.midnightPriceFromApy = exports.midnightBookBestFirst = exports.midnightApyFromPrice = exports.buildMidnightParsedBook = void 0;
15
+ exports.getMorphoMidnightPaybackUnitsQuote = exports.getMorphoMidnightPaybackQuote = exports.getMorphoMidnightBorrowQuote = exports.getMorphoMidnightMarketBook = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.getMorphoMidnightAggregatedPositionData = exports.tenorOfferToApiOffer = exports.tenorOfferFillToApiFill = exports.tenorBookRateToApyPercent = exports.tenorBookKeyFor = exports.MIDNIGHT_DEFAULT_RATE_SLIPPAGE = exports.midnightTimeToMaturityDays = exports.midnightPriceFromApy = exports.midnightBoundPrice = exports.midnightBookBestFirst = exports.midnightApyFromPrice = exports.buildMidnightParsedBook = 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");
@@ -27,8 +27,10 @@ var rate_2 = require("./rate");
27
27
  Object.defineProperty(exports, "buildMidnightParsedBook", { enumerable: true, get: function () { return rate_2.buildMidnightParsedBook; } });
28
28
  Object.defineProperty(exports, "midnightApyFromPrice", { enumerable: true, get: function () { return rate_2.midnightApyFromPrice; } });
29
29
  Object.defineProperty(exports, "midnightBookBestFirst", { enumerable: true, get: function () { return rate_2.midnightBookBestFirst; } });
30
+ Object.defineProperty(exports, "midnightBoundPrice", { enumerable: true, get: function () { return rate_2.midnightBoundPrice; } });
30
31
  Object.defineProperty(exports, "midnightPriceFromApy", { enumerable: true, get: function () { return rate_2.midnightPriceFromApy; } });
31
32
  Object.defineProperty(exports, "midnightTimeToMaturityDays", { enumerable: true, get: function () { return rate_2.midnightTimeToMaturityDays; } });
33
+ Object.defineProperty(exports, "MIDNIGHT_DEFAULT_RATE_SLIPPAGE", { enumerable: true, get: function () { return rate_2.MIDNIGHT_DEFAULT_RATE_SLIPPAGE; } });
32
34
  var tenor_2 = require("./tenor");
33
35
  Object.defineProperty(exports, "tenorBookKeyFor", { enumerable: true, get: function () { return tenor_2.tenorBookKeyFor; } });
34
36
  Object.defineProperty(exports, "tenorBookRateToApyPercent", { enumerable: true, get: function () { return tenor_2.tenorBookRateToApyPercent; } });
@@ -194,39 +196,17 @@ const fetchMorphoMidnightQuote = (marketId, side, size, slippagePercent) => __aw
194
196
  takeableOffers: d.takeable_offers || [],
195
197
  };
196
198
  });
197
- /**
198
- * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
199
- * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
200
- * (and the returned `newUnits`/`maxUnits`) are raw loan-token base units — callers convert to/from human
201
- * amounts. Throws if the book can't fill the amount (caller handles).
202
- *
203
- * Two ways to set the cap:
204
- * - `maxBorrowRate` — an absolute APY ceiling, honoured **exactly**: the cap price is derived locally via
205
- * `midnightPriceFromApy`. Prefer this when a user pins a max rate.
206
- * - otherwise `slippagePercent`, the API's own knob. Note it is a **price**-level slippage, not APY points:
207
- * near maturity the annualisation factor (365 / ttmDays) multiplies it heavily, so on a 22-day market a
208
- * slippage of 0.5 permitted an APY ~9pp above the estimate, not 0.5pp. It also saturates at the book's
209
- * cheapest bid. `maxRate` therefore reports what the cap actually permits, derived from the cap price.
210
- *
211
- * A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
212
- * Compare the two before submitting and tell the user their ceiling is under the market rate.
213
- *
214
- * Tenor-hosted markets (see `isTenorMidnightMarket`) are quoted against Tenor's router instead of Morpho's
215
- * book API. Slippage is applied locally to `maxUnits`; `taker` is the position owner (the smart wallet).
216
- */
217
- const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
199
+ const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
218
200
  if ((0, morphoMidnight_1.isTenorMidnightMarket)(marketId)) {
219
- return (0, tenor_1.getTenorBorrowQuote)(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker);
201
+ return (0, tenor_1.getTenorBorrowQuote)(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent);
220
202
  }
221
203
  const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
222
- const { bestPrice, worstPrice } = quote;
204
+ const { bestPrice } = quote;
223
205
  const ttmDays = (0, rate_1.midnightTimeToMaturityDays)(maturity);
224
206
  const estBorrowRate = (0, rate_1.midnightApyFromPrice)(bestPrice, ttmDays);
225
207
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
226
208
  // `maxUnits` can never disagree about what the user is protected at.
227
- const capPrice = maxBorrowRate !== undefined && new decimal_js_1.default(maxBorrowRate).gt(0)
228
- ? (0, rate_1.midnightPriceFromApy)(maxBorrowRate, ttmDays)
229
- : worstPrice;
209
+ const capPrice = (0, rate_1.midnightBoundPrice)(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
230
210
  const maxRate = (0, rate_1.midnightApyFromPrice)(capPrice, ttmDays);
231
211
  const newUnits = new decimal_js_1.default(bestPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(bestPrice).toFixed(0);
232
212
  const maxUnits = new decimal_js_1.default(capPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(capPrice).toFixed(0);
@@ -236,35 +216,15 @@ const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, matu
236
216
  maxUnits });
237
217
  });
238
218
  exports.getMorphoMidnightBorrowQuote = getMorphoMidnightBorrowQuote;
239
- /**
240
- * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
241
- * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
242
- * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
243
- * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
244
- * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
245
- * Throws if the book can't fill the amount (caller handles).
246
- *
247
- * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
248
- * so the guard is a floor rather than a ceiling:
249
- * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
250
- * this when a user pins a min rate.
251
- * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
252
- * annualisation factor near maturity. `minRate` reports what the floor actually permits.
253
- *
254
- * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
255
- * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
256
- */
257
- const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
219
+ const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
258
220
  if ((0, morphoMidnight_1.isTenorMidnightMarket)(marketId)) {
259
- return (0, tenor_1.getTenorPaybackQuote)(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker);
221
+ return (0, tenor_1.getTenorPaybackQuote)(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
260
222
  }
261
223
  const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
262
- const { bestPrice, worstPrice } = quote;
224
+ const { bestPrice } = quote;
263
225
  const ttmDays = (0, rate_1.midnightTimeToMaturityDays)(maturity);
264
226
  const estPaybackRate = (0, rate_1.midnightApyFromPrice)(bestPrice, ttmDays);
265
- const capPrice = minPaybackRate !== undefined && new decimal_js_1.default(minPaybackRate).gt(0)
266
- ? (0, rate_1.midnightPriceFromApy)(minPaybackRate, ttmDays)
267
- : worstPrice;
227
+ const capPrice = (0, rate_1.midnightBoundPrice)(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
268
228
  const minRate = (0, rate_1.midnightApyFromPrice)(capPrice, ttmDays);
269
229
  // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
270
230
  // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
@@ -276,32 +236,15 @@ const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, mat
276
236
  minUnits });
277
237
  });
278
238
  exports.getMorphoMidnightPaybackQuote = getMorphoMidnightPaybackQuote;
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
- const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
239
+ const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
295
240
  if ((0, morphoMidnight_1.isTenorMidnightMarket)(marketId)) {
296
- return (0, tenor_1.getTenorPaybackUnitsQuote)(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker);
241
+ return (0, tenor_1.getTenorPaybackUnitsQuote)(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
297
242
  }
298
243
  const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
299
- const { bestPrice, worstPrice } = quote;
244
+ const { bestPrice } = quote;
300
245
  const ttmDays = (0, rate_1.midnightTimeToMaturityDays)(maturity);
301
246
  const estPaybackRate = (0, rate_1.midnightApyFromPrice)(bestPrice, ttmDays);
302
- const capPrice = minPaybackRate !== undefined && new decimal_js_1.default(minPaybackRate).gt(0)
303
- ? (0, rate_1.midnightPriceFromApy)(minPaybackRate, ttmDays)
304
- : worstPrice;
247
+ const capPrice = (0, rate_1.midnightBoundPrice)(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
305
248
  const minRate = (0, rate_1.midnightApyFromPrice)(capPrice, ttmDays);
306
249
  // Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
307
250
  // what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
@@ -7,5 +7,9 @@ export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value
7
7
  * price = (1 + rate)^(−ttmDays / 365).
8
8
  */
9
9
  export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec.Value) => string;
10
+ export declare const MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
11
+ /** Which side of the estimate a guard sits on: a ceiling for borrows, a floor for paybacks. */
12
+ export type MidnightRateBoundKind = 'ceiling' | 'floor';
13
+ export declare const midnightBoundPrice: (estRate: Dec.Value, ttmDays: Dec.Value, kind: MidnightRateBoundKind, boundRate?: Dec.Value, rateSlippagePercent?: Dec.Value) => string;
10
14
  export declare const midnightBookBestFirst: (side: MorphoMidnightBookSide) => 1 | -1;
11
15
  export declare const buildMidnightParsedBook: (offers: MorphoMidnightBookOffer[], side: MorphoMidnightBookSide) => MorphoMidnightParsedBook | null;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.buildMidnightParsedBook = exports.midnightBookBestFirst = exports.midnightPriceFromApy = exports.midnightApyFromPrice = exports.midnightTimeToMaturityDays = void 0;
6
+ exports.buildMidnightParsedBook = exports.midnightBookBestFirst = exports.midnightBoundPrice = exports.MIDNIGHT_DEFAULT_RATE_SLIPPAGE = exports.midnightPriceFromApy = exports.midnightApyFromPrice = exports.midnightTimeToMaturityDays = void 0;
7
7
  const decimal_js_1 = __importDefault(require("decimal.js"));
8
8
  const constants_1 = require("../../constants");
9
9
  const nowInSeconds = () => Math.floor(Date.now() / 1000);
@@ -34,6 +34,19 @@ const midnightPriceFromApy = (ratePercent, ttmDays) => {
34
34
  return new decimal_js_1.default(1).div(new decimal_js_1.default(1).add(rate.div(100)).pow(ttm.div(365))).toString();
35
35
  };
36
36
  exports.midnightPriceFromApy = midnightPriceFromApy;
37
+ exports.MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
38
+ const MIDNIGHT_MIN_FLOOR_RATIO = 0.5;
39
+ const midnightBoundPrice = (estRate, ttmDays, kind, boundRate, rateSlippagePercent = exports.MIDNIGHT_DEFAULT_RATE_SLIPPAGE) => {
40
+ if (boundRate !== undefined && new decimal_js_1.default(boundRate).gt(0))
41
+ return (0, exports.midnightPriceFromApy)(boundRate, ttmDays);
42
+ const est = new decimal_js_1.default(estRate);
43
+ const slippage = new decimal_js_1.default(rateSlippagePercent);
44
+ const bound = kind === 'ceiling'
45
+ ? est.add(slippage)
46
+ : decimal_js_1.default.max(est.sub(slippage), est.mul(MIDNIGHT_MIN_FLOOR_RATIO));
47
+ return (0, exports.midnightPriceFromApy)(bound, ttmDays);
48
+ };
49
+ exports.midnightBoundPrice = midnightBoundPrice;
37
50
  const midnightBookBestFirst = (side) => (side === 'asks' ? -1 : 1);
38
51
  exports.midnightBookBestFirst = midnightBookBestFirst;
39
52
  const buildMidnightParsedBook = (offers, side) => {
@@ -4,7 +4,6 @@ import type { MorphoMidnightBorrowQuote, MorphoMidnightPaybackQuote, MorphoMidni
4
4
  type TenorBookKey = 'asks' | 'bids';
5
5
  export declare const tenorBookKeyFor: (side: MorphoMidnightBookSide) => TenorBookKey;
6
6
  export declare const tenorBookRateToApyPercent: (rate: Dec.Value) => string;
7
- export declare const tenorCapPrice: (bestPrice: string, slippagePercent: Dec.Value, direction: 1 | -1, ttmDays: Dec.Value, boundRate?: Dec.Value) => string;
8
7
  interface TenorOfferCollateral {
9
8
  token: string;
10
9
  lltv: string | number;
@@ -119,7 +118,12 @@ export declare const parseTenorBookSide: (json: TenorBookResponse, side: MorphoM
119
118
  }[];
120
119
  export declare const parseTenorOrderBook: (json: TenorBookResponse, side: MorphoMidnightBookSide, loanSymbol: string) => MorphoMidnightParsedBook | null;
121
120
  export declare const getTenorMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
122
- export declare const getTenorBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string, network?: NetworkNumber) => Promise<MorphoMidnightBorrowQuote>;
123
- export declare const getTenorPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, network?: NetworkNumber) => Promise<MorphoMidnightPaybackQuote>;
124
- export declare const getTenorPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, network?: NetworkNumber) => Promise<MorphoMidnightPaybackUnitsQuote>;
121
+ /**
122
+ * Tenor's router takes no slippage of its own it prices the fill and hands back the offers. `slippagePercent`
123
+ * is therefore accepted only to keep the signature aligned with the Morpho-hosted quote that `index.ts`
124
+ * forwards to positionally; the guard comes from `rateSlippagePercent` via `midnightBoundPrice`.
125
+ */
126
+ export declare const getTenorBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value, network?: NetworkNumber) => Promise<MorphoMidnightBorrowQuote>;
127
+ export declare const getTenorPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value, network?: NetworkNumber) => Promise<MorphoMidnightPaybackQuote>;
128
+ export declare const getTenorPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value, network?: NetworkNumber) => Promise<MorphoMidnightPaybackUnitsQuote>;
125
129
  export {};
@@ -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.getTenorPaybackUnitsQuote = exports.getTenorPaybackQuote = exports.getTenorBorrowQuote = exports.getTenorMarketBook = exports.parseTenorOrderBook = exports.parseTenorBookSide = exports.tenorOfferFillToApiFill = exports.tenorOfferToApiOffer = exports.tenorCapPrice = exports.tenorBookRateToApyPercent = exports.tenorBookKeyFor = void 0;
15
+ exports.getTenorPaybackUnitsQuote = exports.getTenorPaybackQuote = exports.getTenorBorrowQuote = exports.getTenorMarketBook = exports.parseTenorOrderBook = exports.parseTenorBookSide = exports.tenorOfferFillToApiFill = exports.tenorOfferToApiOffer = exports.tenorBookRateToApyPercent = exports.tenorBookKeyFor = void 0;
16
16
  const decimal_js_1 = __importDefault(require("decimal.js"));
17
17
  const tokens_1 = require("@defisaver/tokens");
18
18
  const constants_1 = require("../../constants");
@@ -31,13 +31,6 @@ exports.tenorBookKeyFor = tenorBookKeyFor;
31
31
  const tenorBookRateToApyPercent = (rate) => new decimal_js_1.default(rate || 0).div(100).toString();
32
32
  exports.tenorBookRateToApyPercent = tenorBookRateToApyPercent;
33
33
  const tenorFillPrice = (assets, units) => (new decimal_js_1.default(units).lte(0) ? '0' : new decimal_js_1.default(assets).div(units).toString());
34
- const tenorCapPrice = (bestPrice, slippagePercent, direction, ttmDays, boundRate) => {
35
- if (boundRate !== undefined && new decimal_js_1.default(boundRate).gt(0))
36
- return (0, rate_1.midnightPriceFromApy)(boundRate, ttmDays);
37
- const band = new decimal_js_1.default(1).add(new decimal_js_1.default(slippagePercent || 0).div(100).mul(direction));
38
- return decimal_js_1.default.max(0, new decimal_js_1.default(bestPrice).mul(band)).toString();
39
- };
40
- exports.tenorCapPrice = tenorCapPrice;
41
34
  /**
42
35
  * Tenor's offer JSON is flat (market fields live on the offer). Morpho's is nested, and that nested
43
36
  * shape is what the app encodes for `Midnight.take`. Map Tenor into that shape so recipes stay on one encoder.
@@ -166,7 +159,12 @@ const fetchTenorQuote = (_a) => __awaiter(void 0, [_a], void 0, function* ({ mar
166
159
  };
167
160
  });
168
161
  const TENOR_NO_AVAILABLE_UNITS = '0';
169
- const getTenorBorrowQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, maxBorrowRate_1, taker_1, ...args_1) => __awaiter(void 0, [marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, maxBorrowRate_1, taker_1, ...args_1], void 0, function* (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, network = types_1.NetworkNumber.Base) {
162
+ /**
163
+ * Tenor's router takes no slippage of its own — it prices the fill and hands back the offers. `slippagePercent`
164
+ * is therefore accepted only to keep the signature aligned with the Morpho-hosted quote that `index.ts`
165
+ * forwards to positionally; the guard comes from `rateSlippagePercent` via `midnightBoundPrice`.
166
+ */
167
+ const getTenorBorrowQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, maxBorrowRate_1, taker_1, rateSlippagePercent_1, ...args_1) => __awaiter(void 0, [marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, maxBorrowRate_1, taker_1, rateSlippagePercent_1, ...args_1], void 0, function* (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent, network = types_1.NetworkNumber.Base) {
170
168
  if (!(0, morphoMidnight_1.isTenorMidnightMarket)(marketId)) {
171
169
  throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
172
170
  }
@@ -179,7 +177,7 @@ const getTenorBorrowQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturit
179
177
  const ttmDays = (0, rate_1.midnightTimeToMaturityDays)(maturity);
180
178
  const bestPrice = tenorFillPrice(assetsRaw, quote.units);
181
179
  const estBorrowRate = (0, rate_1.midnightApyFromPrice)(bestPrice, ttmDays);
182
- const capPrice = (0, exports.tenorCapPrice)(bestPrice, slippagePercent, -1, ttmDays, maxBorrowRate);
180
+ const capPrice = (0, rate_1.midnightBoundPrice)(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
183
181
  const maxRate = (0, rate_1.midnightApyFromPrice)(capPrice, ttmDays);
184
182
  const maxUnits = new decimal_js_1.default(capPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(capPrice).toFixed(0);
185
183
  return {
@@ -195,7 +193,7 @@ const getTenorBorrowQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturit
195
193
  };
196
194
  });
197
195
  exports.getTenorBorrowQuote = getTenorBorrowQuote;
198
- const getTenorPaybackQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, ...args_1) => __awaiter(void 0, [marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, ...args_1], void 0, function* (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, network = types_1.NetworkNumber.Base) {
196
+ const getTenorPaybackQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, rateSlippagePercent_1, ...args_1) => __awaiter(void 0, [marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, rateSlippagePercent_1, ...args_1], void 0, function* (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent, network = types_1.NetworkNumber.Base) {
199
197
  if (!(0, morphoMidnight_1.isTenorMidnightMarket)(marketId)) {
200
198
  throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
201
199
  }
@@ -208,7 +206,7 @@ const getTenorPaybackQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturi
208
206
  const ttmDays = (0, rate_1.midnightTimeToMaturityDays)(maturity);
209
207
  const bestPrice = tenorFillPrice(assetsRaw, quote.units);
210
208
  const estPaybackRate = (0, rate_1.midnightApyFromPrice)(bestPrice, ttmDays);
211
- const capPrice = (0, exports.tenorCapPrice)(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
209
+ const capPrice = (0, rate_1.midnightBoundPrice)(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
212
210
  const minRate = (0, rate_1.midnightApyFromPrice)(capPrice, ttmDays);
213
211
  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);
214
212
  return {
@@ -224,7 +222,7 @@ const getTenorPaybackQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturi
224
222
  };
225
223
  });
226
224
  exports.getTenorPaybackQuote = getTenorPaybackQuote;
227
- const getTenorPaybackUnitsQuote = (marketId_1, unitsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, ...args_1) => __awaiter(void 0, [marketId_1, unitsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, ...args_1], void 0, function* (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, network = types_1.NetworkNumber.Base) {
225
+ const getTenorPaybackUnitsQuote = (marketId_1, unitsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, rateSlippagePercent_1, ...args_1) => __awaiter(void 0, [marketId_1, unitsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, rateSlippagePercent_1, ...args_1], void 0, function* (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent, network = types_1.NetworkNumber.Base) {
228
226
  if (!(0, morphoMidnight_1.isTenorMidnightMarket)(marketId)) {
229
227
  throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
230
228
  }
@@ -238,7 +236,7 @@ const getTenorPaybackUnitsQuote = (marketId_1, unitsRaw_1, slippagePercent_1, ma
238
236
  const newAssets = quote.buyerAssets;
239
237
  const bestPrice = tenorFillPrice(newAssets, unitsRaw);
240
238
  const estPaybackRate = (0, rate_1.midnightApyFromPrice)(bestPrice, ttmDays);
241
- const capPrice = (0, exports.tenorCapPrice)(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
239
+ const capPrice = (0, rate_1.midnightBoundPrice)(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
242
240
  const minRate = (0, rate_1.midnightApyFromPrice)(capPrice, ttmDays);
243
241
  const maxAssets = new decimal_js_1.default(unitsRaw).mul(capPrice).toFixed(0, decimal_js_1.default.ROUND_UP);
244
242
  return {
@@ -169,12 +169,14 @@ function _getMorphoMidnightAccountData(provider, network, account, selectedMarke
169
169
  if (new decimal_js_1.default(positionInfo.debt.toString()).gt(0)) {
170
170
  try {
171
171
  const borrowInfo = yield (0, morphoMidnightHelpers_1.getMorphoMidnightUserBorrowInfo)(account, marketId, marketInfo.loanToken);
172
- borrowRate = borrowInfo.borrowRate;
173
- debtBase = borrowInfo.debtBase;
174
- debtInterest = borrowInfo.debtInterest;
175
- usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
176
- // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
177
- assetsDataForApy = Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.loanToken]: Object.assign(Object.assign({}, loanTokenData), { borrowRate }) });
172
+ if (new decimal_js_1.default(borrowInfo.debtTotal).gt(0)) {
173
+ borrowRate = borrowInfo.borrowRate;
174
+ debtBase = borrowInfo.debtBase;
175
+ debtInterest = borrowInfo.debtInterest;
176
+ usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
177
+ // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
178
+ assetsDataForApy = Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.loanToken]: Object.assign(Object.assign({}, loanTokenData), { borrowRate }) });
179
+ }
178
180
  }
179
181
  catch (err) {
180
182
  // Orderbook API unavailable — keep the on-chain-only fallback above.
@@ -1,7 +1,7 @@
1
1
  import Dec from 'decimal.js';
2
2
  import { MMUsedAssets, NetworkNumber } from '../../types/common';
3
3
  import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightBookSide, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
4
- export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightPriceFromApy, midnightTimeToMaturityDays, } from './rate';
4
+ export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightBoundPrice, midnightPriceFromApy, midnightTimeToMaturityDays, MIDNIGHT_DEFAULT_RATE_SLIPPAGE, } from './rate';
5
5
  export { tenorBookKeyFor, tenorBookRateToApyPercent, tenorOfferFillToApiFill, tenorOfferToApiOffer, } from './tenor';
6
6
  /**
7
7
  * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
@@ -88,59 +88,6 @@ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId
88
88
  * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
89
89
  */
90
90
  export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
91
- /**
92
- * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
93
- * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
94
- * (and the returned `newUnits`/`maxUnits`) are raw loan-token base units — callers convert to/from human
95
- * amounts. Throws if the book can't fill the amount (caller handles).
96
- *
97
- * Two ways to set the cap:
98
- * - `maxBorrowRate` — an absolute APY ceiling, honoured **exactly**: the cap price is derived locally via
99
- * `midnightPriceFromApy`. Prefer this when a user pins a max rate.
100
- * - otherwise `slippagePercent`, the API's own knob. Note it is a **price**-level slippage, not APY points:
101
- * near maturity the annualisation factor (365 / ttmDays) multiplies it heavily, so on a 22-day market a
102
- * slippage of 0.5 permitted an APY ~9pp above the estimate, not 0.5pp. It also saturates at the book's
103
- * cheapest bid. `maxRate` therefore reports what the cap actually permits, derived from the cap price.
104
- *
105
- * A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
106
- * Compare the two before submitting and tell the user their ceiling is under the market rate.
107
- *
108
- * Tenor-hosted markets (see `isTenorMidnightMarket`) are quoted against Tenor's router instead of Morpho's
109
- * book API. Slippage is applied locally to `maxUnits`; `taker` is the position owner (the smart wallet).
110
- */
111
- export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightBorrowQuote>;
112
- /**
113
- * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
114
- * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
115
- * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
116
- * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
117
- * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
118
- * Throws if the book can't fill the amount (caller handles).
119
- *
120
- * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
121
- * so the guard is a floor rather than a ceiling:
122
- * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
123
- * this when a user pins a min rate.
124
- * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
125
- * annualisation factor near maturity. `minRate` reports what the floor actually permits.
126
- *
127
- * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
128
- * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
129
- */
130
- export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightPaybackQuote>;
131
- /**
132
- * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
133
- * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
134
- * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
135
- * a ceiling on assets spent rather than a floor on units bought.
136
- *
137
- * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
138
- * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
139
- * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
140
- * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
141
- * directly, rather than through an action contract that sweeps the remainder back — need this quote.
142
- *
143
- * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
144
- * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
145
- */
146
- export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightPaybackUnitsQuote>;
91
+ export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
92
+ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
93
+ export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value) => Promise<MorphoMidnightPaybackUnitsQuote>;
@@ -15,9 +15,9 @@ import { LeverageType, } from '../../types/common';
15
15
  import { WAD } from '../../constants';
16
16
  import { LONGER_TIMEOUT } from '../../services/utils';
17
17
  import { isTenorMidnightMarket } from '../../markets/morphoMidnight';
18
- import { buildMidnightParsedBook, midnightApyFromPrice, midnightPriceFromApy, midnightTimeToMaturityDays, } from './rate';
18
+ import { buildMidnightParsedBook, midnightApyFromPrice, midnightBoundPrice, midnightTimeToMaturityDays, } from './rate';
19
19
  import { getTenorBorrowQuote, getTenorMarketBook, getTenorPaybackQuote, getTenorPaybackUnitsQuote, } from './tenor';
20
- export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightPriceFromApy, midnightTimeToMaturityDays, } from './rate';
20
+ export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightBoundPrice, midnightPriceFromApy, midnightTimeToMaturityDays, MIDNIGHT_DEFAULT_RATE_SLIPPAGE, } from './rate';
21
21
  export { tenorBookKeyFor, tenorBookRateToApyPercent, tenorOfferFillToApiFill, tenorOfferToApiOffer, } from './tenor';
22
22
  /**
23
23
  * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
@@ -175,39 +175,17 @@ const fetchMorphoMidnightQuote = (marketId, side, size, slippagePercent) => __aw
175
175
  takeableOffers: d.takeable_offers || [],
176
176
  };
177
177
  });
178
- /**
179
- * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
180
- * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
181
- * (and the returned `newUnits`/`maxUnits`) are raw loan-token base units — callers convert to/from human
182
- * amounts. Throws if the book can't fill the amount (caller handles).
183
- *
184
- * Two ways to set the cap:
185
- * - `maxBorrowRate` — an absolute APY ceiling, honoured **exactly**: the cap price is derived locally via
186
- * `midnightPriceFromApy`. Prefer this when a user pins a max rate.
187
- * - otherwise `slippagePercent`, the API's own knob. Note it is a **price**-level slippage, not APY points:
188
- * near maturity the annualisation factor (365 / ttmDays) multiplies it heavily, so on a 22-day market a
189
- * slippage of 0.5 permitted an APY ~9pp above the estimate, not 0.5pp. It also saturates at the book's
190
- * cheapest bid. `maxRate` therefore reports what the cap actually permits, derived from the cap price.
191
- *
192
- * A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
193
- * Compare the two before submitting and tell the user their ceiling is under the market rate.
194
- *
195
- * Tenor-hosted markets (see `isTenorMidnightMarket`) are quoted against Tenor's router instead of Morpho's
196
- * book API. Slippage is applied locally to `maxUnits`; `taker` is the position owner (the smart wallet).
197
- */
198
- export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
178
+ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
199
179
  if (isTenorMidnightMarket(marketId)) {
200
- return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker);
180
+ return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent);
201
181
  }
202
182
  const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
203
- const { bestPrice, worstPrice } = quote;
183
+ const { bestPrice } = quote;
204
184
  const ttmDays = midnightTimeToMaturityDays(maturity);
205
185
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
206
186
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
207
187
  // `maxUnits` can never disagree about what the user is protected at.
208
- const capPrice = maxBorrowRate !== undefined && new Dec(maxBorrowRate).gt(0)
209
- ? midnightPriceFromApy(maxBorrowRate, ttmDays)
210
- : worstPrice;
188
+ const capPrice = midnightBoundPrice(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
211
189
  const maxRate = midnightApyFromPrice(capPrice, ttmDays);
212
190
  const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
213
191
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
@@ -216,35 +194,15 @@ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercen
216
194
  newUnits,
217
195
  maxUnits });
218
196
  });
219
- /**
220
- * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
221
- * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
222
- * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
223
- * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
224
- * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
225
- * Throws if the book can't fill the amount (caller handles).
226
- *
227
- * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
228
- * so the guard is a floor rather than a ceiling:
229
- * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
230
- * this when a user pins a min rate.
231
- * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
232
- * annualisation factor near maturity. `minRate` reports what the floor actually permits.
233
- *
234
- * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
235
- * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
236
- */
237
- export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
197
+ export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
238
198
  if (isTenorMidnightMarket(marketId)) {
239
- return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker);
199
+ return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
240
200
  }
241
201
  const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
242
- const { bestPrice, worstPrice } = quote;
202
+ const { bestPrice } = quote;
243
203
  const ttmDays = midnightTimeToMaturityDays(maturity);
244
204
  const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
245
- const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
246
- ? midnightPriceFromApy(minPaybackRate, ttmDays)
247
- : worstPrice;
205
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
248
206
  const minRate = midnightApyFromPrice(capPrice, ttmDays);
249
207
  // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
250
208
  // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
@@ -255,32 +213,15 @@ export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePerce
255
213
  newUnits,
256
214
  minUnits });
257
215
  });
258
- /**
259
- * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
260
- * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
261
- * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
262
- * a ceiling on assets spent rather than a floor on units bought.
263
- *
264
- * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
265
- * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
266
- * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
267
- * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
268
- * directly, rather than through an action contract that sweeps the remainder back — need this quote.
269
- *
270
- * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
271
- * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
272
- */
273
- export const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
216
+ export const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
274
217
  if (isTenorMidnightMarket(marketId)) {
275
- return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker);
218
+ return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
276
219
  }
277
220
  const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
278
- const { bestPrice, worstPrice } = quote;
221
+ const { bestPrice } = quote;
279
222
  const ttmDays = midnightTimeToMaturityDays(maturity);
280
223
  const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
281
- const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
282
- ? midnightPriceFromApy(minPaybackRate, ttmDays)
283
- : worstPrice;
224
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
284
225
  const minRate = midnightApyFromPrice(capPrice, ttmDays);
285
226
  // Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
286
227
  // what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
@@ -7,5 +7,9 @@ export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value
7
7
  * price = (1 + rate)^(−ttmDays / 365).
8
8
  */
9
9
  export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec.Value) => string;
10
+ export declare const MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
11
+ /** Which side of the estimate a guard sits on: a ceiling for borrows, a floor for paybacks. */
12
+ export type MidnightRateBoundKind = 'ceiling' | 'floor';
13
+ export declare const midnightBoundPrice: (estRate: Dec.Value, ttmDays: Dec.Value, kind: MidnightRateBoundKind, boundRate?: Dec.Value, rateSlippagePercent?: Dec.Value) => string;
10
14
  export declare const midnightBookBestFirst: (side: MorphoMidnightBookSide) => 1 | -1;
11
15
  export declare const buildMidnightParsedBook: (offers: MorphoMidnightBookOffer[], side: MorphoMidnightBookSide) => MorphoMidnightParsedBook | null;
@@ -25,6 +25,18 @@ export const midnightPriceFromApy = (ratePercent, ttmDays) => {
25
25
  return '1';
26
26
  return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
27
27
  };
28
+ export const MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
29
+ const MIDNIGHT_MIN_FLOOR_RATIO = 0.5;
30
+ export const midnightBoundPrice = (estRate, ttmDays, kind, boundRate, rateSlippagePercent = MIDNIGHT_DEFAULT_RATE_SLIPPAGE) => {
31
+ if (boundRate !== undefined && new Dec(boundRate).gt(0))
32
+ return midnightPriceFromApy(boundRate, ttmDays);
33
+ const est = new Dec(estRate);
34
+ const slippage = new Dec(rateSlippagePercent);
35
+ const bound = kind === 'ceiling'
36
+ ? est.add(slippage)
37
+ : Dec.max(est.sub(slippage), est.mul(MIDNIGHT_MIN_FLOOR_RATIO));
38
+ return midnightPriceFromApy(bound, ttmDays);
39
+ };
28
40
  export const midnightBookBestFirst = (side) => (side === 'asks' ? -1 : 1);
29
41
  export const buildMidnightParsedBook = (offers, side) => {
30
42
  const bestFirst = [...offers].sort((a, b) => new Dec(a.rate).minus(b.rate).mul(midnightBookBestFirst(side)).toNumber());
@@ -4,7 +4,6 @@ import type { MorphoMidnightBorrowQuote, MorphoMidnightPaybackQuote, MorphoMidni
4
4
  type TenorBookKey = 'asks' | 'bids';
5
5
  export declare const tenorBookKeyFor: (side: MorphoMidnightBookSide) => TenorBookKey;
6
6
  export declare const tenorBookRateToApyPercent: (rate: Dec.Value) => string;
7
- export declare const tenorCapPrice: (bestPrice: string, slippagePercent: Dec.Value, direction: 1 | -1, ttmDays: Dec.Value, boundRate?: Dec.Value) => string;
8
7
  interface TenorOfferCollateral {
9
8
  token: string;
10
9
  lltv: string | number;
@@ -119,7 +118,12 @@ export declare const parseTenorBookSide: (json: TenorBookResponse, side: MorphoM
119
118
  }[];
120
119
  export declare const parseTenorOrderBook: (json: TenorBookResponse, side: MorphoMidnightBookSide, loanSymbol: string) => MorphoMidnightParsedBook | null;
121
120
  export declare const getTenorMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
122
- export declare const getTenorBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string, network?: NetworkNumber) => Promise<MorphoMidnightBorrowQuote>;
123
- export declare const getTenorPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, network?: NetworkNumber) => Promise<MorphoMidnightPaybackQuote>;
124
- export declare const getTenorPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, network?: NetworkNumber) => Promise<MorphoMidnightPaybackUnitsQuote>;
121
+ /**
122
+ * Tenor's router takes no slippage of its own it prices the fill and hands back the offers. `slippagePercent`
123
+ * is therefore accepted only to keep the signature aligned with the Morpho-hosted quote that `index.ts`
124
+ * forwards to positionally; the guard comes from `rateSlippagePercent` via `midnightBoundPrice`.
125
+ */
126
+ export declare const getTenorBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value, network?: NetworkNumber) => Promise<MorphoMidnightBorrowQuote>;
127
+ export declare const getTenorPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value, network?: NetworkNumber) => Promise<MorphoMidnightPaybackQuote>;
128
+ export declare const getTenorPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value, network?: NetworkNumber) => Promise<MorphoMidnightPaybackUnitsQuote>;
125
129
  export {};
@@ -12,7 +12,7 @@ import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
12
12
  import { ZERO_ADDRESS, ZERO_BYTES32 } from '../../constants';
13
13
  import { NetworkNumber, } from '../../types';
14
14
  import { isTenorMidnightMarket, MIDNIGHT_BASE } from '../../markets/morphoMidnight';
15
- import { buildMidnightParsedBook, midnightApyFromPrice, midnightPriceFromApy, midnightTimeToMaturityDays, } from './rate';
15
+ import { buildMidnightParsedBook, midnightApyFromPrice, midnightBoundPrice, midnightTimeToMaturityDays, } from './rate';
16
16
  // Notion doc by Rajko: https://app.notion.com/p/defisaver/Tenor-API-3ba0be682adc80dfad35c81a9a4cb442
17
17
  const TENOR_QUOTES_URL = 'https://router.tenor.finance/v1/quotes';
18
18
  const TENOR_ORDERBOOK_URL = 'https://router.tenor.finance/v1/orderbook';
@@ -23,12 +23,6 @@ export const tenorBookKeyFor = (side) => (side === 'bids' ? 'asks' : 'bids');
23
23
  // Tenor order-book levels are annualized APR in basis points: 25 → 0.25%, 700 → 7%.
24
24
  export const tenorBookRateToApyPercent = (rate) => new Dec(rate || 0).div(100).toString();
25
25
  const tenorFillPrice = (assets, units) => (new Dec(units).lte(0) ? '0' : new Dec(assets).div(units).toString());
26
- export const tenorCapPrice = (bestPrice, slippagePercent, direction, ttmDays, boundRate) => {
27
- if (boundRate !== undefined && new Dec(boundRate).gt(0))
28
- return midnightPriceFromApy(boundRate, ttmDays);
29
- const band = new Dec(1).add(new Dec(slippagePercent || 0).div(100).mul(direction));
30
- return Dec.max(0, new Dec(bestPrice).mul(band)).toString();
31
- };
32
26
  /**
33
27
  * Tenor's offer JSON is flat (market fields live on the offer). Morpho's is nested, and that nested
34
28
  * shape is what the app encodes for `Midnight.take`. Map Tenor into that shape so recipes stay on one encoder.
@@ -152,7 +146,12 @@ const fetchTenorQuote = (_a) => __awaiter(void 0, [_a], void 0, function* ({ mar
152
146
  };
153
147
  });
154
148
  const TENOR_NO_AVAILABLE_UNITS = '0';
155
- export const getTenorBorrowQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, maxBorrowRate_1, taker_1, ...args_1) => __awaiter(void 0, [marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, maxBorrowRate_1, taker_1, ...args_1], void 0, function* (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, network = NetworkNumber.Base) {
149
+ /**
150
+ * Tenor's router takes no slippage of its own — it prices the fill and hands back the offers. `slippagePercent`
151
+ * is therefore accepted only to keep the signature aligned with the Morpho-hosted quote that `index.ts`
152
+ * forwards to positionally; the guard comes from `rateSlippagePercent` via `midnightBoundPrice`.
153
+ */
154
+ export const getTenorBorrowQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, maxBorrowRate_1, taker_1, rateSlippagePercent_1, ...args_1) => __awaiter(void 0, [marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, maxBorrowRate_1, taker_1, rateSlippagePercent_1, ...args_1], void 0, function* (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent, network = NetworkNumber.Base) {
156
155
  if (!isTenorMidnightMarket(marketId)) {
157
156
  throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
158
157
  }
@@ -165,7 +164,7 @@ export const getTenorBorrowQuote = (marketId_1, assetsRaw_1, slippagePercent_1,
165
164
  const ttmDays = midnightTimeToMaturityDays(maturity);
166
165
  const bestPrice = tenorFillPrice(assetsRaw, quote.units);
167
166
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
168
- const capPrice = tenorCapPrice(bestPrice, slippagePercent, -1, ttmDays, maxBorrowRate);
167
+ const capPrice = midnightBoundPrice(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
169
168
  const maxRate = midnightApyFromPrice(capPrice, ttmDays);
170
169
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
171
170
  return {
@@ -180,7 +179,7 @@ export const getTenorBorrowQuote = (marketId_1, assetsRaw_1, slippagePercent_1,
180
179
  takeableOffers: quote.offerFills,
181
180
  };
182
181
  });
183
- export const getTenorPaybackQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, ...args_1) => __awaiter(void 0, [marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, ...args_1], void 0, function* (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, network = NetworkNumber.Base) {
182
+ export const getTenorPaybackQuote = (marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, rateSlippagePercent_1, ...args_1) => __awaiter(void 0, [marketId_1, assetsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, rateSlippagePercent_1, ...args_1], void 0, function* (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent, network = NetworkNumber.Base) {
184
183
  if (!isTenorMidnightMarket(marketId)) {
185
184
  throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
186
185
  }
@@ -193,7 +192,7 @@ export const getTenorPaybackQuote = (marketId_1, assetsRaw_1, slippagePercent_1,
193
192
  const ttmDays = midnightTimeToMaturityDays(maturity);
194
193
  const bestPrice = tenorFillPrice(assetsRaw, quote.units);
195
194
  const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
196
- const capPrice = tenorCapPrice(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
195
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
197
196
  const minRate = midnightApyFromPrice(capPrice, ttmDays);
198
197
  const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
199
198
  return {
@@ -208,7 +207,7 @@ export const getTenorPaybackQuote = (marketId_1, assetsRaw_1, slippagePercent_1,
208
207
  takeableOffers: quote.offerFills,
209
208
  };
210
209
  });
211
- export const getTenorPaybackUnitsQuote = (marketId_1, unitsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, ...args_1) => __awaiter(void 0, [marketId_1, unitsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, ...args_1], void 0, function* (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, network = NetworkNumber.Base) {
210
+ export const getTenorPaybackUnitsQuote = (marketId_1, unitsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, rateSlippagePercent_1, ...args_1) => __awaiter(void 0, [marketId_1, unitsRaw_1, slippagePercent_1, maturity_1, minPaybackRate_1, taker_1, rateSlippagePercent_1, ...args_1], void 0, function* (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent, network = NetworkNumber.Base) {
212
211
  if (!isTenorMidnightMarket(marketId)) {
213
212
  throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
214
213
  }
@@ -222,7 +221,7 @@ export const getTenorPaybackUnitsQuote = (marketId_1, unitsRaw_1, slippagePercen
222
221
  const newAssets = quote.buyerAssets;
223
222
  const bestPrice = tenorFillPrice(newAssets, unitsRaw);
224
223
  const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
225
- const capPrice = tenorCapPrice(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
224
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
226
225
  const minRate = midnightApyFromPrice(capPrice, ttmDays);
227
226
  const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
228
227
  return {
@@ -158,12 +158,14 @@ export function _getMorphoMidnightAccountData(provider, network, account, select
158
158
  if (new Dec(positionInfo.debt.toString()).gt(0)) {
159
159
  try {
160
160
  const borrowInfo = yield getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
161
- borrowRate = borrowInfo.borrowRate;
162
- debtBase = borrowInfo.debtBase;
163
- debtInterest = borrowInfo.debtInterest;
164
- usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
165
- // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
166
- assetsDataForApy = Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.loanToken]: Object.assign(Object.assign({}, loanTokenData), { borrowRate }) });
161
+ if (new Dec(borrowInfo.debtTotal).gt(0)) {
162
+ borrowRate = borrowInfo.borrowRate;
163
+ debtBase = borrowInfo.debtBase;
164
+ debtInterest = borrowInfo.debtInterest;
165
+ usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
166
+ // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
167
+ assetsDataForApy = Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.loanToken]: Object.assign(Object.assign({}, loanTokenData), { borrowRate }) });
168
+ }
167
169
  }
168
170
  catch (err) {
169
171
  // Orderbook API unavailable — keep the on-chain-only fallback above.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.127-midnight-9-dev",
3
+ "version": "2.1.127-midnight-11-dev",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -20,7 +20,7 @@ import { WAD } from '../../constants';
20
20
  import { LONGER_TIMEOUT } from '../../services/utils';
21
21
  import { isTenorMidnightMarket } from '../../markets/morphoMidnight';
22
22
  import {
23
- buildMidnightParsedBook, midnightApyFromPrice, midnightPriceFromApy, midnightTimeToMaturityDays,
23
+ buildMidnightParsedBook, midnightApyFromPrice, midnightBoundPrice, midnightTimeToMaturityDays,
24
24
  } from './rate';
25
25
  import {
26
26
  getTenorBorrowQuote, getTenorMarketBook, getTenorPaybackQuote, getTenorPaybackUnitsQuote,
@@ -30,8 +30,10 @@ export {
30
30
  buildMidnightParsedBook,
31
31
  midnightApyFromPrice,
32
32
  midnightBookBestFirst,
33
+ midnightBoundPrice,
33
34
  midnightPriceFromApy,
34
35
  midnightTimeToMaturityDays,
36
+ MIDNIGHT_DEFAULT_RATE_SLIPPAGE,
35
37
  } from './rate';
36
38
  export {
37
39
  tenorBookKeyFor,
@@ -332,26 +334,6 @@ const fetchMorphoMidnightQuote = async (
332
334
  };
333
335
  };
334
336
 
335
- /**
336
- * Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
337
- * and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
338
- * (and the returned `newUnits`/`maxUnits`) are raw loan-token base units — callers convert to/from human
339
- * amounts. Throws if the book can't fill the amount (caller handles).
340
- *
341
- * Two ways to set the cap:
342
- * - `maxBorrowRate` — an absolute APY ceiling, honoured **exactly**: the cap price is derived locally via
343
- * `midnightPriceFromApy`. Prefer this when a user pins a max rate.
344
- * - otherwise `slippagePercent`, the API's own knob. Note it is a **price**-level slippage, not APY points:
345
- * near maturity the annualisation factor (365 / ttmDays) multiplies it heavily, so on a 22-day market a
346
- * slippage of 0.5 permitted an APY ~9pp above the estimate, not 0.5pp. It also saturates at the book's
347
- * cheapest bid. `maxRate` therefore reports what the cap actually permits, derived from the cap price.
348
- *
349
- * A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
350
- * Compare the two before submitting and tell the user their ceiling is under the market rate.
351
- *
352
- * Tenor-hosted markets (see `isTenorMidnightMarket`) are quoted against Tenor's router instead of Morpho's
353
- * book API. Slippage is applied locally to `maxUnits`; `taker` is the position owner (the smart wallet).
354
- */
355
337
  export const getMorphoMidnightBorrowQuote = async (
356
338
  marketId: string,
357
339
  assetsRaw: string,
@@ -359,21 +341,20 @@ export const getMorphoMidnightBorrowQuote = async (
359
341
  maturity: number,
360
342
  maxBorrowRate?: Dec.Value,
361
343
  taker?: string,
344
+ rateSlippagePercent?: Dec.Value,
362
345
  ): Promise<MorphoMidnightBorrowQuote> => {
363
346
  if (isTenorMidnightMarket(marketId)) {
364
- return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker);
347
+ return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent);
365
348
  }
366
349
 
367
350
  const quote = await fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
368
- const { bestPrice, worstPrice } = quote;
351
+ const { bestPrice } = quote;
369
352
  const ttmDays = midnightTimeToMaturityDays(maturity);
370
353
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
371
354
 
372
355
  // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
373
356
  // `maxUnits` can never disagree about what the user is protected at.
374
- const capPrice = maxBorrowRate !== undefined && new Dec(maxBorrowRate).gt(0)
375
- ? midnightPriceFromApy(maxBorrowRate, ttmDays)
376
- : worstPrice;
357
+ const capPrice = midnightBoundPrice(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
377
358
  const maxRate = midnightApyFromPrice(capPrice, ttmDays);
378
359
  const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
379
360
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
@@ -387,24 +368,6 @@ export const getMorphoMidnightBorrowQuote = async (
387
368
  };
388
369
  };
389
370
 
390
- /**
391
- * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
392
- * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
393
- * the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
394
- * matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
395
- * therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
396
- * Throws if the book can't fill the amount (caller handles).
397
- *
398
- * The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
399
- * so the guard is a floor rather than a ceiling:
400
- * - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
401
- * this when a user pins a min rate.
402
- * - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
403
- * annualisation factor near maturity. `minRate` reports what the floor actually permits.
404
- *
405
- * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
406
- * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
407
- */
408
371
  export const getMorphoMidnightPaybackQuote = async (
409
372
  marketId: string,
410
373
  assetsRaw: string,
@@ -412,19 +375,18 @@ export const getMorphoMidnightPaybackQuote = async (
412
375
  maturity: number,
413
376
  minPaybackRate?: Dec.Value,
414
377
  taker?: string,
378
+ rateSlippagePercent?: Dec.Value,
415
379
  ): Promise<MorphoMidnightPaybackQuote> => {
416
380
  if (isTenorMidnightMarket(marketId)) {
417
- return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker);
381
+ return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
418
382
  }
419
383
 
420
384
  const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
421
- const { bestPrice, worstPrice } = quote;
385
+ const { bestPrice } = quote;
422
386
  const ttmDays = midnightTimeToMaturityDays(maturity);
423
387
  const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
424
388
 
425
- const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
426
- ? midnightPriceFromApy(minPaybackRate, ttmDays)
427
- : worstPrice;
389
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
428
390
  const minRate = midnightApyFromPrice(capPrice, ttmDays);
429
391
  // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
430
392
  // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
@@ -440,21 +402,7 @@ export const getMorphoMidnightPaybackQuote = async (
440
402
  };
441
403
  };
442
404
 
443
- /**
444
- * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
445
- * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
446
- * side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
447
- * a ceiling on assets spent rather than a floor on units bought.
448
- *
449
- * This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
450
- * maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
451
- * close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
452
- * than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
453
- * directly, rather than through an action contract that sweeps the remainder back — need this quote.
454
- *
455
- * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
456
- * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
457
- */
405
+
458
406
  export const getMorphoMidnightPaybackUnitsQuote = async (
459
407
  marketId: string,
460
408
  unitsRaw: string,
@@ -462,19 +410,18 @@ export const getMorphoMidnightPaybackUnitsQuote = async (
462
410
  maturity: number,
463
411
  minPaybackRate?: Dec.Value,
464
412
  taker?: string,
413
+ rateSlippagePercent?: Dec.Value,
465
414
  ): Promise<MorphoMidnightPaybackUnitsQuote> => {
466
415
  if (isTenorMidnightMarket(marketId)) {
467
- return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker);
416
+ return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
468
417
  }
469
418
 
470
419
  const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
471
- const { bestPrice, worstPrice } = quote;
420
+ const { bestPrice } = quote;
472
421
  const ttmDays = midnightTimeToMaturityDays(maturity);
473
422
  const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
474
423
 
475
- const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
476
- ? midnightPriceFromApy(minPaybackRate, ttmDays)
477
- : worstPrice;
424
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
478
425
  const minRate = midnightApyFromPrice(capPrice, ttmDays);
479
426
  // Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
480
427
  // what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
@@ -29,6 +29,31 @@ export const midnightPriceFromApy = (ratePercent: Dec.Value, ttmDays: Dec.Value)
29
29
  return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
30
30
  };
31
31
 
32
+ export const MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
33
+
34
+ const MIDNIGHT_MIN_FLOOR_RATIO = 0.5;
35
+
36
+ /** Which side of the estimate a guard sits on: a ceiling for borrows, a floor for paybacks. */
37
+ export type MidnightRateBoundKind = 'ceiling' | 'floor';
38
+
39
+ export const midnightBoundPrice = (
40
+ estRate: Dec.Value,
41
+ ttmDays: Dec.Value,
42
+ kind: MidnightRateBoundKind,
43
+ boundRate?: Dec.Value,
44
+ rateSlippagePercent: Dec.Value = MIDNIGHT_DEFAULT_RATE_SLIPPAGE,
45
+ ): string => {
46
+ if (boundRate !== undefined && new Dec(boundRate).gt(0)) return midnightPriceFromApy(boundRate, ttmDays);
47
+
48
+ const est = new Dec(estRate);
49
+ const slippage = new Dec(rateSlippagePercent);
50
+ const bound = kind === 'ceiling'
51
+ ? est.add(slippage)
52
+ : Dec.max(est.sub(slippage), est.mul(MIDNIGHT_MIN_FLOOR_RATIO));
53
+
54
+ return midnightPriceFromApy(bound, ttmDays);
55
+ };
56
+
32
57
  export const midnightBookBestFirst = (side: MorphoMidnightBookSide): 1 | -1 => (side === 'asks' ? -1 : 1);
33
58
 
34
59
  export const buildMidnightParsedBook = (
@@ -17,7 +17,7 @@ import type {
17
17
  import {
18
18
  buildMidnightParsedBook,
19
19
  midnightApyFromPrice,
20
- midnightPriceFromApy,
20
+ midnightBoundPrice,
21
21
  midnightTimeToMaturityDays,
22
22
  } from './rate';
23
23
 
@@ -43,18 +43,6 @@ const tenorFillPrice = (assets: Dec.Value, units: Dec.Value): string => (
43
43
  new Dec(units).lte(0) ? '0' : new Dec(assets).div(units).toString()
44
44
  );
45
45
 
46
- export const tenorCapPrice = (
47
- bestPrice: string,
48
- slippagePercent: Dec.Value,
49
- direction: 1 | -1,
50
- ttmDays: Dec.Value,
51
- boundRate?: Dec.Value,
52
- ): string => {
53
- if (boundRate !== undefined && new Dec(boundRate).gt(0)) return midnightPriceFromApy(boundRate, ttmDays);
54
- const band = new Dec(1).add(new Dec(slippagePercent || 0).div(100).mul(direction));
55
- return Dec.max(0, new Dec(bestPrice).mul(band)).toString();
56
- };
57
-
58
46
  interface TenorOfferCollateral {
59
47
  token: string;
60
48
  lltv: string | number;
@@ -286,6 +274,11 @@ const fetchTenorQuote = async ({
286
274
 
287
275
  const TENOR_NO_AVAILABLE_UNITS = '0';
288
276
 
277
+ /**
278
+ * Tenor's router takes no slippage of its own — it prices the fill and hands back the offers. `slippagePercent`
279
+ * is therefore accepted only to keep the signature aligned with the Morpho-hosted quote that `index.ts`
280
+ * forwards to positionally; the guard comes from `rateSlippagePercent` via `midnightBoundPrice`.
281
+ */
289
282
  export const getTenorBorrowQuote = async (
290
283
  marketId: string,
291
284
  assetsRaw: string,
@@ -293,6 +286,7 @@ export const getTenorBorrowQuote = async (
293
286
  maturity: number,
294
287
  maxBorrowRate?: Dec.Value,
295
288
  taker?: string,
289
+ rateSlippagePercent?: Dec.Value,
296
290
  network: NetworkNumber = NetworkNumber.Base,
297
291
  ): Promise<MorphoMidnightBorrowQuote> => {
298
292
  if (!isTenorMidnightMarket(marketId)) {
@@ -310,7 +304,7 @@ export const getTenorBorrowQuote = async (
310
304
  const bestPrice = tenorFillPrice(assetsRaw, quote.units);
311
305
  const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
312
306
 
313
- const capPrice = tenorCapPrice(bestPrice, slippagePercent, -1, ttmDays, maxBorrowRate);
307
+ const capPrice = midnightBoundPrice(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
314
308
  const maxRate = midnightApyFromPrice(capPrice, ttmDays);
315
309
  const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
316
310
 
@@ -334,6 +328,7 @@ export const getTenorPaybackQuote = async (
334
328
  maturity: number,
335
329
  minPaybackRate?: Dec.Value,
336
330
  taker?: string,
331
+ rateSlippagePercent?: Dec.Value,
337
332
  network: NetworkNumber = NetworkNumber.Base,
338
333
  ): Promise<MorphoMidnightPaybackQuote> => {
339
334
  if (!isTenorMidnightMarket(marketId)) {
@@ -351,7 +346,7 @@ export const getTenorPaybackQuote = async (
351
346
  const bestPrice = tenorFillPrice(assetsRaw, quote.units);
352
347
  const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
353
348
 
354
- const capPrice = tenorCapPrice(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
349
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
355
350
  const minRate = midnightApyFromPrice(capPrice, ttmDays);
356
351
  const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
357
352
 
@@ -375,6 +370,7 @@ export const getTenorPaybackUnitsQuote = async (
375
370
  maturity: number,
376
371
  minPaybackRate?: Dec.Value,
377
372
  taker?: string,
373
+ rateSlippagePercent?: Dec.Value,
378
374
  network: NetworkNumber = NetworkNumber.Base,
379
375
  ): Promise<MorphoMidnightPaybackUnitsQuote> => {
380
376
  if (!isTenorMidnightMarket(marketId)) {
@@ -393,7 +389,7 @@ export const getTenorPaybackUnitsQuote = async (
393
389
  const bestPrice = tenorFillPrice(newAssets, unitsRaw);
394
390
  const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
395
391
 
396
- const capPrice = tenorCapPrice(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
392
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
397
393
  const minRate = midnightApyFromPrice(capPrice, ttmDays);
398
394
  const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
399
395
 
@@ -167,15 +167,17 @@ export async function _getMorphoMidnightAccountData(provider: Client, network: N
167
167
  if (new Dec(positionInfo.debt.toString()).gt(0)) {
168
168
  try {
169
169
  const borrowInfo = await getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
170
- borrowRate = borrowInfo.borrowRate;
171
- debtBase = borrowInfo.debtBase;
172
- debtInterest = borrowInfo.debtInterest;
173
- usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
174
- // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
175
- assetsDataForApy = {
176
- ...marketInfo.assetsData,
177
- [marketInfo.loanToken]: { ...loanTokenData, borrowRate },
178
- };
170
+ if (new Dec(borrowInfo.debtTotal).gt(0)) {
171
+ borrowRate = borrowInfo.borrowRate;
172
+ debtBase = borrowInfo.debtBase;
173
+ debtInterest = borrowInfo.debtInterest;
174
+ usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
175
+ // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
176
+ assetsDataForApy = {
177
+ ...marketInfo.assetsData,
178
+ [marketInfo.loanToken]: { ...loanTokenData, borrowRate },
179
+ };
180
+ }
179
181
  } catch (err) {
180
182
  // Orderbook API unavailable — keep the on-chain-only fallback above.
181
183
  }