@defisaver/positions-sdk 2.1.127-midnight-5-dev → 2.1.127-midnight-7-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.
Files changed (36) hide show
  1. package/cjs/constants/index.d.ts +1 -0
  2. package/cjs/constants/index.js +2 -1
  3. package/cjs/helpers/morphoMidnightHelpers/index.d.ts +8 -14
  4. package/cjs/helpers/morphoMidnightHelpers/index.js +50 -62
  5. package/cjs/helpers/morphoMidnightHelpers/rate.d.ts +11 -0
  6. package/cjs/helpers/morphoMidnightHelpers/rate.js +49 -0
  7. package/cjs/helpers/morphoMidnightHelpers/tenor.d.ts +125 -0
  8. package/cjs/helpers/morphoMidnightHelpers/tenor.js +256 -0
  9. package/cjs/markets/index.d.ts +1 -1
  10. package/cjs/markets/index.js +3 -1
  11. package/cjs/markets/morphoMidnight/index.d.ts +45 -1
  12. package/cjs/markets/morphoMidnight/index.js +542 -42
  13. package/cjs/types/morphoMidnight.d.ts +22 -1
  14. package/cjs/types/morphoMidnight.js +16 -0
  15. package/esm/constants/index.d.ts +1 -0
  16. package/esm/constants/index.js +1 -0
  17. package/esm/helpers/morphoMidnightHelpers/index.d.ts +8 -14
  18. package/esm/helpers/morphoMidnightHelpers/index.js +27 -45
  19. package/esm/helpers/morphoMidnightHelpers/rate.d.ts +11 -0
  20. package/esm/helpers/morphoMidnightHelpers/rate.js +38 -0
  21. package/esm/helpers/morphoMidnightHelpers/tenor.d.ts +125 -0
  22. package/esm/helpers/morphoMidnightHelpers/tenor.js +239 -0
  23. package/esm/markets/index.d.ts +1 -1
  24. package/esm/markets/index.js +1 -1
  25. package/esm/markets/morphoMidnight/index.d.ts +45 -1
  26. package/esm/markets/morphoMidnight/index.js +507 -24
  27. package/esm/types/morphoMidnight.d.ts +22 -1
  28. package/esm/types/morphoMidnight.js +16 -0
  29. package/package.json +1 -1
  30. package/src/constants/index.ts +1 -0
  31. package/src/helpers/morphoMidnightHelpers/index.ts +49 -45
  32. package/src/helpers/morphoMidnightHelpers/rate.ts +46 -0
  33. package/src/helpers/morphoMidnightHelpers/tenor.ts +411 -0
  34. package/src/markets/index.ts +3 -1
  35. package/src/markets/morphoMidnight/index.ts +538 -24
  36. package/src/types/morphoMidnight.ts +23 -0
@@ -9,4 +9,20 @@ export var MorphoMidnightVersions;
9
9
  MorphoMidnightVersions["MorphoMidnightCbBTCUSDC_860_20261030_Base"] = "morphomidnightcbbtcusdc_860_20261030_base";
10
10
  MorphoMidnightVersions["MorphoMidnightCbBTCUSDC_860_20261127_Base"] = "morphomidnightcbbtcusdc_860_20261127_base";
11
11
  MorphoMidnightVersions["MorphoMidnightCbBTCUSDC_860_20261225_Base"] = "morphomidnightcbbtcusdc_860_20261225_base";
