@defisaver/positions-sdk 2.1.127-midnight-5-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.
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 +2 -1
  11. package/cjs/markets/morphoMidnight/index.d.ts +37 -0
  12. package/cjs/markets/morphoMidnight/index.js +434 -42
  13. package/cjs/types/morphoMidnight.d.ts +18 -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 +37 -0
  26. package/esm/markets/morphoMidnight/index.js +399 -23
  27. package/esm/types/morphoMidnight.d.ts +18 -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 +1 -1
  35. package/src/markets/morphoMidnight/index.ts +424 -23
  36. package/src/types/morphoMidnight.ts +19 -0
@@ -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'];
@@ -1,6 +1,8 @@
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';
5
+ export { tenorBookKeyFor, tenorBookRateToApyPercent, tenorOfferFillToApiFill, tenorOfferToApiOffer, } from './tenor';
4
6
  /**
5
7
  * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
6
8
  * the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
@@ -54,17 +56,6 @@ export interface MorphoMidnightPaybackUnitsQuote {
54
56
  availableUnits: string;
55
57
  takeableOffers: any[];
56
58
  }
57
- export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
58
- export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
59
- /**
60
- * Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
61
- * price = (1 + rate)^(−ttmDays / 365).
62
- *
63
- * This is what turns an absolute rate ceiling into an on-chain `maxUnits` cap (units = assets / price),
64
- * and equally the principal a unit of borrow power is worth — Midnight debt is recorded at its maturity
65
- * face value, so borrowing the full limit as principal would overshoot it by the interest.
66
- */
67
- export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec.Value) => string;
68
59
  /**
69
60
  * Coerce a slippage into what the quote endpoint accepts: 0.1–100 with at most one decimal place. The
70
61
  * validation is lexical, so a computed value (`4.15066671050631467`) is rejected outright — without this
@@ -113,8 +104,11 @@ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketD
113
104
  *
114
105
  * A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
115
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).
116
110
  */
117
- export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
111
+ export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightBorrowQuote>;
118
112
  /**
119
113
  * Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
120
114
  * retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
@@ -133,7 +127,7 @@ export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw:
133
127
  * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
134
128
  * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
135
129
  */
136
- export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
130
+ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightPaybackQuote>;
137
131
  /**
138
132
  * Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
139
133
  * me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
@@ -149,4 +143,4 @@ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw
149
143
  * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
150
144
  * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
151
145
  */
152
- export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackUnitsQuote>;
146
+ export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string) => Promise<MorphoMidnightPaybackUnitsQuote>;
@@ -12,8 +12,13 @@ import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
12
12
  import { calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos, } from '../../moneymarket';
13
13
  import { calculateNetApy } from '../../staking';
14
14
  import { LeverageType, } from '../../types/common';
15
- import { SECONDS_PER_DAY, WAD } from '../../constants';
15
+ import { WAD } from '../../constants';
16
16
  import { LONGER_TIMEOUT } from '../../services/utils';
