@defisaver/positions-sdk 2.1.144-dev → 2.1.145
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/config/contracts.d.ts +249 -0
- package/cjs/config/contracts.js +11 -1
- package/cjs/constants/index.d.ts +1 -0
- package/cjs/constants/index.js +2 -1
- package/cjs/contracts.d.ts +1510 -0
- package/cjs/contracts.js +3 -2
- package/cjs/fluid/index.js +6 -2
- package/cjs/helpers/fluidHelpers/index.js +5 -4
- package/cjs/helpers/index.d.ts +1 -0
- package/cjs/helpers/index.js +2 -1
- package/cjs/helpers/morphoMidnightHelpers/headroom.d.ts +48 -0
- package/cjs/helpers/morphoMidnightHelpers/headroom.js +52 -0
- package/cjs/helpers/morphoMidnightHelpers/index.d.ts +113 -0
- package/cjs/helpers/morphoMidnightHelpers/index.js +298 -0
- package/cjs/helpers/morphoMidnightHelpers/rate.d.ts +15 -0
- package/cjs/helpers/morphoMidnightHelpers/rate.js +62 -0
- package/cjs/helpers/morphoMidnightHelpers/tenor.d.ts +129 -0
- package/cjs/helpers/morphoMidnightHelpers/tenor.js +254 -0
- package/cjs/index.d.ts +2 -1
- package/cjs/index.js +3 -1
- package/cjs/markets/index.d.ts +1 -0
- package/cjs/markets/index.js +6 -1
- package/cjs/markets/morphoMidnight/index.d.ts +60 -0
- package/cjs/markets/morphoMidnight/index.js +659 -0
- package/cjs/morphoMidnight/index.d.ts +14 -0
- package/cjs/morphoMidnight/index.js +261 -0
- package/cjs/portfolio/discovery.js +4 -0
- package/cjs/portfolio/index.js +44 -0
- package/cjs/services/viem.d.ts +11 -11
- package/cjs/staking/eligibility.d.ts +15 -1
- package/cjs/staking/eligibility.js +12 -1
- package/cjs/staking/staking.js +18 -22
- package/cjs/types/index.d.ts +1 -0
- package/cjs/types/index.js +1 -0
- package/cjs/types/morphoMidnight.d.ts +141 -0
- package/cjs/types/morphoMidnight.js +47 -0
- package/cjs/types/portfolio.d.ts +4 -0
- package/esm/config/contracts.d.ts +249 -0
- package/esm/config/contracts.js +9 -0
- package/esm/constants/index.d.ts +1 -0
- package/esm/constants/index.js +1 -0
- package/esm/contracts.d.ts +1510 -0
- package/esm/contracts.js +1 -0
- package/esm/fluid/index.js +6 -2
- package/esm/helpers/fluidHelpers/index.js +6 -5
- package/esm/helpers/index.d.ts +1 -0
- package/esm/helpers/index.js +1 -0
- package/esm/helpers/morphoMidnightHelpers/headroom.d.ts +48 -0
- package/esm/helpers/morphoMidnightHelpers/headroom.js +45 -0
- package/esm/helpers/morphoMidnightHelpers/index.d.ts +113 -0
- package/esm/helpers/morphoMidnightHelpers/index.js +271 -0
- package/esm/helpers/morphoMidnightHelpers/rate.d.ts +15 -0
- package/esm/helpers/morphoMidnightHelpers/rate.js +50 -0
- package/esm/helpers/morphoMidnightHelpers/tenor.d.ts +129 -0
- package/esm/helpers/morphoMidnightHelpers/tenor.js +238 -0
- package/esm/index.d.ts +2 -1
- package/esm/index.js +2 -1
- package/esm/markets/index.d.ts +1 -0
- package/esm/markets/index.js +1 -0
- package/esm/markets/morphoMidnight/index.d.ts +60 -0
- package/esm/markets/morphoMidnight/index.js +631 -0
- package/esm/morphoMidnight/index.d.ts +14 -0
- package/esm/morphoMidnight/index.js +248 -0
- package/esm/portfolio/discovery.js +5 -1
- package/esm/portfolio/index.js +45 -1
- package/esm/services/viem.d.ts +11 -11
- package/esm/staking/eligibility.d.ts +15 -1
- package/esm/staking/eligibility.js +10 -0
- package/esm/staking/staking.js +19 -23
- package/esm/types/index.d.ts +1 -0
- package/esm/types/index.js +1 -0
- package/esm/types/morphoMidnight.d.ts +141 -0
- package/esm/types/morphoMidnight.js +44 -0
- package/esm/types/portfolio.d.ts +4 -0
- package/package.json +1 -1
- package/src/config/contracts.ts +9 -0
- package/src/constants/index.ts +1 -0
- package/src/contracts.ts +1 -0
- package/src/fluid/index.ts +6 -2
- package/src/helpers/fluidHelpers/index.ts +6 -5
- package/src/helpers/index.ts +1 -0
- package/src/helpers/morphoMidnightHelpers/headroom.ts +82 -0
- package/src/helpers/morphoMidnightHelpers/index.ts +485 -0
- package/src/helpers/morphoMidnightHelpers/rate.ts +71 -0
- package/src/helpers/morphoMidnightHelpers/tenor.ts +407 -0
- package/src/index.ts +2 -0
- package/src/markets/index.ts +3 -0
- package/src/markets/morphoMidnight/index.ts +675 -0
- package/src/morphoMidnight/index.ts +276 -0
- package/src/portfolio/discovery.ts +6 -0
- package/src/portfolio/index.ts +44 -0
- package/src/staking/eligibility.ts +29 -2
- package/src/staking/staking.ts +20 -22
- package/src/types/index.ts +1 -0
- package/src/types/morphoMidnight.ts +163 -0
- package/src/types/portfolio.ts +4 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import Dec from 'decimal.js';
|
|
2
|
+
import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
|
|
3
|
+
import {
|
|
4
|
+
calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos,
|
|
5
|
+
} from '../../moneymarket';
|
|
6
|
+
import { calculateNetApy } from '../../staking';
|
|
7
|
+
import {
|
|
8
|
+
LeverageType, MMAssetsData, MMUsedAsset, MMUsedAssets, NetworkNumber,
|
|
9
|
+
} from '../../types/common';
|
|
10
|
+
import {
|
|
11
|
+
MorphoMidnightAggregatedPositionData,
|
|
12
|
+
MorphoMidnightAssetsData,
|
|
13
|
+
MorphoMidnightBookOffer,
|
|
14
|
+
MorphoMidnightBookSide,
|
|
15
|
+
MorphoMidnightMarketData,
|
|
16
|
+
MorphoMidnightMarketInfo,
|
|
17
|
+
MorphoMidnightParsedBook,
|
|
18
|
+
} from '../../types';
|
|
19
|
+
import { WAD } from '../../constants';
|
|
20
|
+
import { LONGER_TIMEOUT } from '../../services/utils';
|
|
21
|
+
import { isTenorMidnightMarket } from '../../markets/morphoMidnight';
|
|
22
|
+
import {
|
|
23
|
+
buildMidnightParsedBook, midnightApyFromPrice, midnightBoundPrice, midnightTimeToMaturityDays,
|
|
24
|
+
} from './rate';
|
|
25
|
+
import {
|
|
26
|
+
getTenorBorrowQuote, getTenorMarketBook, getTenorPaybackQuote, getTenorPaybackUnitsQuote,
|
|
27
|
+
} from './tenor';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
buildMidnightParsedBook,
|
|
31
|
+
midnightApyFromPrice,
|
|
32
|
+
midnightBookBestFirst,
|
|
33
|
+
midnightBoundPrice,
|
|
34
|
+
midnightPriceFromApy,
|
|
35
|
+
midnightTimeToMaturityDays,
|
|
36
|
+
MIDNIGHT_DEFAULT_RATE_SLIPPAGE,
|
|
37
|
+
} from './rate';
|
|
38
|
+
export {
|
|
39
|
+
getMorphoMidnightBorrowHeadroom,
|
|
40
|
+
MIDNIGHT_MIN_HEALTH_RATIO,
|
|
41
|
+
} from './headroom';
|
|
42
|
+
export type { MidnightBorrowHeadroom, MidnightBorrowHeadroomParams } from './headroom';
|
|
43
|
+
export {
|
|
44
|
+
tenorBookKeyFor,
|
|
45
|
+
tenorBookRateToApyPercent,
|
|
46
|
+
tenorOfferFillToApiFill,
|
|
47
|
+
tenorOfferToApiOffer,
|
|
48
|
+
} from './tenor';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
|
|
52
|
+
* the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
|
|
53
|
+
*
|
|
54
|
+
* Note on amounts: `borrowedUsd` is derived from the position's `debt`, which is the face value owed at
|
|
55
|
+
* maturity (principal + fixed interest). Health is therefore measured against the full maturity debt,
|
|
56
|
+
* matching how MidnightView computes `ratio`. Fixed-rate APY is not derived on-chain in MVP, so
|
|
57
|
+
* `netApy` reflects the `'0'` rates in `assetsData` (see the module getter).
|
|
58
|
+
*/
|
|
59
|
+
export const getMorphoMidnightAggregatedPositionData = ({
|
|
60
|
+
usedAssets,
|
|
61
|
+
assetsData,
|
|
62
|
+
marketInfo,
|
|
63
|
+
}: {
|
|
64
|
+
usedAssets: MMUsedAssets,
|
|
65
|
+
assetsData: MorphoMidnightAssetsData,
|
|
66
|
+
marketInfo: MorphoMidnightMarketInfo,
|
|
67
|
+
}): MorphoMidnightAggregatedPositionData => {
|
|
68
|
+
const payload = {} as MorphoMidnightAggregatedPositionData;
|
|
69
|
+
|
|
70
|
+
payload.suppliedUsd = getAssetsTotal(usedAssets, ({ isSupplied }: { isSupplied: boolean }) => isSupplied, ({ suppliedUsd }: { suppliedUsd: string }) => suppliedUsd);
|
|
71
|
+
payload.suppliedCollateralUsd = getAssetsTotal(usedAssets, ({ isSupplied, collateral }: { isSupplied: boolean, collateral: boolean }) => isSupplied && collateral, ({ suppliedUsd }: { suppliedUsd: string }) => suppliedUsd);
|
|
72
|
+
payload.borrowedUsd = getAssetsTotal(usedAssets, ({ isBorrowed }: { isBorrowed: boolean }) => isBorrowed, ({ borrowedUsd }: { borrowedUsd: string }) => borrowedUsd);
|
|
73
|
+
|
|
74
|
+
// borrowLimit = Σ collateralUsd_i * lltv_i (per-collateral lltv carried on assetsData)
|
|
75
|
+
payload.borrowLimitUsd = getAssetsTotal(
|
|
76
|
+
usedAssets,
|
|
77
|
+
({ isSupplied, collateral }: { isSupplied: boolean, collateral: boolean }) => isSupplied && collateral,
|
|
78
|
+
({ symbol, suppliedUsd }: { symbol: string, suppliedUsd: string }) => new Dec(suppliedUsd).mul(assetsData[symbol]?.lltv || 0),
|
|
79
|
+
);
|
|
80
|
+
payload.liquidationLimitUsd = payload.borrowLimitUsd;
|
|
81
|
+
|
|
82
|
+
// Same subtraction every other money market uses, but it does NOT mean the same thing here. Elsewhere
|
|
83
|
+
// `borrowedUsd` is debt at present value, so the remainder is what a borrow would pay out. Midnight
|
|
84
|
+
// records debt at its face value at maturity, so this is face-value headroom: borrowing it would add
|
|
85
|
+
// more debt than the number says, by the market's discount. It also reserves no safety margin, and this
|
|
86
|
+
// aggregate is synchronous so it has no book rate to discount by.
|
|
87
|
+
//
|
|
88
|
+
// DO NOT SURFACE THIS AS "what you can borrow" — use `getMorphoMidnightBorrowHeadroom` instead, which
|
|
89
|
+
// applies both corrections (the margin, and the loan-per-unit price off a book rate) and caps on the
|
|
90
|
+
// depth the book can fill. The discount alone grows past 7% on a one-year market.
|
|
91
|
+
const leftToBorrowUsd = new Dec(payload.borrowLimitUsd).sub(payload.borrowedUsd);
|
|
92
|
+
payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
|
|
93
|
+
|
|
94
|
+
const loanTokenPrice = assetsData[marketInfo.loanToken]?.price || '0';
|
|
95
|
+
payload.leftToBorrow = new Dec(loanTokenPrice).eq(0) ? '0' : new Dec(payload.leftToBorrowUsd).div(loanTokenPrice).toString();
|
|
96
|
+
|
|
97
|
+
const { netApy, incentiveUsd, totalInterestUsd } = calculateNetApy({ usedAssets, assetsData: assetsData as unknown as MMAssetsData });
|
|
98
|
+
payload.netApy = netApy;
|
|
99
|
+
payload.incentiveUsd = incentiveUsd;
|
|
100
|
+
payload.totalInterestUsd = totalInterestUsd;
|
|
101
|
+
|
|
102
|
+
payload.ltv = new Dec(payload.suppliedCollateralUsd).eq(0) ? '0' : new Dec(payload.borrowedUsd).div(payload.suppliedCollateralUsd).toString();
|
|
103
|
+
payload.ratio = new Dec(payload.borrowedUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.borrowedUsd).mul(100).toString();
|
|
104
|
+
payload.healthRatio = new Dec(payload.borrowedUsd).eq(0) ? 'Infinity' : new Dec(payload.liquidationLimitUsd).div(payload.borrowedUsd).toDP(4).toString();
|
|
105
|
+
|
|
106
|
+
const { leveragedType, leveragedAsset } = isLeveragedPos(usedAssets);
|
|
107
|
+
payload.leveragedType = leveragedType;
|
|
108
|
+
payload.liquidationPrice = '';
|
|
109
|
+
if (leveragedType !== '') {
|
|
110
|
+
payload.leveragedAsset = leveragedAsset;
|
|
111
|
+
let assetPrice = assetsData[leveragedAsset].price;
|
|
112
|
+
if (leveragedType === LeverageType.VolatilePair) {
|
|
113
|
+
const borrowedAsset = (Object.values(usedAssets) as MMUsedAsset[]).find(({ borrowedUsd }: { borrowedUsd: string }) => +borrowedUsd > 0);
|
|
114
|
+
const borrowedAssetPrice = assetsData[borrowedAsset!.symbol].price;
|
|
115
|
+
const leveragedAssetPrice = assetsData[leveragedAsset].price;
|
|
116
|
+
const isReverse = new Dec(leveragedAssetPrice).lt(borrowedAssetPrice);
|
|
117
|
+
if (isReverse) {
|
|
118
|
+
payload.leveragedType = LeverageType.VolatilePairReverse;
|
|
119
|
+
payload.currentVolatilePairRatio = new Dec(borrowedAssetPrice).div(leveragedAssetPrice).toDP(18).toString();
|
|
120
|
+
assetPrice = new Dec(borrowedAssetPrice).div(assetPrice).toString();
|
|
121
|
+
} else {
|
|
122
|
+
assetPrice = new Dec(assetPrice).div(borrowedAssetPrice).toString();
|
|
123
|
+
payload.currentVolatilePairRatio = new Dec(leveragedAssetPrice).div(borrowedAssetPrice).toDP(18).toString();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
payload.liquidationPrice = calcLeverageLiqPrice(payload.leveragedType, assetPrice, payload.borrowedUsd, payload.liquidationLimitUsd);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
payload.minCollRatio = new Dec(payload.borrowLimitUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.borrowLimitUsd).mul(100).toString();
|
|
130
|
+
payload.collLiquidationRatio = new Dec(payload.liquidationLimitUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.liquidationLimitUsd).mul(100).toString();
|
|
131
|
+
payload.exposure = getExposure(payload.borrowedUsd, payload.suppliedUsd);
|
|
132
|
+
|
|
133
|
+
return payload;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ── Off-chain order-book rate helpers ──────────────────────────────────────────────────────────────
|
|
137
|
+
// notion: https://app.notion.com/p/defisaver/Estimate-borrow-rate-and-slippage-before-execution-3a70be682adc80c783c8c11fdb761dd2
|
|
138
|
+
// the borrow rate is not exposed on-chain (MidnightView only stores total debt at maturity in `units`).
|
|
139
|
+
// derive the rate + interest from Morpho's public keyless Midnight API
|
|
140
|
+
// Quote prices are WAD-scaled
|
|
141
|
+
// loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
|
|
142
|
+
|
|
143
|
+
const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
|
|
144
|
+
|
|
145
|
+
// The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
|
|
146
|
+
// enough that markets drop out of the list for no reason.
|
|
147
|
+
const MIDNIGHT_BOOK_TIMEOUT = 30000;
|
|
148
|
+
|
|
149
|
+
// The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
|
|
150
|
+
// place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
|
|
151
|
+
const MIDNIGHT_SLIPPAGE_MIN = 0.1;
|
|
152
|
+
const MIDNIGHT_SLIPPAGE_MAX = 100;
|
|
153
|
+
|
|
154
|
+
interface MidnightPosition {
|
|
155
|
+
market_id: string,
|
|
156
|
+
type: string, // 'borrow' | 'lend'
|
|
157
|
+
debt: string, // raw loan-token base units — matches MidnightView.getPositionInfo exactly
|
|
158
|
+
cost_basis: string, // WAD-scaled raw base units — outstanding principal, net of exits/liquidations
|
|
159
|
+
effective_rate_wad: string, // WAD-scaled borrow APY, e.g. 0.05e18 = 5%
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface MidnightApiError {
|
|
163
|
+
code?: string,
|
|
164
|
+
message?: string,
|
|
165
|
+
details?: ({ field?: string, issue?: string })[] | null,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface MidnightRawOffer {
|
|
169
|
+
price: string, // WAD-scaled loan-per-unit
|
|
170
|
+
assets: string, // loan-token base units available at this offer
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface MidnightQuoteResponse {
|
|
174
|
+
average_best_price?: string,
|
|
175
|
+
average_worst_price?: string,
|
|
176
|
+
available_assets?: string,
|
|
177
|
+
available_units?: string,
|
|
178
|
+
takeable_offers?: unknown[],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface MorphoMidnightBorrowInfo {
|
|
182
|
+
borrowRate: string, // effective borrow APY as a percent
|
|
183
|
+
debtBase: string, // outstanding principal (cost_basis), loan-token units
|
|
184
|
+
debtInterest: string, // debtTotal − debtBase (interest owed at maturity), loan-token units
|
|
185
|
+
debtTotal: string, // on-chain debt at maturity, loan-token units
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface MorphoMidnightBorrowQuote {
|
|
189
|
+
bestPrice: string, // average_best_price, loan-per-unit
|
|
190
|
+
worstPrice: string, // average_worst_price, slippage-adjusted
|
|
191
|
+
estBorrowRate: string, // estimated borrow APY as a percent
|
|
192
|
+
maxRate: string, // borrow APY the on-chain cap permits, i.e. `maxUnits` annualized (display only)
|
|
193
|
+
newUnits: string, // debt added at best price, raw loan-token base units
|
|
194
|
+
maxUnits: string, // capped debt (on-chain cap), raw loan-token base units
|
|
195
|
+
availableAssets: string,
|
|
196
|
+
availableUnits: string,
|
|
197
|
+
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface MorphoMidnightPaybackQuote {
|
|
201
|
+
bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
|
|
202
|
+
worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
|
|
203
|
+
estPaybackRate: string, // APY the repayment retires debt at, as a percent
|
|
204
|
+
minRate: string, // APY the on-chain floor permits, i.e. `minUnits` annualized (display only)
|
|
205
|
+
newUnits: string, // debt retired at best price, raw loan-token base units
|
|
206
|
+
minUnits: string, // floor on debt retired (on-chain guard), raw loan-token base units
|
|
207
|
+
availableAssets: string,
|
|
208
|
+
availableUnits: string,
|
|
209
|
+
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Payback quoted the other way round: the caller names the debt units to retire, and the quote prices
|
|
213
|
+
// what buying them costs. Same rate guard as the assets-target quote, expressed as a spend ceiling.
|
|
214
|
+
export interface MorphoMidnightPaybackUnitsQuote {
|
|
215
|
+
bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
|
|
216
|
+
worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
|
|
217
|
+
estPaybackRate: string, // APY the repayment retires debt at, as a percent
|
|
218
|
+
minRate: string, // APY the on-chain ceiling permits, i.e. `maxAssets` annualized (display only)
|
|
219
|
+
newAssets: string, // assets the target units cost at best price, raw loan-token base units
|
|
220
|
+
maxAssets: string, // ceiling on assets spent (on-chain guard), raw loan-token base units
|
|
221
|
+
availableAssets: string,
|
|
222
|
+
availableUnits: string,
|
|
223
|
+
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Coerce a slippage into what the quote endpoint accepts: 0.1–100 with at most one decimal place. The
|
|
228
|
+
* validation is lexical, so a computed value (`4.15066671050631467`) is rejected outright — without this
|
|
229
|
+
* the request 400s and the quote looks unavailable.
|
|
230
|
+
*
|
|
231
|
+
* Rounded **down**, since a wider slippage is a looser cap than the caller asked for.
|
|
232
|
+
*/
|
|
233
|
+
export const midnightSlippageParam = (slippagePercent: Dec.Value): string => Dec.min(
|
|
234
|
+
Dec.max(new Dec(slippagePercent), MIDNIGHT_SLIPPAGE_MIN),
|
|
235
|
+
MIDNIGHT_SLIPPAGE_MAX,
|
|
236
|
+
).toDP(1, Dec.ROUND_DOWN).toString();
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
240
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
241
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
242
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
243
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
244
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
245
|
+
* The caller swallows errors — a missing rate must never block position rendering.
|
|
246
|
+
*/
|
|
247
|
+
export const getMorphoMidnightUserBorrowInfo = async (
|
|
248
|
+
account: string,
|
|
249
|
+
marketId: string,
|
|
250
|
+
loanTokenSymbol: string,
|
|
251
|
+
): Promise<MorphoMidnightBorrowInfo> => {
|
|
252
|
+
const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
253
|
+
const json: { data?: MidnightPosition[] } = await res.json();
|
|
254
|
+
const position = (json?.data || []).find((p) => p.type === 'borrow' && p.market_id?.toLowerCase() === marketId.toLowerCase());
|
|
255
|
+
|
|
256
|
+
const debtTotal = assetAmountInEth(position?.debt || '0', loanTokenSymbol);
|
|
257
|
+
const costBasisRaw = new Dec(position?.cost_basis || 0).div(WAD); // WAD-scaled → raw base units
|
|
258
|
+
const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
|
|
259
|
+
const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
|
|
260
|
+
const borrowRate = new Dec(position?.effective_rate_wad || 0).div(WAD).mul(100).toString();
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
borrowRate, debtBase, debtInterest, debtTotal,
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
|
|
269
|
+
* interest off the indexer, this carries that same split forward through a payback, from a debt of
|
|
270
|
+
* `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
|
|
271
|
+
* so both scale by the same fraction.
|
|
272
|
+
*
|
|
273
|
+
* Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
|
|
274
|
+
* `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
|
|
275
|
+
* in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
|
|
276
|
+
* payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
|
|
277
|
+
* payback it goes negative on a position that still owes. Callers detect that window as
|
|
278
|
+
* `debtBase + debtInterest !== debt`.
|
|
279
|
+
*
|
|
280
|
+
* Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
|
|
281
|
+
* it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
|
|
282
|
+
* rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
|
|
283
|
+
*/
|
|
284
|
+
export const scaleMorphoMidnightDebtSplit = (
|
|
285
|
+
{ debtBase, debtInterest }: Pick<MorphoMidnightBorrowInfo, 'debtBase' | 'debtInterest'>,
|
|
286
|
+
borrowedBefore: Dec.Value,
|
|
287
|
+
borrowedAfter: Dec.Value,
|
|
288
|
+
): Pick<MorphoMidnightBorrowInfo, 'debtBase' | 'debtInterest'> => {
|
|
289
|
+
const fractionRemaining = new Dec(borrowedBefore).lte(0)
|
|
290
|
+
? new Dec(0)
|
|
291
|
+
: Dec.max(0, new Dec(borrowedAfter)).div(borrowedBefore);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
debtBase: new Dec(debtBase).mul(fractionRemaining).toString(),
|
|
295
|
+
debtInterest: new Dec(debtInterest).mul(fractionRemaining).toString(),
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
301
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
302
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
303
|
+
*
|
|
304
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
305
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
306
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
307
|
+
*
|
|
308
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
309
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
310
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
311
|
+
*/
|
|
312
|
+
export const getMorphoMidnightMarketBook = async (
|
|
313
|
+
market: MorphoMidnightMarketData,
|
|
314
|
+
network: NetworkNumber,
|
|
315
|
+
side: MorphoMidnightBookSide = 'bids',
|
|
316
|
+
): Promise<MorphoMidnightParsedBook | null> => {
|
|
317
|
+
if (isTenorMidnightMarket(market)) {
|
|
318
|
+
return getTenorMarketBook(market, network, side);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
|
|
322
|
+
const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
|
|
323
|
+
if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
|
|
324
|
+
|
|
325
|
+
const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
|
|
326
|
+
const ttmDays = midnightTimeToMaturityDays(market.maturity);
|
|
327
|
+
|
|
328
|
+
const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || []).map((offer) => ({
|
|
329
|
+
rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
|
|
330
|
+
liquidity: assetAmountInEth(offer.assets, loanSymbol),
|
|
331
|
+
}));
|
|
332
|
+
|
|
333
|
+
return buildMidnightParsedBook(offers, side);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
|
|
337
|
+
// (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
|
|
338
|
+
// Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
|
|
339
|
+
const midnightQuoteError = (error?: MidnightApiError): string => {
|
|
340
|
+
const detail = (error?.details || []).map(({ issue }) => issue).filter(Boolean).join('; ');
|
|
341
|
+
const reason = detail || error?.message || error?.code;
|
|
342
|
+
return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
interface MidnightParsedQuote {
|
|
346
|
+
bestPrice: string, // loan-per-unit
|
|
347
|
+
worstPrice: string, // slippage-adjusted; below best on `bids`, above it on `asks`
|
|
348
|
+
availableAssets: string,
|
|
349
|
+
availableUnits: string,
|
|
350
|
+
takeableOffers: any[],
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* How much of the book to quote. The endpoint takes exactly one of the two — it rejects a request with
|
|
355
|
+
* neither ("Either assets or units must be provided") — and answers the same `takeable_offers` list either
|
|
356
|
+
* way, since that list is the whole in-band depth rather than the slice this size consumes.
|
|
357
|
+
*/
|
|
358
|
+
type MidnightQuoteSize = { assets: string } | { units: string };
|
|
359
|
+
|
|
360
|
+
// The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
|
|
361
|
+
const fetchMorphoMidnightQuote = async (
|
|
362
|
+
marketId: string,
|
|
363
|
+
side: MorphoMidnightBookSide,
|
|
364
|
+
size: MidnightQuoteSize,
|
|
365
|
+
slippagePercent: Dec.Value,
|
|
366
|
+
): Promise<MidnightParsedQuote> => {
|
|
367
|
+
const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
|
|
368
|
+
const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${midnightSlippageParam(slippagePercent)}`;
|
|
369
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
370
|
+
const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
|
|
371
|
+
const d = json?.data;
|
|
372
|
+
if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
|
|
376
|
+
worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
|
|
377
|
+
availableAssets: d.available_assets || '0',
|
|
378
|
+
availableUnits: d.available_units || '0',
|
|
379
|
+
takeableOffers: d.takeable_offers || [],
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export const getMorphoMidnightBorrowQuote = async (
|
|
384
|
+
marketId: string,
|
|
385
|
+
assetsRaw: string,
|
|
386
|
+
slippagePercent: Dec.Value,
|
|
387
|
+
maturity: number,
|
|
388
|
+
maxBorrowRate?: Dec.Value,
|
|
389
|
+
taker?: string,
|
|
390
|
+
rateSlippagePercent?: Dec.Value,
|
|
391
|
+
): Promise<MorphoMidnightBorrowQuote> => {
|
|
392
|
+
if (isTenorMidnightMarket(marketId)) {
|
|
393
|
+
return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
|
|
397
|
+
const { bestPrice } = quote;
|
|
398
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
399
|
+
const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
400
|
+
|
|
401
|
+
// Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
|
|
402
|
+
// `maxUnits` can never disagree about what the user is protected at.
|
|
403
|
+
const capPrice = midnightBoundPrice(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
|
|
404
|
+
const maxRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
405
|
+
const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
|
|
406
|
+
const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
...quote,
|
|
410
|
+
estBorrowRate,
|
|
411
|
+
maxRate,
|
|
412
|
+
newUnits,
|
|
413
|
+
maxUnits,
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
export const getMorphoMidnightPaybackQuote = async (
|
|
418
|
+
marketId: string,
|
|
419
|
+
assetsRaw: string,
|
|
420
|
+
slippagePercent: Dec.Value,
|
|
421
|
+
maturity: number,
|
|
422
|
+
minPaybackRate?: Dec.Value,
|
|
423
|
+
taker?: string,
|
|
424
|
+
rateSlippagePercent?: Dec.Value,
|
|
425
|
+
): Promise<MorphoMidnightPaybackQuote> => {
|
|
426
|
+
if (isTenorMidnightMarket(marketId)) {
|
|
427
|
+
return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
|
|
431
|
+
const { bestPrice } = quote;
|
|
432
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
433
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
434
|
+
|
|
435
|
+
const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
|
|
436
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
437
|
+
// Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
|
|
438
|
+
// `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
|
|
439
|
+
const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
440
|
+
const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
...quote,
|
|
444
|
+
estPaybackRate,
|
|
445
|
+
minRate,
|
|
446
|
+
newUnits,
|
|
447
|
+
minUnits,
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
export const getMorphoMidnightPaybackUnitsQuote = async (
|
|
453
|
+
marketId: string,
|
|
454
|
+
unitsRaw: string,
|
|
455
|
+
slippagePercent: Dec.Value,
|
|
456
|
+
maturity: number,
|
|
457
|
+
minPaybackRate?: Dec.Value,
|
|
458
|
+
taker?: string,
|
|
459
|
+
rateSlippagePercent?: Dec.Value,
|
|
460
|
+
): Promise<MorphoMidnightPaybackUnitsQuote> => {
|
|
461
|
+
if (isTenorMidnightMarket(marketId)) {
|
|
462
|
+
return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
|
|
466
|
+
const { bestPrice } = quote;
|
|
467
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
468
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
469
|
+
|
|
470
|
+
const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
|
|
471
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
472
|
+
// Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
|
|
473
|
+
// what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
|
|
474
|
+
// buy short of the units it was asked for.
|
|
475
|
+
const newAssets = new Dec(unitsRaw).mul(bestPrice).toFixed(0, Dec.ROUND_UP);
|
|
476
|
+
const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
...quote,
|
|
480
|
+
estPaybackRate,
|
|
481
|
+
minRate,
|
|
482
|
+
newAssets,
|
|
483
|
+
maxAssets,
|
|
484
|
+
};
|
|
485
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
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 MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
|
|
33
|
+
|
|
34
|
+
const MIDNIGHT_MIN_FLOOR_RATIO = 0.5;
|
|
35
|
+
|
|
36
|
+
/** Which side of the estimate a guard sits on: a ceiling for borrows, a floor for paybacks. */
|
|
37
|
+
export type MidnightRateBoundKind = 'ceiling' | 'floor';
|
|
38
|
+
|
|
39
|
+
export const midnightBoundPrice = (
|
|
40
|
+
estRate: Dec.Value,
|
|
41
|
+
ttmDays: Dec.Value,
|
|
42
|
+
kind: MidnightRateBoundKind,
|
|
43
|
+
boundRate?: Dec.Value,
|
|
44
|
+
rateSlippagePercent: Dec.Value = MIDNIGHT_DEFAULT_RATE_SLIPPAGE,
|
|
45
|
+
): string => {
|
|
46
|
+
if (boundRate !== undefined && new Dec(boundRate).gt(0)) return midnightPriceFromApy(boundRate, ttmDays);
|
|
47
|
+
|
|
48
|
+
const est = new Dec(estRate);
|
|
49
|
+
const slippage = new Dec(rateSlippagePercent);
|
|
50
|
+
const bound = kind === 'ceiling'
|
|
51
|
+
? est.add(slippage)
|
|
52
|
+
: Dec.max(est.sub(slippage), est.mul(MIDNIGHT_MIN_FLOOR_RATIO));
|
|
53
|
+
|
|
54
|
+
return midnightPriceFromApy(bound, ttmDays);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const midnightBookBestFirst = (side: MorphoMidnightBookSide): 1 | -1 => (side === 'asks' ? -1 : 1);
|
|
58
|
+
|
|
59
|
+
export const buildMidnightParsedBook = (
|
|
60
|
+
offers: MorphoMidnightBookOffer[],
|
|
61
|
+
side: MorphoMidnightBookSide,
|
|
62
|
+
): MorphoMidnightParsedBook | null => {
|
|
63
|
+
const bestFirst = [...offers].sort((a, b) => new Dec(a.rate).minus(b.rate).mul(midnightBookBestFirst(side)).toNumber());
|
|
64
|
+
if (bestFirst.length === 0) return null;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
bestRate: bestFirst[0].rate,
|
|
68
|
+
totalLiquidity: bestFirst.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
|
|
69
|
+
offers: bestFirst,
|
|
70
|
+
};
|
|
71
|
+
};
|