12
+ // Tenor-hosted Midnight markets (same core, different order book)
13
+ MorphoMidnightVersions["MorphoMidnightTenorCbBTCUSDC_20260827_Base"] = "morphomidnighttenorcbbtcusdc_20260827_base";
14
+ MorphoMidnightVersions["MorphoMidnightTenorCbBTCUSDC_20260924_Base"] = "morphomidnighttenorcbbtcusdc_20260924_base";
15
+ MorphoMidnightVersions["MorphoMidnightTenorCbBTCUSDC_20261022_Base"] = "morphomidnighttenorcbbtcusdc_20261022_base";
16
+ MorphoMidnightVersions["MorphoMidnightTenorCbBTCUSDC_20261119_Base"] = "morphomidnighttenorcbbtcusdc_20261119_base";
17
+ MorphoMidnightVersions["MorphoMidnightTenorCbBTCUSDC_20261217_Base"] = "morphomidnighttenorcbbtcusdc_20261217_base";
18
+ MorphoMidnightVersions["MorphoMidnightTenorWETHUSDC_20260827_Base"] = "morphomidnighttenorwethusdc_20260827_base";
19
+ MorphoMidnightVersions["MorphoMidnightTenorWETHUSDC_20260924_Base"] = "morphomidnighttenorwethusdc_20260924_base";
20
+ MorphoMidnightVersions["MorphoMidnightTenorWETHUSDC_20261022_Base"] = "morphomidnighttenorwethusdc_20261022_base";
21
+ MorphoMidnightVersions["MorphoMidnightTenorWETHUSDC_20261119_Base"] = "morphomidnighttenorwethusdc_20261119_base";
22
+ MorphoMidnightVersions["MorphoMidnightTenorWETHUSDC_20261217_Base"] = "morphomidnighttenorwethusdc_20261217_base";
23
+ MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20260827_Base"] = "morphomidnighttenorcbethweth_20260827_base";
24
+ MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20260924_Base"] = "morphomidnighttenorcbethweth_20260924_base";
25
+ MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20261022_Base"] = "morphomidnighttenorcbethweth_20261022_base";
26
+ MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20261119_Base"] = "morphomidnighttenorcbethweth_20261119_base";
27
+ MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20261217_Base"] = "morphomidnighttenorcbethweth_20261217_base";
12
28
  })(MorphoMidnightVersions || (MorphoMidnightVersions = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.127-midnight-5-dev",
3
+ "version": "2.1.127-midnight-7-dev",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -6,6 +6,7 @@ export const SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY;
6
6
  export const AVG_BLOCK_TIME = 12;
7
7
  export const BLOCKS_IN_A_YEAR = SECONDS_PER_YEAR / AVG_BLOCK_TIME;
8
8
  export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
9
+ export const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
9
10
  export const WAD = 1e18;
10
11
  export const USD_QUOTE = '0x0000000000000000000000000000000000000348';
11
12
  export const borrowOperations = ['borrow', 'payback'];
@@ -16,8 +16,29 @@ import {
16
16
  MorphoMidnightMarketInfo,
17
17
  MorphoMidnightParsedBook,
18
18
  } from '../../types';
19
- import { SECONDS_PER_DAY, WAD } from '../../constants';
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.
@@ -187,35 +207,6 @@ export interface MorphoMidnightPaybackUnitsQuote {
187
207
  takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
188
208
  }
189
209
 
190
- // Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
191
- export const midnightTimeToMaturityDays = (maturity: number, atSeconds: number = nowInSeconds()): number => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
192
-
193
- // Annualize a fixed-term discount price into an APY percent: (1 / price)^(365 / ttmDays) − 1.
194
- // `price` is loan-per-unit (assets received / units owed), so 1/price ≥ 1.
195
- export const midnightApyFromPrice = (price: Dec.Value, ttmDays: Dec.Value): string => {
196
- const p = new Dec(price);
197
- const ttm = new Dec(ttmDays);
198
- if (p.lte(0) || ttm.lte(0)) return '0';
199
- return new Dec(1).div(p).pow(new Dec(365).div(ttm)).sub(1)
200
- .mul(100)
201
- .toString();
202
- };
203
-
204
- /**
205
- * Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
206
- * price = (1 + rate)^(−ttmDays / 365).
207
- *
208
- * This is what turns an absolute rate ceiling into an on-chain `maxUnits` cap (units = assets / price),
209
- * and equally the principal a unit of borrow power is worth — Midnight debt is recorded at its maturity
210
- * face value, so borrowing the full limit as principal would overshoot it by the interest.
211
- */
212
- export const midnightPriceFromApy = (ratePercent: Dec.Value, ttmDays: Dec.Value): string => {
213
- const rate = new Dec(ratePercent);
214
- const ttm = new Dec(ttmDays);
215
- if (rate.lte(0) || ttm.lte(0)) return '1';
216
- return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
217
- };
218
-
219
210
  /**
220
211
  * Coerce a slippage into what the quote endpoint accepts: 0.1–100 with at most one decimal place. The
221
212
  * validation is lexical, so a computed value (`4.15066671050631467`) is rejected outright — without this
@@ -275,28 +266,23 @@ export const getMorphoMidnightMarketBook = async (
275
266
  network: NetworkNumber,
276
267
  side: MorphoMidnightBookSide = 'bids',
277
268
  ): Promise<MorphoMidnightParsedBook | null> => {
269
+ if (isTenorMidnightMarket(market)) {
270
+ return getTenorMarketBook(market, network, side);
271
+ }
272
+
278
273
  const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
279
274
  const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
280
275
  if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
281
276
 
282
277
  const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
283
278
  const ttmDays = midnightTimeToMaturityDays(market.maturity);
284
- const bestFirst = side === 'asks' ? -1 : 1;
285
279
 
286
- const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || [])
287
- .map((offer) => ({
288
- rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
289
- liquidity: assetAmountInEth(offer.assets, loanSymbol),
290
- }))
291
- .sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
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
+ }));
292
284
 
293
- if (offers.length === 0) return null;
294
-
295
- return {
296
- bestRate: offers[0].rate,
297
- totalLiquidity: offers.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
298
- offers,
299
- };
285
+ return buildMidnightParsedBook(offers, side);
300
286
  };
301
287
 
302
288
  // The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
@@ -362,6 +348,9 @@ const fetchMorphoMidnightQuote = async (
362
348
  *
363
349
  * A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
364
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).
365
354
  */
366
355
  export const getMorphoMidnightBorrowQuote = async (
367
356
  marketId: string,
@@ -369,7 +358,12 @@ export const getMorphoMidnightBorrowQuote = async (
369
358
  slippagePercent: Dec.Value,
370
359
  maturity: number,
371
360
  maxBorrowRate?: Dec.Value,
361
+ taker?: string,
372
362
  ): Promise<MorphoMidnightBorrowQuote> => {
363
+ if (isTenorMidnightMarket(marketId)) {
364
+ return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker);
365
+ }
366
+
373
367
  const quote = await fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
374
368
  const { bestPrice, worstPrice } = quote;
375
369
  const ttmDays = midnightTimeToMaturityDays(maturity);
@@ -417,7 +411,12 @@ export const getMorphoMidnightPaybackQuote = async (
417
411
  slippagePercent: Dec.Value,
418
412
  maturity: number,
419
413
  minPaybackRate?: Dec.Value,
414
+ taker?: string,
420
415
  ): Promise<MorphoMidnightPaybackQuote> => {
416
+ if (isTenorMidnightMarket(marketId)) {
417
+ return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker);
418
+ }
419
+
421
420
  const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
422
421
  const { bestPrice, worstPrice } = quote;
423
422
  const ttmDays = midnightTimeToMaturityDays(maturity);
@@ -462,7 +461,12 @@ export const getMorphoMidnightPaybackUnitsQuote = async (
462
461
  slippagePercent: Dec.Value,
463
462
  maturity: number,
464
463
  minPaybackRate?: Dec.Value,
464
+ taker?: string,
465
465
  ): Promise<MorphoMidnightPaybackUnitsQuote> => {
466
+ if (isTenorMidnightMarket(marketId)) {
467
+ return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker);
468
+ }
469
+
466
470
  const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
467
471
  const { bestPrice, worstPrice } = quote;
468
472
  const ttmDays = midnightTimeToMaturityDays(maturity);
@@ -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
+ };
@@ -0,0 +1,411 @@
1
+ import Dec from 'decimal.js';
2
+ import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
3
+ import { ZERO_ADDRESS, ZERO_BYTES32 } from '../../constants';
4
+ import {
5
+ MorphoMidnightBookOffer,
6
+ MorphoMidnightBookSide,
7
+ MorphoMidnightMarketData,
8
+ MorphoMidnightParsedBook,
9
+ NetworkNumber,
10
+ } from '../../types';
11
+ import { isTenorMidnightMarket, MIDNIGHT_BASE } from '../../markets/morphoMidnight';
12
+ import type {
13
+ MorphoMidnightBorrowQuote,
14
+ MorphoMidnightPaybackQuote,
15
+ MorphoMidnightPaybackUnitsQuote,
16
+ } from './index';
17
+ import {
18
+ buildMidnightParsedBook,
19
+ midnightApyFromPrice,
20
+ midnightPriceFromApy,
21
+ midnightTimeToMaturityDays,
22
+ } from './rate';
23
+
24
+
25
+ // Notion doc by Rajko: https://app.notion.com/p/defisaver/Tenor-API-3ba0be682adc80dfad35c81a9a4cb442
26
+
27
+ const TENOR_QUOTES_URL = 'https://router.tenor.finance/v1/quotes';
28
+ const TENOR_ORDERBOOK_URL = 'https://router.tenor.finance/v1/orderbook';
29
+ const TENOR_TIMEOUT_MS = 15000;
30
+ const TENOR_ALGORITHM = 'branch_and_bound';
31
+
32
+ const tenorQuoteError = (reason?: string): string => (
33
+ reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable'
34
+ );
35
+
36
+ type TenorBookKey = 'asks' | 'bids';
37
+ export const tenorBookKeyFor = (side: MorphoMidnightBookSide): TenorBookKey => (side === 'bids' ? 'asks' : 'bids');
38
+
39
+ // Tenor order-book levels are annualized APR in basis points: 25 → 0.25%, 700 → 7%.
40
+ export const tenorBookRateToApyPercent = (rate: Dec.Value): string => new Dec(rate || 0).div(100).toString();
41
+
42
+ const tenorFillPrice = (assets: Dec.Value, units: Dec.Value): string => (
43
+ new Dec(units).lte(0) ? '0' : new Dec(assets).div(units).toString()
44
+ );
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
+ interface TenorOfferCollateral {
59
+ token: string;
60
+ lltv: string | number;
61
+ liquidation_cursor: string | number;
62
+ oracle: string;
63
+ }
64
+
65
+ interface TenorOffer {
66
+ start: string | number;
67
+ group?: string;
68
+ callback?: string;
69
+ tick: string | number;
70
+ chain_id: string | number;
71
+ maturity: string | number;
72
+ buy: boolean;
73
+ maker: string;
74
+ loan_token_address: string;
75
+ callback_data?: string;
76
+ expiry: string | number;
77
+ ratifier?: string;
78
+ collaterals: TenorOfferCollateral[];
79
+ continuous_fee_cap: string | number;
80
+ enter_gate: string;
81
+ liquidator_gate: string;
82
+ max_assets: string | number;
83
+ max_units: string | number;
84
+ ratifier_data?: string;
85
+ rcf_threshold: string | number;
86
+ receiver_if_maker_is_seller?: string;
87
+ reduce_only: boolean;
88
+ }
89
+
90
+ interface TenorOfferFill {
91
+ units: string | number;
92
+ offer: TenorOffer;
93
+ }
94
+
95
+ /**
96
+ * Tenor's offer JSON is flat (market fields live on the offer). Morpho's is nested, and that nested
97
+ * shape is what the app encodes for `Midnight.take`. Map Tenor into that shape so recipes stay on one encoder.
98
+ */
99
+ export const tenorOfferToApiOffer = (offer: TenorOffer) => ({
100
+ market: {
101
+ chain_id: offer.chain_id,
102
+ midnight: MIDNIGHT_BASE,
103
+ loan_token: offer.loan_token_address,
104
+ collaterals: offer.collaterals || [],
105
+ maturity: offer.maturity,
106
+ rcf_threshold: offer.rcf_threshold,
107
+ enter_gate: offer.enter_gate,
108
+ liquidator_gate: offer.liquidator_gate,
109
+ },
110
+ buy: offer.buy,
111
+ maker: offer.maker,
112
+ start: offer.start,
113
+ expiry: offer.expiry,
114
+ tick: offer.tick,
115
+ group: offer.group || ZERO_BYTES32,
116
+ callback: offer.callback || ZERO_ADDRESS,
117
+ callback_data: offer.callback_data || '0x',
118
+ receiver_if_maker_is_seller: offer.receiver_if_maker_is_seller || ZERO_ADDRESS,
119
+ ratifier: offer.ratifier || ZERO_ADDRESS,
120
+ reduce_only: offer.reduce_only,
121
+ max_units: offer.max_units,
122
+ max_assets: offer.max_assets,
123
+ continuous_fee_cap: offer.continuous_fee_cap,
124
+ });
125
+
126
+ export const tenorOfferFillToApiFill = (fill: TenorOfferFill) => ({
127
+ units: fill.units,
128
+ offer: tenorOfferToApiOffer(fill.offer),
129
+ ratifier_data: fill.offer.ratifier_data || '0x',
130
+ market_id: '',
131
+ });
132
+
133
+ interface TenorBookBucket {
134
+ rate: number | string;
135
+ liquidity: number | string;
136
+ cumulative_liquidity?: number | string;
137
+ }
138
+
139
+ interface TenorBookSidePayload {
140
+ buckets?: TenorBookBucket[];
141
+ }
142
+
143
+ interface TenorBookResponse {
144
+ asks?: TenorBookSidePayload | TenorBookBucket[];
145
+ bids?: TenorBookSidePayload | TenorBookBucket[];
146
+ }
147
+
148
+ const tenorBookBuckets = (side?: TenorBookSidePayload | TenorBookBucket[]): TenorBookBucket[] => {
149
+ if (!side) return [];
150
+ if (Array.isArray(side)) return side;
151
+ return side.buckets || [];
152
+ };
153
+
154
+ /** The levels resting on the Tenor side a taker of `side` fills, in raw loan-token base units. */
155
+ export const parseTenorBookSide = (
156
+ json: TenorBookResponse,
157
+ side: MorphoMidnightBookSide,
158
+ ) => tenorBookBuckets(json[tenorBookKeyFor(side)])
159
+ .map((bucket) => ({
160
+ rate: tenorBookRateToApyPercent(bucket.rate),
161
+ liquidityRaw: new Dec(bucket.liquidity || 0).toFixed(0),
162
+ }))
163
+ .filter((offer) => new Dec(offer.liquidityRaw).gt(0));
164
+
165
+ export const parseTenorOrderBook = (
166
+ json: TenorBookResponse,
167
+ side: MorphoMidnightBookSide,
168
+ loanSymbol: string,
169
+ ): MorphoMidnightParsedBook | null => {
170
+ const offers: MorphoMidnightBookOffer[] = parseTenorBookSide(json, side).map((offer) => ({
171
+ rate: offer.rate,
172
+ liquidity: assetAmountInEth(offer.liquidityRaw, loanSymbol),
173
+ }));
174
+
175
+ return buildMidnightParsedBook(offers, side);
176
+ };
177
+
178
+ const fetchTenorBook = async (marketId: string, network: NetworkNumber): Promise<TenorBookResponse> => {
179
+ const res = await fetch(
180
+ `${TENOR_ORDERBOOK_URL}/${marketId}?chain_id=${network}`,
181
+ { signal: AbortSignal.timeout(TENOR_TIMEOUT_MS) },
182
+ );
183
+ if (!res.ok) throw new Error(`Midnight book request failed for ${marketId} (${res.status})`);
184
+ return res.json();
185
+ };
186
+
187
+ export const getTenorMarketBook = async (
188
+ market: MorphoMidnightMarketData,
189
+ network: NetworkNumber,
190
+ side: MorphoMidnightBookSide = 'bids',
191
+ ): Promise<MorphoMidnightParsedBook | null> => {
192
+ const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
193
+ return parseTenorOrderBook(await fetchTenorBook(market.marketId, network), side, loanSymbol);
194
+ };
195
+
196
+ const tenorBookAvailableAssetsRaw = async (
197
+ marketId: string,
198
+ network: NetworkNumber,
199
+ side: MorphoMidnightBookSide,
200
+ ): Promise<string> => {
201
+ try {
202
+ const json = await fetchTenorBook(marketId, network);
203
+ return parseTenorBookSide(json, side).reduce((sum, offer) => sum.add(offer.liquidityRaw), new Dec(0)).toFixed(0);
204
+ } catch {
205
+ return '0';
206
+ }
207
+ };
208
+
209
+ interface TenorQuotePayload {
210
+ units?: string;
211
+ rate?: string;
212
+ buyer_assets?: string;
213
+ seller_assets?: string;
214
+ offers?: TenorOfferFill[];
215
+ }
216
+
217
+ interface TenorQuoteResponse extends TenorQuotePayload {
218
+ quotes?: TenorQuotePayload[];
219
+ error?: string;
220
+ message?: string;
221
+ }
222
+
223
+ const fetchTenorQuote = async ({
224
+ marketId,
225
+ side,
226
+ assets,
227
+ units,
228
+ taker,
229
+ network = NetworkNumber.Base,
230
+ }: {
231
+ marketId: string,
232
+ side: MorphoMidnightBookSide,
233
+ assets?: string,
234
+ units?: string,
235
+ taker?: string,
236
+ network?: NetworkNumber,
237
+ }) => {
238
+ const hasAssets = assets !== undefined && assets !== null;
239
+ const hasUnits = units !== undefined && units !== null;
240
+ if (hasAssets === hasUnits) {
241
+ throw new Error(tenorQuoteError('Either assets or units must be provided'));
242
+ }
243
+ if (side !== 'asks' && side !== 'bids') {
244
+ throw new Error(tenorQuoteError(`Unsupported Tenor quote side: ${side}`));
245
+ }
246
+
247
+ const isBuy = side === 'asks';
248
+ const amount = (hasAssets ? assets : units) as string;
249
+ const res = await fetch(TENOR_QUOTES_URL, {
250
+ method: 'POST',
251
+ headers: { 'Content-Type': 'application/json' },
252
+ body: JSON.stringify({
253
+ market_hashes: [marketId],
254
+ amount,
255
+ chain_id: network,
256
+ is_buy: isBuy,
257
+ is_exact_in: hasAssets ? isBuy : !isBuy,
258
+ limit_rate: null,
259
+ valid_for: null,
260
+ algorithm: TENOR_ALGORITHM,
261
+ allow_partial: false,
262
+ taker: taker || ZERO_ADDRESS,
263
+ }),
264
+ signal: AbortSignal.timeout(TENOR_TIMEOUT_MS),
265
+ });
266
+
267
+ const result: TenorQuoteResponse = await res.json().catch(() => ({} as TenorQuoteResponse));
268
+ if (!res.ok) {
269
+ throw new Error(tenorQuoteError(result.error || result.message || `Tenor quote request failed with status ${res.status}`));
270
+ }
271
+
272
+ const payload: TenorQuotePayload = result.quotes?.[0] || result;
273
+ const quotedUnits = new Dec(payload.units || result.units || 0);
274
+ const offers = payload.offers || result.offers || [];
275
+ // A book that can't fill the size comes back 200 with everything zeroed rather than as an error.
276
+ if (quotedUnits.lte(0) || offers.length === 0) {
277
+ throw new Error(tenorQuoteError('INSUFFICIENT_LIQUIDITY'));
278
+ }
279
+
280
+ return {
281
+ units: quotedUnits.toFixed(0),
282
+ buyerAssets: (payload.buyer_assets || result.buyer_assets || '0').toString(),
283
+ offerFills: offers.map(tenorOfferFillToApiFill),
284
+ };
285
+ };
286
+
287
+ const TENOR_NO_AVAILABLE_UNITS = '0';
288
+
289
+ export const getTenorBorrowQuote = async (
290
+ marketId: string,
291
+ assetsRaw: string,
292
+ slippagePercent: Dec.Value,
293
+ maturity: number,
294
+ maxBorrowRate?: Dec.Value,
295
+ taker?: string,
296
+ network: NetworkNumber = NetworkNumber.Base,
297
+ ): Promise<MorphoMidnightBorrowQuote> => {
298
+ if (!isTenorMidnightMarket(marketId)) {
299
+ throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
300
+ }
301
+
302
+ const [quote, availableAssets] = await Promise.all([
303
+ fetchTenorQuote({
304
+ marketId, side: 'bids', assets: assetsRaw, taker, network,
305
+ }),
306
+ tenorBookAvailableAssetsRaw(marketId, network, 'bids'),
307
+ ]);
308
+
309
+ const ttmDays = midnightTimeToMaturityDays(maturity);
310
+ const bestPrice = tenorFillPrice(assetsRaw, quote.units);
311
+ const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
312
+
313
+ const capPrice = tenorCapPrice(bestPrice, slippagePercent, -1, ttmDays, maxBorrowRate);
314
+ const maxRate = midnightApyFromPrice(capPrice, ttmDays);
315
+ const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
316
+
317
+ return {
318
+ bestPrice,
319
+ worstPrice: capPrice,
320
+ estBorrowRate,
321
+ maxRate,
322
+ newUnits: quote.units,
323
+ maxUnits,
324
+ availableAssets,
325
+ availableUnits: TENOR_NO_AVAILABLE_UNITS,
326
+ takeableOffers: quote.offerFills,
327
+ };
328
+ };
329
+
330
+ export const getTenorPaybackQuote = async (
331
+ marketId: string,
332
+ assetsRaw: string,
333
+ slippagePercent: Dec.Value,
334
+ maturity: number,
335
+ minPaybackRate?: Dec.Value,
336
+ taker?: string,
337
+ network: NetworkNumber = NetworkNumber.Base,
338
+ ): Promise<MorphoMidnightPaybackQuote> => {
339
+ if (!isTenorMidnightMarket(marketId)) {
340
+ throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
341
+ }
342
+
343
+ const [quote, availableAssets] = await Promise.all([
344
+ fetchTenorQuote({
345
+ marketId, side: 'asks', assets: assetsRaw, taker, network,
346
+ }),
347
+ tenorBookAvailableAssetsRaw(marketId, network, 'asks'),
348
+ ]);
349
+
350
+ const ttmDays = midnightTimeToMaturityDays(maturity);
351
+ const bestPrice = tenorFillPrice(assetsRaw, quote.units);
352
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
353
+
354
+ const capPrice = tenorCapPrice(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
355
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
356
+ const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
357
+
358
+ return {
359
+ bestPrice,
360
+ worstPrice: capPrice,
361
+ estPaybackRate,
362
+ minRate,
363
+ newUnits: quote.units,
364
+ minUnits,
365
+ availableAssets,
366
+ availableUnits: TENOR_NO_AVAILABLE_UNITS,
367
+ takeableOffers: quote.offerFills,
368
+ };
369
+ };
370
+
371
+ export const getTenorPaybackUnitsQuote = async (
372
+ marketId: string,
373
+ unitsRaw: string,
374
+ slippagePercent: Dec.Value,
375
+ maturity: number,
376
+ minPaybackRate?: Dec.Value,
377
+ taker?: string,
378
+ network: NetworkNumber = NetworkNumber.Base,
379
+ ): Promise<MorphoMidnightPaybackUnitsQuote> => {
380
+ if (!isTenorMidnightMarket(marketId)) {
381
+ throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
382
+ }
383
+
384
+ const [quote, availableAssets] = await Promise.all([
385
+ fetchTenorQuote({
386
+ marketId, side: 'asks', units: unitsRaw, taker, network,
387
+ }),
388
+ tenorBookAvailableAssetsRaw(marketId, network, 'asks'),
389
+ ]);
390
+
391
+ const ttmDays = midnightTimeToMaturityDays(maturity);
392
+ const newAssets = quote.buyerAssets;
393
+ const bestPrice = tenorFillPrice(newAssets, unitsRaw);
394
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
395
+
396
+ const capPrice = tenorCapPrice(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
397
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
398
+ const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
399
+
400
+ return {
401
+ bestPrice,
402
+ worstPrice: capPrice,
403
+ estPaybackRate,
404
+ minRate,
405
+ newAssets,
406
+ maxAssets,
407
+ availableAssets,
408
+ availableUnits: TENOR_NO_AVAILABLE_UNITS,
409
+ takeableOffers: quote.offerFills,
410
+ };
411
+ };
@@ -17,7 +17,9 @@ export {
17
17
  export { SparkMarkets } from './spark';
18
18
  export { CrvUsdMarkets } from './curveUsd';
19
19
  export { MorphoBlueMarkets, findMorphoBlueMarket } from './morphoBlue';
20
- export { MorphoMidnightMarkets, findMorphoMidnightMarket } from './morphoMidnight';
20
+ export {
21
+ MorphoMidnightMarkets, findMorphoMidnightMarket, isTenorMidnightMarket, morphoMidnightMarketCollateralParams,
22
+ } from './morphoMidnight';
21
23
  export { LlamaLendMarkets } from './llamaLend';
22
24
  export { LiquityV2Markets, findLiquityV2MarketByAddress } from './liquityV2';
23
25
  export { EulerV2Markets } from './euler';