17
+ import { isTenorMidnightMarket } from '../../markets/morphoMidnight';
18
+ import { buildMidnightParsedBook, midnightApyFromPrice, midnightPriceFromApy, midnightTimeToMaturityDays, } from './rate';
19
+ import { getTenorBorrowQuote, getTenorMarketBook, getTenorPaybackQuote, getTenorPaybackUnitsQuote, } from './tenor';
20
+ export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightPriceFromApy, midnightTimeToMaturityDays, } from './rate';
21
+ export { tenorBookKeyFor, tenorBookRateToApyPercent, tenorOfferFillToApiFill, tenorOfferToApiOffer, } from './tenor';
17
22
  /**
18
23
  * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
19
24
  * the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
@@ -78,7 +83,6 @@ export const getMorphoMidnightAggregatedPositionData = ({ usedAssets, assetsData
78
83
  // Quote prices are WAD-scaled
79
84
  // loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
80
85
  const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
81
- const nowInSeconds = () => Math.floor(Date.now() / 1000);
82
86
  // The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
83
87
  // enough that markets drop out of the list for no reason.
84
88
  const MIDNIGHT_BOOK_TIMEOUT = 30000;
@@ -86,34 +90,6 @@ const MIDNIGHT_BOOK_TIMEOUT = 30000;
86
90
  // place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
87
91
  const MIDNIGHT_SLIPPAGE_MIN = 0.1;
88
92
  const MIDNIGHT_SLIPPAGE_MAX = 100;
89
- // Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
90
- export const midnightTimeToMaturityDays = (maturity, atSeconds = nowInSeconds()) => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
91
- // Annualize a fixed-term discount price into an APY percent: (1 / price)^(365 / ttmDays) − 1.
92
- // `price` is loan-per-unit (assets received / units owed), so 1/price ≥ 1.
93
- export const midnightApyFromPrice = (price, ttmDays) => {
94
- const p = new Dec(price);
95
- const ttm = new Dec(ttmDays);
96
- if (p.lte(0) || ttm.lte(0))
97
- return '0';
98
- return new Dec(1).div(p).pow(new Dec(365).div(ttm)).sub(1)
99
- .mul(100)
100
- .toString();
101
- };
102
- /**
103
- * Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
104
- * price = (1 + rate)^(−ttmDays / 365).
105
- *
106
- * This is what turns an absolute rate ceiling into an on-chain `maxUnits` cap (units = assets / price),
107
- * and equally the principal a unit of borrow power is worth — Midnight debt is recorded at its maturity
108
- * face value, so borrowing the full limit as principal would overshoot it by the interest.
109
- */
110
- export const midnightPriceFromApy = (ratePercent, ttmDays) => {
111
- const rate = new Dec(ratePercent);
112
- const ttm = new Dec(ttmDays);
113
- if (rate.lte(0) || ttm.lte(0))
114
- return '1';
115
- return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
116
- };
117
93
  /**
118
94
  * Coerce a slippage into what the quote endpoint accepts: 0.1–100 with at most one decimal place. The
119
95
  * validation is lexical, so a computed value (`4.15066671050631467`) is rejected outright — without this
@@ -159,26 +135,20 @@ export const getMorphoMidnightUserBorrowInfo = (account, marketId, loanTokenSymb
159
135
  */
160
136
  export const getMorphoMidnightMarketBook = (market_1, network_1, ...args_1) => __awaiter(void 0, [market_1, network_1, ...args_1], void 0, function* (market, network, side = 'bids') {
161
137
  var _a;
138
+ if (isTenorMidnightMarket(market)) {
139
+ return getTenorMarketBook(market, network, side);
140
+ }
162
141
  const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
163
142
  const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
164
143
  if (!res.ok)
165
144
  throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
166
145
  const json = yield res.json();
167
146
  const ttmDays = midnightTimeToMaturityDays(market.maturity);
168
- const bestFirst = side === 'asks' ? -1 : 1;
169
- const offers = (((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a[side]) || [])
170
- .map((offer) => ({
147
+ const offers = (((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a[side]) || []).map((offer) => ({
171
148
  rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
172
149
  liquidity: assetAmountInEth(offer.assets, loanSymbol),
173
- }))
174
- .sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
175
- if (offers.length === 0)
176
- return null;
177
- return {
178
- bestRate: offers[0].rate,
179
- totalLiquidity: offers.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
180
- offers,
181
- };
150
+ }));
151
+ return buildMidnightParsedBook(offers, side);
182
152
  });
183
153
  // The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
184
154
  // (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
@@ -221,8 +191,14 @@ const fetchMorphoMidnightQuote = (marketId, side, size, slippagePercent) => __aw
221
191
  *
222
192
  * A `maxBorrowRate` below `estBorrowRate` yields `maxUnits < newUnits` — the borrow would revert on-chain.
223
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).
224
197
  */
225
- export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
198
+ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
199
+ if (isTenorMidnightMarket(marketId)) {
200
+ return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker);
201
+ }
226
202
  const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
227
203
  const { bestPrice, worstPrice } = quote;
228
204
  const ttmDays = midnightTimeToMaturityDays(maturity);
@@ -258,7 +234,10 @@ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercen
258
234
  * A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
259
235
  * on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
260
236
  */
261
- export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
237
+ export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
238
+ if (isTenorMidnightMarket(marketId)) {
239
+ return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker);
240
+ }
262
241
  const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
263
242
  const { bestPrice, worstPrice } = quote;
264
243
  const ttmDays = midnightTimeToMaturityDays(maturity);
@@ -291,7 +270,10 @@ export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePerce
291
270
  * A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
