@defisaver/positions-sdk 2.1.127-midnight-4-dev → 2.1.127-midnight-6-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.
- package/cjs/constants/index.d.ts +1 -0
- package/cjs/constants/index.js +2 -1
- package/cjs/helpers/morphoMidnightHelpers/index.d.ts +34 -13
- package/cjs/helpers/morphoMidnightHelpers/index.js +85 -61
- package/cjs/helpers/morphoMidnightHelpers/rate.d.ts +11 -0
- package/cjs/helpers/morphoMidnightHelpers/rate.js +49 -0
- package/cjs/helpers/morphoMidnightHelpers/tenor.d.ts +125 -0
- package/cjs/helpers/morphoMidnightHelpers/tenor.js +256 -0
- package/cjs/markets/index.d.ts +1 -1
- package/cjs/markets/index.js +2 -1
- package/cjs/markets/morphoMidnight/index.d.ts +37 -0
- package/cjs/markets/morphoMidnight/index.js +434 -42
- package/cjs/types/morphoMidnight.d.ts +18 -1
- package/cjs/types/morphoMidnight.js +16 -0
- package/esm/constants/index.d.ts +1 -0
- package/esm/constants/index.js +1 -0
- package/esm/helpers/morphoMidnightHelpers/index.d.ts +34 -13
- package/esm/helpers/morphoMidnightHelpers/index.js +65 -48
- package/esm/helpers/morphoMidnightHelpers/rate.d.ts +11 -0
- package/esm/helpers/morphoMidnightHelpers/rate.js +38 -0
- package/esm/helpers/morphoMidnightHelpers/tenor.d.ts +125 -0
- package/esm/helpers/morphoMidnightHelpers/tenor.js +239 -0
- package/esm/markets/index.d.ts +1 -1
- package/esm/markets/index.js +1 -1
- package/esm/markets/morphoMidnight/index.d.ts +37 -0
- package/esm/markets/morphoMidnight/index.js +399 -23
- package/esm/types/morphoMidnight.d.ts +18 -1
- package/esm/types/morphoMidnight.js +16 -0
- package/package.json +1 -1
- package/src/constants/index.ts +1 -0
- package/src/helpers/morphoMidnightHelpers/index.ts +120 -48
- package/src/helpers/morphoMidnightHelpers/rate.ts +46 -0
- package/src/helpers/morphoMidnightHelpers/tenor.ts +411 -0
- package/src/markets/index.ts +1 -1
- package/src/markets/morphoMidnight/index.ts +424 -23
- package/src/types/morphoMidnight.ts +19 -0
|
@@ -16,8 +16,29 @@ import {
|
|
|
16
16
|
MorphoMidnightMarketInfo,
|
|
17
17
|
MorphoMidnightParsedBook,
|
|
18
18
|
} from '../../types';
|
|
19
|
-
import {
|
|
19
|
+
import { WAD } from '../../constants';
|
|
20
20
|
import { LONGER_TIMEOUT } from '../../services/utils';
|
|
21
|
+
import { isTenorMidnightMarket } from '../../markets/morphoMidnight';
|
|
22
|
+
import {
|
|
23
|
+
buildMidnightParsedBook, midnightApyFromPrice, midnightPriceFromApy, midnightTimeToMaturityDays,
|
|
24
|
+
} from './rate';
|
|
25
|
+
import {
|
|
26
|
+
getTenorBorrowQuote, getTenorMarketBook, getTenorPaybackQuote, getTenorPaybackUnitsQuote,
|
|
27
|
+
} from './tenor';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
buildMidnightParsedBook,
|
|
31
|
+
midnightApyFromPrice,
|
|
32
|
+
midnightBookBestFirst,
|
|
33
|
+
midnightPriceFromApy,
|
|
34
|
+
midnightTimeToMaturityDays,
|
|
35
|
+
} from './rate';
|
|
36
|
+
export {
|
|
37
|
+
tenorBookKeyFor,
|
|
38
|
+
tenorBookRateToApyPercent,
|
|
39
|
+
tenorOfferFillToApiFill,
|
|
40
|
+
tenorOfferToApiOffer,
|
|
41
|
+
} from './tenor';
|
|
21
42
|
|
|
22
43
|
/**
|
|
23
44
|
* Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
|
|
@@ -104,7 +125,6 @@ export const getMorphoMidnightAggregatedPositionData = ({
|
|
|
104
125
|
// loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
|
|
105
126
|
|
|
106
127
|
const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
|
|
107
|
-
const nowInSeconds = () => Math.floor(Date.now() / 1000);
|
|
108
128
|
|
|
109
129
|
// The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
|
|
110
130
|
// enough that markets drop out of the list for no reason.
|
|
@@ -173,34 +193,19 @@ export interface MorphoMidnightPaybackQuote {
|
|
|
173
193
|
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
174
194
|
}
|
|
175
195
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
|
|
192
|
-
* price = (1 + rate)^(−ttmDays / 365).
|
|
193
|
-
*
|
|
194
|
-
* This is what turns an absolute rate ceiling into an on-chain `maxUnits` cap (units = assets / price),
|
|
195
|
-
* and equally the principal a unit of borrow power is worth — Midnight debt is recorded at its maturity
|
|
196
|
-
* face value, so borrowing the full limit as principal would overshoot it by the interest.
|
|
197
|
-
*/
|
|
198
|
-
export const midnightPriceFromApy = (ratePercent: Dec.Value, ttmDays: Dec.Value): string => {
|
|
199
|
-
const rate = new Dec(ratePercent);
|
|
200
|
-
const ttm = new Dec(ttmDays);
|
|
201
|
-
if (rate.lte(0) || ttm.lte(0)) return '1';
|
|
202
|
-
return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
|
|
203
|
-
};
|
|
196
|
+
// Payback quoted the other way round: the caller names the debt units to retire, and the quote prices
|
|
197
|
+
// what buying them costs. Same rate guard as the assets-target quote, expressed as a spend ceiling.
|
|
198
|
+
export interface MorphoMidnightPaybackUnitsQuote {
|
|
199
|
+
bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
|
|
200
|
+
worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
|
|
201
|
+
estPaybackRate: string, // APY the repayment retires debt at, as a percent
|
|
202
|
+
minRate: string, // APY the on-chain ceiling permits, i.e. `maxAssets` annualized (display only)
|
|
203
|
+
newAssets: string, // assets the target units cost at best price, raw loan-token base units
|
|
204
|
+
maxAssets: string, // ceiling on assets spent (on-chain guard), raw loan-token base units
|
|
205
|
+
availableAssets: string,
|
|
206
|
+
availableUnits: string,
|
|
207
|
+
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
208
|
+
}
|
|
204
209
|
|
|
205
210
|
/**
|
|
206
211
|
* Coerce a slippage into what the quote endpoint accepts: 0.1–100 with at most one decimal place. The
|
|
@@ -261,28 +266,23 @@ export const getMorphoMidnightMarketBook = async (
|
|
|
261
266
|
network: NetworkNumber,
|
|
262
267
|
side: MorphoMidnightBookSide = 'bids',
|
|
263
268
|
): Promise<MorphoMidnightParsedBook | null> => {
|
|
269
|
+
if (isTenorMidnightMarket(market)) {
|
|
270
|
+
return getTenorMarketBook(market, network, side);
|
|
271
|
+
}
|
|
272
|
+
|
|
264
273
|
const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
|
|
265
274
|
const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
|
|
266
275
|
if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
|
|
267
276
|
|
|
268
277
|
const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
|
|
269
278
|
const ttmDays = midnightTimeToMaturityDays(market.maturity);
|
|
270
|
-
const bestFirst = side === 'asks' ? -1 : 1;
|
|
271
|
-
|
|
272
|
-
const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || [])
|
|
273
|
-
.map((offer) => ({
|
|
274
|
-
rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
|
|
275
|
-
liquidity: assetAmountInEth(offer.assets, loanSymbol),
|
|
276
|
-
}))
|
|
277
|
-
.sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
|
|
278
279
|
|
|
279
|
-
|
|
280
|
+
const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || []).map((offer) => ({
|
|
281
|
+
rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
|
|
282
|
+
liquidity: assetAmountInEth(offer.assets, loanSymbol),
|
|
283
|
+
}));
|
|
280
284
|
|
|
281
|
-
return
|
|
282
|
-
bestRate: offers[0].rate,
|
|
283
|
-
totalLiquidity: offers.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
|
|
284
|
-
offers,
|
|
285
|
-
};
|
|
285
|
+
return buildMidnightParsedBook(offers, side);
|
|
286
286
|
};
|
|
287
287
|
|
|
288
288
|
// The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
|
|
@@ -302,14 +302,22 @@ interface MidnightParsedQuote {
|
|
|
302
302
|
takeableOffers: any[],
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/**
|
|
306
|
+
* How much of the book to quote. The endpoint takes exactly one of the two — it rejects a request with
|
|
307
|
+
* neither ("Either assets or units must be provided") — and answers the same `takeable_offers` list either
|
|
308
|
+
* way, since that list is the whole in-band depth rather than the slice this size consumes.
|
|
309
|
+
*/
|
|
310
|
+
type MidnightQuoteSize = { assets: string } | { units: string };
|
|
311
|
+
|
|
305
312
|
// The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
|
|
306
313
|
const fetchMorphoMidnightQuote = async (
|
|
307
314
|
marketId: string,
|
|
308
315
|
side: MorphoMidnightBookSide,
|
|
309
|
-
|
|
316
|
+
size: MidnightQuoteSize,
|
|
310
317
|
slippagePercent: Dec.Value,
|
|
311
318
|
): Promise<MidnightParsedQuote> => {
|
|
312
|
-
const
|
|
319
|
+
const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
|
|
320
|
+
const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${midnightSlippageParam(slippagePercent)}`;
|
|
313
321
|
const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
314
322
|
const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
|
|
315
323
|
const d = json?.data;
|
|
@@ -340,6 +348,9 @@ const fetchMorphoMidnightQuote = async (
|
|
|
340
348
|
*
|
|
341
349
|
* A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
|
|
342
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).
|
|
343
354
|
*/
|
|
344
355
|
export const getMorphoMidnightBorrowQuote = async (
|
|
345
356
|
marketId: string,
|
|
@@ -347,8 +358,13 @@ export const getMorphoMidnightBorrowQuote = async (
|
|
|
347
358
|
slippagePercent: Dec.Value,
|
|
348
359
|
maturity: number,
|
|
349
360
|
maxBorrowRate?: Dec.Value,
|
|
361
|
+
taker?: string,
|
|
350
362
|
): Promise<MorphoMidnightBorrowQuote> => {
|
|
351
|
-
|
|
363
|
+
if (isTenorMidnightMarket(marketId)) {
|
|
364
|
+
return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
|
|
352
368
|
const { bestPrice, worstPrice } = quote;
|
|
353
369
|
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
354
370
|
const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
@@ -395,8 +411,13 @@ export const getMorphoMidnightPaybackQuote = async (
|
|
|
395
411
|
slippagePercent: Dec.Value,
|
|
396
412
|
maturity: number,
|
|
397
413
|
minPaybackRate?: Dec.Value,
|
|
414
|
+
taker?: string,
|
|
398
415
|
): Promise<MorphoMidnightPaybackQuote> => {
|
|
399
|
-
|
|
416
|
+
if (isTenorMidnightMarket(marketId)) {
|
|
417
|
+
return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
|
|
400
421
|
const { bestPrice, worstPrice } = quote;
|
|
401
422
|
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
402
423
|
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
@@ -418,3 +439,54 @@ export const getMorphoMidnightPaybackQuote = async (
|
|
|
418
439
|
minUnits,
|
|
419
440
|
};
|
|
420
441
|
};
|
|
442
|
+
|
|
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
|
+
*/
|
|
458
|
+
export const getMorphoMidnightPaybackUnitsQuote = async (
|
|
459
|
+
marketId: string,
|
|
460
|
+
unitsRaw: string,
|
|
461
|
+
slippagePercent: Dec.Value,
|
|
462
|
+
maturity: number,
|
|
463
|
+
minPaybackRate?: Dec.Value,
|
|
464
|
+
taker?: string,
|
|
465
|
+
): Promise<MorphoMidnightPaybackUnitsQuote> => {
|
|
466
|
+
if (isTenorMidnightMarket(marketId)) {
|
|
467
|
+
return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
|
|
471
|
+
const { bestPrice, worstPrice } = quote;
|
|
472
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
473
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
474
|
+
|
|
475
|
+
const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
|
|
476
|
+
? midnightPriceFromApy(minPaybackRate, ttmDays)
|
|
477
|
+
: worstPrice;
|
|
478
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
479
|
+
// Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
|
|
480
|
+
// what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
|
|
481
|
+
// buy short of the units it was asked for.
|
|
482
|
+
const newAssets = new Dec(unitsRaw).mul(bestPrice).toFixed(0, Dec.ROUND_UP);
|
|
483
|
+
const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
...quote,
|
|
487
|
+
estPaybackRate,
|
|
488
|
+
minRate,
|
|
489
|
+
newAssets,
|
|
490
|
+
maxAssets,
|
|
491
|
+
};
|
|
492
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Dec from 'decimal.js';
|
|
2
|
+
import { SECONDS_PER_DAY } from '../../constants';
|
|
3
|
+
import { MorphoMidnightBookOffer, MorphoMidnightBookSide, MorphoMidnightParsedBook } from '../../types';
|
|
4
|
+
|
|
5
|
+
const nowInSeconds = () => Math.floor(Date.now() / 1000);
|
|
6
|
+
|
|
7
|
+
// Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
|
|
8
|
+
export const midnightTimeToMaturityDays = (maturity: number, atSeconds: number = nowInSeconds()): number => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
|
|
9
|
+
|
|
10
|
+
// Annualize a fixed-term discount price into an APY percent: (1 / price)^(365 / ttmDays) − 1.
|
|
11
|
+
// `price` is loan-per-unit (assets received / units owed), so 1/price ≥ 1.
|
|
12
|
+
export const midnightApyFromPrice = (price: Dec.Value, ttmDays: Dec.Value): string => {
|
|
13
|
+
const p = new Dec(price);
|
|
14
|
+
const ttm = new Dec(ttmDays);
|
|
15
|
+
if (p.lte(0) || ttm.lte(0)) return '0';
|
|
16
|
+
return new Dec(1).div(p).pow(new Dec(365).div(ttm)).sub(1)
|
|
17
|
+
.mul(100)
|
|
18
|
+
.toString();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
|
|
23
|
+
* price = (1 + rate)^(−ttmDays / 365).
|
|
24
|
+
*/
|
|
25
|
+
export const midnightPriceFromApy = (ratePercent: Dec.Value, ttmDays: Dec.Value): string => {
|
|
26
|
+
const rate = new Dec(ratePercent);
|
|
27
|
+
const ttm = new Dec(ttmDays);
|
|
28
|
+
if (rate.lte(0) || ttm.lte(0)) return '1';
|
|
29
|
+
return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const midnightBookBestFirst = (side: MorphoMidnightBookSide): 1 | -1 => (side === 'asks' ? -1 : 1);
|
|
33
|
+
|
|
34
|
+
export const buildMidnightParsedBook = (
|
|
35
|
+
offers: MorphoMidnightBookOffer[],
|
|
36
|
+
side: MorphoMidnightBookSide,
|
|
37
|
+
): MorphoMidnightParsedBook | null => {
|
|
38
|
+
const bestFirst = [...offers].sort((a, b) => new Dec(a.rate).minus(b.rate).mul(midnightBookBestFirst(side)).toNumber());
|
|
39
|
+
if (bestFirst.length === 0) return null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
bestRate: bestFirst[0].rate,
|
|
43
|
+
totalLiquidity: bestFirst.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
|
|
44
|
+
offers: bestFirst,
|
|
45
|
+
};
|
|
46
|
+
};
|