@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.
- package/cjs/constants/index.d.ts +1 -0
- package/cjs/constants/index.js +2 -1
- package/cjs/helpers/morphoMidnightHelpers/index.d.ts +8 -14
- package/cjs/helpers/morphoMidnightHelpers/index.js +50 -62
- 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 +3 -1
- package/cjs/markets/morphoMidnight/index.d.ts +45 -1
- package/cjs/markets/morphoMidnight/index.js +542 -42
- package/cjs/types/morphoMidnight.d.ts +22 -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 +8 -14
- package/esm/helpers/morphoMidnightHelpers/index.js +27 -45
- 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 +45 -1
- package/esm/markets/morphoMidnight/index.js +507 -24
- package/esm/types/morphoMidnight.d.ts +22 -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 +49 -45
- package/src/helpers/morphoMidnightHelpers/rate.ts +46 -0
- package/src/helpers/morphoMidnightHelpers/tenor.ts +411 -0
- package/src/markets/index.ts +3 -1
- package/src/markets/morphoMidnight/index.ts +538 -24
- 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
package/src/constants/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/src/markets/index.ts
CHANGED
|
@@ -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 {
|
|
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';
|