292
271
  * buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
293
272
  */
294
- export const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
273
+ export const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker) => __awaiter(void 0, void 0, void 0, function* () {
274
+ if (isTenorMidnightMarket(marketId)) {
275
+ return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker);
276
+ }
295
277
  const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
296
278
  const { bestPrice, worstPrice } = quote;
297
279
  const ttmDays = midnightTimeToMaturityDays(maturity);
@@ -0,0 +1,11 @@
1
+ import Dec from 'decimal.js';
2
+ import { MorphoMidnightBookOffer, MorphoMidnightBookSide, MorphoMidnightParsedBook } from '../../types';
3
+ export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
4
+ export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
5
+ /**
6
+ * Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
7
+ * price = (1 + rate)^(−ttmDays / 365).
8
+ */
9
+ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec.Value) => string;
10
+ export declare const midnightBookBestFirst: (side: MorphoMidnightBookSide) => 1 | -1;
11
+ export declare const buildMidnightParsedBook: (offers: MorphoMidnightBookOffer[], side: MorphoMidnightBookSide) => MorphoMidnightParsedBook | null;
@@ -0,0 +1,38 @@
1
+ import Dec from 'decimal.js';
2
+ import { SECONDS_PER_DAY } from '../../constants';
3
+ const nowInSeconds = () => Math.floor(Date.now() / 1000);
4
+ // Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
5
+ export const midnightTimeToMaturityDays = (maturity, atSeconds = nowInSeconds()) => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
6
+ // Annualize a fixed-term discount price into an APY percent: (1 / price)^(365 / ttmDays) − 1.
7
+ // `price` is loan-per-unit (assets received / units owed), so 1/price ≥ 1.
8
+ export const midnightApyFromPrice = (price, ttmDays) => {
9
+ const p = new Dec(price);
10
+ const ttm = new Dec(ttmDays);
11
+ if (p.lte(0) || ttm.lte(0))
12
+ return '0';
13
+ return new Dec(1).div(p).pow(new Dec(365).div(ttm)).sub(1)
14
+ .mul(100)
15
+ .toString();
16
+ };
17
+ /**
18
+ * Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
19
+ * price = (1 + rate)^(−ttmDays / 365).
20
+ */
21
+ export const midnightPriceFromApy = (ratePercent, ttmDays) => {
22
+ const rate = new Dec(ratePercent);
23
+ const ttm = new Dec(ttmDays);
24
+ if (rate.lte(0) || ttm.lte(0))
25
+ return '1';
26
+ return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
27
+ };
28
+ export const midnightBookBestFirst = (side) => (side === 'asks' ? -1 : 1);
29
+ export const buildMidnightParsedBook = (offers, side) => {
30
+ const bestFirst = [...offers].sort((a, b) => new Dec(a.rate).minus(b.rate).mul(midnightBookBestFirst(side)).toNumber());
31
+ if (bestFirst.length === 0)
32
+ return null;
33
+ return {
34
+ bestRate: bestFirst[0].rate,
35
+ totalLiquidity: bestFirst.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
36
+ offers: bestFirst,
37
+ };
38
+ };
@@ -0,0 +1,125 @@
1
+ import Dec from 'decimal.js';
2
+ import { MorphoMidnightBookSide, MorphoMidnightMarketData, MorphoMidnightParsedBook, NetworkNumber } from '../../types';
3
+ import type { MorphoMidnightBorrowQuote, MorphoMidnightPaybackQuote, MorphoMidnightPaybackUnitsQuote } from './index';
4
+ type TenorBookKey = 'asks' | 'bids';
5
+ export declare const tenorBookKeyFor: (side: MorphoMidnightBookSide) => TenorBookKey;
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
+ interface TenorOfferCollateral {
9
+ token: string;
10
+ lltv: string | number;
11
+ liquidation_cursor: string | number;
12
+ oracle: string;
13
+ }
14
+ interface TenorOffer {
15
+ start: string | number;
16
+ group?: string;
17
+ callback?: string;
18
+ tick: string | number;
19
+ chain_id: string | number;
20
+ maturity: string | number;
21
+ buy: boolean;
22
+ maker: string;
23
+ loan_token_address: string;
24
+ callback_data?: string;
25
+ expiry: string | number;
26
+ ratifier?: string;
27
+ collaterals: TenorOfferCollateral[];
28
+ continuous_fee_cap: string | number;
29
+ enter_gate: string;
30
+ liquidator_gate: string;
31
+ max_assets: string | number;
32
+ max_units: string | number;
33
+ ratifier_data?: string;
34
+ rcf_threshold: string | number;
35
+ receiver_if_maker_is_seller?: string;
36
+ reduce_only: boolean;
37
+ }
38
+ interface TenorOfferFill {
39
+ units: string | number;
40
+ offer: TenorOffer;
41
+ }
42
+ /**
43
+ * Tenor's offer JSON is flat (market fields live on the offer). Morpho's is nested, and that nested
44
+ * shape is what the app encodes for `Midnight.take`. Map Tenor into that shape so recipes stay on one encoder.
45
+ */
46
+ export declare const tenorOfferToApiOffer: (offer: TenorOffer) => {
47
+ market: {
48
+ chain_id: string | number;
49
+ midnight: "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A";
50
+ loan_token: string;
51
+ collaterals: TenorOfferCollateral[];
52
+ maturity: string | number;
53
+ rcf_threshold: string | number;
54
+ enter_gate: string;
55
+ liquidator_gate: string;
56
+ };
57
+ buy: boolean;
58
+ maker: string;
59
+ start: string | number;
60
+ expiry: string | number;
61
+ tick: string | number;
62
+ group: string;
63
+ callback: string;
64
+ callback_data: string;
65
+ receiver_if_maker_is_seller: string;
66
+ ratifier: string;
67
+ reduce_only: boolean;
68
+ max_units: string | number;
69
+ max_assets: string | number;
70
+ continuous_fee_cap: string | number;
71
+ };
72
+ export declare const tenorOfferFillToApiFill: (fill: TenorOfferFill) => {
73
+ units: string | number;
74
+ offer: {
75
+ market: {
76
+ chain_id: string | number;
77
+ midnight: "0xAdedD8ab6dE832766Fedf0FaC4992E5C4D3EA18A";
78
+ loan_token: string;
79
+ collaterals: TenorOfferCollateral[];
80
+ maturity: string | number;
81
+ rcf_threshold: string | number;
82
+ enter_gate: string;
83
+ liquidator_gate: string;
84
+ };
85
+ buy: boolean;
86
+ maker: string;
87
+ start: string | number;
88
+ expiry: string | number;
89
+ tick: string | number;
90
+ group: string;
91
+ callback: string;
92
+ callback_data: string;
93
+ receiver_if_maker_is_seller: string;
94
+ ratifier: string;
95
+ reduce_only: boolean;
96
+ max_units: string | number;
97
+ max_assets: string | number;
98
+ continuous_fee_cap: string | number;
99
+ };
100
+ ratifier_data: string;
101
+ market_id: string;
102
+ };
103
+ interface TenorBookBucket {
104
+ rate: number | string;
105
+ liquidity: number | string;
106
+ cumulative_liquidity?: number | string;
107
+ }
108
+ interface TenorBookSidePayload {
109
+ buckets?: TenorBookBucket[];
110
+ }
111
+ interface TenorBookResponse {
112
+ asks?: TenorBookSidePayload | TenorBookBucket[];
113
+ bids?: TenorBookSidePayload | TenorBookBucket[];
114
+ }
115
+ /** The levels resting on the Tenor side a taker of `side` fills, in raw loan-token base units. */
116
+ export declare const parseTenorBookSide: (json: TenorBookResponse, side: MorphoMidnightBookSide) => {
117
+ rate: string;
118
+ liquidityRaw: string;
119
+ }[];
120
+ export declare const parseTenorOrderBook: (json: TenorBookResponse, side: MorphoMidnightBookSide, loanSymbol: string) => MorphoMidnightParsedBook | null;
121
+ 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>;
125
+ export {};
@@ -0,0 +1,239 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import Dec from 'decimal.js';
11
+ import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
12
+ import { ZERO_ADDRESS, ZERO_BYTES32 } from '../../constants';
13
+ import { NetworkNumber, } from '../../types';
14
+ import { isTenorMidnightMarket, MIDNIGHT_BASE } from '../../markets/morphoMidnight';
15
+ import { buildMidnightParsedBook, midnightApyFromPrice, midnightPriceFromApy, midnightTimeToMaturityDays, } from './rate';
16
+ // Notion doc by Rajko: https://app.notion.com/p/defisaver/Tenor-API-3ba0be682adc80dfad35c81a9a4cb442
17
+ const TENOR_QUOTES_URL = 'https://router.tenor.finance/v1/quotes';
18
+ const TENOR_ORDERBOOK_URL = 'https://router.tenor.finance/v1/orderbook';
19
+ const TENOR_TIMEOUT_MS = 15000;
20
+ const TENOR_ALGORITHM = 'branch_and_bound';
21
+ const tenorQuoteError = (reason) => (reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable');
22
+ export const tenorBookKeyFor = (side) => (side === 'bids' ? 'asks' : 'bids');
23
+ // Tenor order-book levels are annualized APR in basis points: 25 → 0.25%, 700 → 7%.
24
+ export const tenorBookRateToApyPercent = (rate) => new Dec(rate || 0).div(100).toString();
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
+ /**
33
+ * Tenor's offer JSON is flat (market fields live on the offer). Morpho's is nested, and that nested
34
+ * shape is what the app encodes for `Midnight.take`. Map Tenor into that shape so recipes stay on one encoder.
35
+ */
36
+ export const tenorOfferToApiOffer = (offer) => ({
37
+ market: {
38
+ chain_id: offer.chain_id,
39
+ midnight: MIDNIGHT_BASE,
40
+ loan_token: offer.loan_token_address,
41
+ collaterals: offer.collaterals || [],
42
+ maturity: offer.maturity,
43
+ rcf_threshold: offer.rcf_threshold,
44
+ enter_gate: offer.enter_gate,
45
+ liquidator_gate: offer.liquidator_gate,
46
+ },
47
+ buy: offer.buy,
48
+ maker: offer.maker,
49
+ start: offer.start,
50
+ expiry: offer.expiry,
51
+ tick: offer.tick,
52
+ group: offer.group || ZERO_BYTES32,
53
+ callback: offer.callback || ZERO_ADDRESS,
54
+ callback_data: offer.callback_data || '0x',
55
+ receiver_if_maker_is_seller: offer.receiver_if_maker_is_seller || ZERO_ADDRESS,
56
+ ratifier: offer.ratifier || ZERO_ADDRESS,
57
+ reduce_only: offer.reduce_only,
58
+ max_units: offer.max_units,
59
+ max_assets: offer.max_assets,
60
+ continuous_fee_cap: offer.continuous_fee_cap,
61
+ });
62
+ export const tenorOfferFillToApiFill = (fill) => ({
63
+ units: fill.units,
64
+ offer: tenorOfferToApiOffer(fill.offer),
65
+ ratifier_data: fill.offer.ratifier_data || '0x',
66
+ market_id: '',
67
+ });
68
+ const tenorBookBuckets = (side) => {
69
+ if (!side)
70
+ return [];
71
+ if (Array.isArray(side))
72
+ return side;
73
+ return side.buckets || [];
74
+ };
75
+ /** The levels resting on the Tenor side a taker of `side` fills, in raw loan-token base units. */
76
+ export const parseTenorBookSide = (json, side) => tenorBookBuckets(json[tenorBookKeyFor(side)])
77
+ .map((bucket) => ({
78
+ rate: tenorBookRateToApyPercent(bucket.rate),
79
+ liquidityRaw: new Dec(bucket.liquidity || 0).toFixed(0),
80
+ }))
81
+ .filter((offer) => new Dec(offer.liquidityRaw).gt(0));
82
+ export const parseTenorOrderBook = (json, side, loanSymbol) => {
83
+ const offers = parseTenorBookSide(json, side).map((offer) => ({
84
+ rate: offer.rate,
85
+ liquidity: assetAmountInEth(offer.liquidityRaw, loanSymbol),
86
+ }));
87
+ return buildMidnightParsedBook(offers, side);
88
+ };
89
+ const fetchTenorBook = (marketId, network) => __awaiter(void 0, void 0, void 0, function* () {
90
+ const res = yield fetch(`${TENOR_ORDERBOOK_URL}/${marketId}?chain_id=${network}`, { signal: AbortSignal.timeout(TENOR_TIMEOUT_MS) });
91
+ if (!res.ok)
92
+ throw new Error(`Midnight book request failed for ${marketId} (${res.status})`);
93
+ return res.json();
94
+ });
95
+ export const getTenorMarketBook = (market_1, network_1, ...args_1) => __awaiter(void 0, [market_1, network_1, ...args_1], void 0, function* (market, network, side = 'bids') {
96
+ const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
97
+ return parseTenorOrderBook(yield fetchTenorBook(market.marketId, network), side, loanSymbol);
98
+ });
99
+ const tenorBookAvailableAssetsRaw = (marketId, network, side) => __awaiter(void 0, void 0, void 0, function* () {
100
+ try {
101
+ const json = yield fetchTenorBook(marketId, network);
102
+ return parseTenorBookSide(json, side).reduce((sum, offer) => sum.add(offer.liquidityRaw), new Dec(0)).toFixed(0);
103
+ }
104
+ catch (_a) {
105
+ return '0';
106
+ }
107
+ });
108
+ const fetchTenorQuote = (_a) => __awaiter(void 0, [_a], void 0, function* ({ marketId, side, assets, units, taker, network = NetworkNumber.Base, }) {
109
+ var _b;
110
+ const hasAssets = assets !== undefined && assets !== null;
111
+ const hasUnits = units !== undefined && units !== null;
112
+ if (hasAssets === hasUnits) {
113
+ throw new Error(tenorQuoteError('Either assets or units must be provided'));
114
+ }
115
+ if (side !== 'asks' && side !== 'bids') {
116
+ throw new Error(tenorQuoteError(`Unsupported Tenor quote side: ${side}`));
117
+ }
118
+ const isBuy = side === 'asks';
119
+ const amount = (hasAssets ? assets : units);
120
+ const res = yield fetch(TENOR_QUOTES_URL, {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify({
124
+ market_hashes: [marketId],
125
+ amount,
126
+ chain_id: network,
127
+ is_buy: isBuy,
128
+ is_exact_in: hasAssets ? isBuy : !isBuy,
129
+ limit_rate: null,
130
+ valid_for: null,
131
+ algorithm: TENOR_ALGORITHM,
132
+ allow_partial: false,
133
+ taker: taker || ZERO_ADDRESS,
134
+ }),
135
+ signal: AbortSignal.timeout(TENOR_TIMEOUT_MS),
136
+ });
137
+ const result = yield res.json().catch(() => ({}));
138
+ if (!res.ok) {
139
+ throw new Error(tenorQuoteError(result.error || result.message || `Tenor quote request failed with status ${res.status}`));
140
+ }
141
+ const payload = ((_b = result.quotes) === null || _b === void 0 ? void 0 : _b[0]) || result;
142
+ const quotedUnits = new Dec(payload.units || result.units || 0);
143
+ const offers = payload.offers || result.offers || [];
144
+ // A book that can't fill the size comes back 200 with everything zeroed rather than as an error.
145
+ if (quotedUnits.lte(0) || offers.length === 0) {
146
+ throw new Error(tenorQuoteError('INSUFFICIENT_LIQUIDITY'));
147
+ }
148
+ return {
149
+ units: quotedUnits.toFixed(0),
150
+ buyerAssets: (payload.buyer_assets || result.buyer_assets || '0').toString(),
151
+ offerFills: offers.map(tenorOfferFillToApiFill),
152
+ };
153
+ });
154
+ 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) {
156
+ if (!isTenorMidnightMarket(marketId)) {
157
+ throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
158
+ }
159
+ const [quote, availableAssets] = yield Promise.all([
160
+ fetchTenorQuote({
161
+ marketId, side: 'bids', assets: assetsRaw, taker, network,
162
+ }),
163
+ tenorBookAvailableAssetsRaw(marketId, network, 'bids'),
164
+ ]);
165
+ const ttmDays = midnightTimeToMaturityDays(maturity);
166
+ const bestPrice = tenorFillPrice(assetsRaw, quote.units);
167
+ const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
168
+ const capPrice = tenorCapPrice(bestPrice, slippagePercent, -1, ttmDays, maxBorrowRate);
169
+ const maxRate = midnightApyFromPrice(capPrice, ttmDays);
170
+ const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
171
+ return {
172
+ bestPrice,
173
+ worstPrice: capPrice,
174
+ estBorrowRate,
175
+ maxRate,
176
+ newUnits: quote.units,
177
+ maxUnits,
178
+ availableAssets,
179
+ availableUnits: TENOR_NO_AVAILABLE_UNITS,
180
+ takeableOffers: quote.offerFills,
181
+ };
182
+ });
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) {
184
+ if (!isTenorMidnightMarket(marketId)) {
185
+ throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
186
+ }
187
+ const [quote, availableAssets] = yield Promise.all([
188
+ fetchTenorQuote({
189
+ marketId, side: 'asks', assets: assetsRaw, taker, network,
190
+ }),
191
+ tenorBookAvailableAssetsRaw(marketId, network, 'asks'),
192
+ ]);
193
+ const ttmDays = midnightTimeToMaturityDays(maturity);
194
+ const bestPrice = tenorFillPrice(assetsRaw, quote.units);
195
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
196
+ const capPrice = tenorCapPrice(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
197
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
198
+ const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
199
+ return {
200
+ bestPrice,
201
+ worstPrice: capPrice,
202
+ estPaybackRate,
203
+ minRate,
204
+ newUnits: quote.units,
205
+ minUnits,
206
+ availableAssets,
207
+ availableUnits: TENOR_NO_AVAILABLE_UNITS,
208
+ takeableOffers: quote.offerFills,
209
+ };
210
+ });
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) {
212
+ if (!isTenorMidnightMarket(marketId)) {
213
+ throw new Error(tenorQuoteError(`Not a Tenor Midnight market: ${marketId}`));
214
+ }
215
+ const [quote, availableAssets] = yield Promise.all([
216
+ fetchTenorQuote({
217
+ marketId, side: 'asks', units: unitsRaw, taker, network,
218
+ }),
219
+ tenorBookAvailableAssetsRaw(marketId, network, 'asks'),
220
+ ]);
221
+ const ttmDays = midnightTimeToMaturityDays(maturity);
222
+ const newAssets = quote.buyerAssets;
223
+ const bestPrice = tenorFillPrice(newAssets, unitsRaw);
224
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
225
+ const capPrice = tenorCapPrice(bestPrice, slippagePercent, 1, ttmDays, minPaybackRate);
226
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
227
+ const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
228
+ return {
229
+ bestPrice,
230
+ worstPrice: capPrice,
231
+ estPaybackRate,
232
+ minRate,
233
+ newAssets,
234
+ maxAssets,
235
+ availableAssets,
236
+ availableUnits: TENOR_NO_AVAILABLE_UNITS,
237
+ takeableOffers: quote.offerFills,
238
+ };
239
+ });
@@ -3,7 +3,7 @@ export { CompoundMarkets, compoundV2CollateralAssets, v3ETHCollAssets, v3USDbCCo
3
3
  export { SparkMarkets } from './spark';
4
4
  export { CrvUsdMarkets } from './curveUsd';
5
5
  export { MorphoBlueMarkets, findMorphoBlueMarket } from './morphoBlue';
6
- export { MorphoMidnightMarkets, findMorphoMidnightMarket } from './morphoMidnight';
6
+ export { MorphoMidnightMarkets, findMorphoMidnightMarket, isTenorMidnightMarket } from './morphoMidnight';
7
7
  export { LlamaLendMarkets } from './llamaLend';
8
8
  export { LiquityV2Markets, findLiquityV2MarketByAddress } from './liquityV2';
9
9
  export { EulerV2Markets } from './euler';
@@ -3,7 +3,7 @@ export { CompoundMarkets, compoundV2CollateralAssets, v3ETHCollAssets, v3USDbCCo
3
3
  export { SparkMarkets } from './spark';
4
4
  export { CrvUsdMarkets } from './curveUsd';
5
5
  export { MorphoBlueMarkets, findMorphoBlueMarket } from './morphoBlue';
6
- export { MorphoMidnightMarkets, findMorphoMidnightMarket } from './morphoMidnight';
6
+ export { MorphoMidnightMarkets, findMorphoMidnightMarket, isTenorMidnightMarket } from './morphoMidnight';
7
7
  export { LlamaLendMarkets } from './llamaLend';
8
8
  export { LiquityV2Markets, findLiquityV2MarketByAddress } from './liquityV2';
9
9
  export { EulerV2Markets } from './euler';