@defisaver/positions-sdk 2.1.127-midnight-2-dev → 2.1.127-midnight-4-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/helpers/morphoMidnightHelpers/index.d.ts +53 -6
- package/cjs/helpers/morphoMidnightHelpers/index.js +111 -42
- package/cjs/morphoMidnight/index.js +1 -1
- package/cjs/types/morphoMidnight.d.ts +10 -0
- package/esm/helpers/morphoMidnightHelpers/index.d.ts +53 -6
- package/esm/helpers/morphoMidnightHelpers/index.js +109 -42
- package/esm/morphoMidnight/index.js +1 -1
- package/esm/types/morphoMidnight.d.ts +10 -0
- package/package.json +1 -1
- package/src/helpers/morphoMidnightHelpers/index.ts +177 -49
- package/src/morphoMidnight/index.ts +1 -1
- package/src/types/morphoMidnight.ts +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Dec from 'decimal.js';
|
|
2
|
-
import { MMUsedAssets } from '../../types/common';
|
|
3
|
-
import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketInfo } from '../../types';
|
|
2
|
+
import { MMUsedAssets, NetworkNumber } from '../../types/common';
|
|
3
|
+
import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightBookSide, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
|
|
4
4
|
/**
|
|
5
5
|
* Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
|
|
6
6
|
* the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
|
|
@@ -32,6 +32,17 @@ export interface MorphoMidnightBorrowQuote {
|
|
|
32
32
|
availableUnits: string;
|
|
33
33
|
takeableOffers: any[];
|
|
34
34
|
}
|
|
35
|
+
export interface MorphoMidnightPaybackQuote {
|
|
36
|
+
bestPrice: string;
|
|
37
|
+
worstPrice: string;
|
|
38
|
+
estPaybackRate: string;
|
|
39
|
+
minRate: string;
|
|
40
|
+
newUnits: string;
|
|
41
|
+
minUnits: string;
|
|
42
|
+
availableAssets: string;
|
|
43
|
+
availableUnits: string;
|
|
44
|
+
takeableOffers: any[];
|
|
45
|
+
}
|
|
35
46
|
export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
|
|
36
47
|
export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
|
|
37
48
|
/**
|
|
@@ -52,12 +63,29 @@ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec
|
|
|
52
63
|
*/
|
|
53
64
|
export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
|
|
54
65
|
/**
|
|
55
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
56
|
-
*
|
|
57
|
-
*
|
|
66
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
67
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
68
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
69
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
70
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
71
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
58
72
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
59
73
|
*/
|
|
60
|
-
export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string,
|
|
74
|
+
export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
|
|
75
|
+
/**
|
|
76
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
77
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
78
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
79
|
+
*
|
|
80
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
81
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
82
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
83
|
+
*
|
|
84
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
85
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
86
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
87
|
+
*/
|
|
88
|
+
export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
|
|
61
89
|
/**
|
|
62
90
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
63
91
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -76,3 +104,22 @@ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId
|
|
|
76
104
|
* Compare the two before submitting and tell the user their ceiling is under the market rate.
|
|
77
105
|
*/
|
|
78
106
|
export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
|
|
107
|
+
/**
|
|
108
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
109
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
110
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
111
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
112
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
113
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
114
|
+
*
|
|
115
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
116
|
+
* so the guard is a floor rather than a ceiling:
|
|
117
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
118
|
+
* this when a user pins a min rate.
|
|
119
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
120
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
121
|
+
*
|
|
122
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
123
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
124
|
+
*/
|
|
125
|
+
export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
|
|
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.getMorphoMidnightBorrowQuote = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.midnightPriceFromApy = exports.midnightApyFromPrice = exports.midnightTimeToMaturityDays = exports.getMorphoMidnightAggregatedPositionData = void 0;
|
|
15
|
+
exports.getMorphoMidnightPaybackQuote = exports.getMorphoMidnightBorrowQuote = exports.getMorphoMidnightMarketBook = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.midnightPriceFromApy = exports.midnightApyFromPrice = exports.midnightTimeToMaturityDays = exports.getMorphoMidnightAggregatedPositionData = void 0;
|
|
16
16
|
const decimal_js_1 = __importDefault(require("decimal.js"));
|
|
17
17
|
const tokens_1 = require("@defisaver/tokens");
|
|
18
18
|
const moneymarket_1 = require("../../moneymarket");
|
|
@@ -86,6 +86,9 @@ exports.getMorphoMidnightAggregatedPositionData = getMorphoMidnightAggregatedPos
|
|
|
86
86
|
// loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
|
|
87
87
|
const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
|
|
88
88
|
const nowInSeconds = () => Math.floor(Date.now() / 1000);
|
|
89
|
+
// The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
|
|
90
|
+
// enough that markets drop out of the list for no reason.
|
|
91
|
+
const MIDNIGHT_BOOK_TIMEOUT = 30000;
|
|
89
92
|
// The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
|
|
90
93
|
// place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
|
|
91
94
|
const MIDNIGHT_SLIPPAGE_MIN = 0.1;
|
|
@@ -131,39 +134,65 @@ exports.midnightPriceFromApy = midnightPriceFromApy;
|
|
|
131
134
|
const midnightSlippageParam = (slippagePercent) => decimal_js_1.default.min(decimal_js_1.default.max(new decimal_js_1.default(slippagePercent), MIDNIGHT_SLIPPAGE_MIN), MIDNIGHT_SLIPPAGE_MAX).toDP(1, decimal_js_1.default.ROUND_DOWN).toString();
|
|
132
135
|
exports.midnightSlippageParam = midnightSlippageParam;
|
|
133
136
|
/**
|
|
134
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
138
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
139
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
140
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
141
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
142
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
137
143
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
138
144
|
*/
|
|
139
|
-
const getMorphoMidnightUserBorrowInfo = (account, marketId,
|
|
140
|
-
const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/
|
|
145
|
+
const getMorphoMidnightUserBorrowInfo = (account, marketId, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
|
|
141
147
|
const json = yield res.json();
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
borrows.forEach((t) => {
|
|
147
|
-
var _a, _b;
|
|
148
|
-
const sellerAssets = new decimal_js_1.default(((_a = t.data) === null || _a === void 0 ? void 0 : _a.seller_assets) || 0);
|
|
149
|
-
const units = new decimal_js_1.default(((_b = t.data) === null || _b === void 0 ? void 0 : _b.units) || 0);
|
|
150
|
-
if (sellerAssets.lte(0) || units.lte(0))
|
|
151
|
-
return;
|
|
152
|
-
const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity, t.created_at);
|
|
153
|
-
const apy = (0, exports.midnightApyFromPrice)(sellerAssets.div(units), ttmDays); // price = seller_assets / units
|
|
154
|
-
sumSeller = sumSeller.add(sellerAssets);
|
|
155
|
-
sumUnits = sumUnits.add(units);
|
|
156
|
-
weightedApy = weightedApy.add(sellerAssets.mul(apy));
|
|
157
|
-
});
|
|
158
|
-
const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
|
|
159
|
-
const debtBase = (0, tokens_1.assetAmountInEth)(sumSeller.toFixed(0), loanTokenSymbol);
|
|
160
|
-
const debtTotal = (0, tokens_1.assetAmountInEth)(sumUnits.toFixed(0), loanTokenSymbol);
|
|
148
|
+
const position = ((json === null || json === void 0 ? void 0 : json.data) || []).find((p) => { var _a; return p.type === 'borrow' && ((_a = p.market_id) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === marketId.toLowerCase(); });
|
|
149
|
+
const debtTotal = (0, tokens_1.assetAmountInEth)((position === null || position === void 0 ? void 0 : position.debt) || '0', loanTokenSymbol);
|
|
150
|
+
const costBasisRaw = new decimal_js_1.default((position === null || position === void 0 ? void 0 : position.cost_basis) || 0).div(constants_1.WAD); // WAD-scaled → raw base units
|
|
151
|
+
const debtBase = decimal_js_1.default.min((0, tokens_1.assetAmountInEth)(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
|
|
161
152
|
const debtInterest = decimal_js_1.default.max(new decimal_js_1.default(debtTotal).sub(debtBase), 0).toString();
|
|
153
|
+
const borrowRate = new decimal_js_1.default((position === null || position === void 0 ? void 0 : position.effective_rate_wad) || 0).div(constants_1.WAD).mul(100).toString();
|
|
162
154
|
return {
|
|
163
155
|
borrowRate, debtBase, debtInterest, debtTotal,
|
|
164
156
|
};
|
|
165
157
|
});
|
|
166
158
|
exports.getMorphoMidnightUserBorrowInfo = getMorphoMidnightUserBorrowInfo;
|
|
159
|
+
/**
|
|
160
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
161
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
162
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
163
|
+
*
|
|
164
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
165
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
166
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
167
|
+
*
|
|
168
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
169
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
170
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
171
|
+
*/
|
|
172
|
+
const getMorphoMidnightMarketBook = (market_1, network_1, ...args_1) => __awaiter(void 0, [market_1, network_1, ...args_1], void 0, function* (market, network, side = 'bids') {
|
|
173
|
+
var _a;
|
|
174
|
+
const loanSymbol = (0, tokens_1.getAssetInfoByAddress)(market.loanToken, network).symbol;
|
|
175
|
+
const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
|
|
176
|
+
if (!res.ok)
|
|
177
|
+
throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
|
|
178
|
+
const json = yield res.json();
|
|
179
|
+
const ttmDays = (0, exports.midnightTimeToMaturityDays)(market.maturity);
|
|
180
|
+
const bestFirst = side === 'asks' ? -1 : 1;
|
|
181
|
+
const offers = (((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a[side]) || [])
|
|
182
|
+
.map((offer) => ({
|
|
183
|
+
rate: (0, exports.midnightApyFromPrice)(new decimal_js_1.default(offer.price).div(constants_1.WAD), ttmDays),
|
|
184
|
+
liquidity: (0, tokens_1.assetAmountInEth)(offer.assets, loanSymbol),
|
|
185
|
+
}))
|
|
186
|
+
.sort((a, b) => new decimal_js_1.default(a.rate).minus(b.rate).mul(bestFirst).toNumber());
|
|
187
|
+
if (offers.length === 0)
|
|
188
|
+
return null;
|
|
189
|
+
return {
|
|
190
|
+
bestRate: offers[0].rate,
|
|
191
|
+
totalLiquidity: offers.reduce((sum, offer) => sum.add(offer.liquidity), new decimal_js_1.default(0)).toString(),
|
|
192
|
+
offers,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
exports.getMorphoMidnightMarketBook = getMorphoMidnightMarketBook;
|
|
167
196
|
// The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
|
|
168
197
|
// (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
|
|
169
198
|
// Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
|
|
@@ -172,6 +201,22 @@ const midnightQuoteError = (error) => {
|
|
|
172
201
|
const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
|
|
173
202
|
return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
|
|
174
203
|
};
|
|
204
|
+
// The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
|
|
205
|
+
const fetchMorphoMidnightQuote = (marketId, side, assetsRaw, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?assets=${assetsRaw}&slippage=${(0, exports.midnightSlippageParam)(slippagePercent)}`;
|
|
207
|
+
const res = yield fetch(url, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
|
|
208
|
+
const json = yield res.json();
|
|
209
|
+
const d = json === null || json === void 0 ? void 0 : json.data;
|
|
210
|
+
if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
|
|
211
|
+
throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
|
|
212
|
+
return {
|
|
213
|
+
bestPrice: new decimal_js_1.default(d.average_best_price).div(constants_1.WAD).toString(),
|
|
214
|
+
worstPrice: new decimal_js_1.default(d.average_worst_price || 0).div(constants_1.WAD).toString(),
|
|
215
|
+
availableAssets: d.available_assets || '0',
|
|
216
|
+
availableUnits: d.available_units || '0',
|
|
217
|
+
takeableOffers: d.takeable_offers || [],
|
|
218
|
+
};
|
|
219
|
+
});
|
|
175
220
|
/**
|
|
176
221
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
177
222
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -190,14 +235,8 @@ const midnightQuoteError = (error) => {
|
|
|
190
235
|
* Compare the two before submitting and tell the user their ceiling is under the market rate.
|
|
191
236
|
*/
|
|
192
237
|
const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const json = yield res.json();
|
|
196
|
-
const d = json === null || json === void 0 ? void 0 : json.data;
|
|
197
|
-
if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
|
|
198
|
-
throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
|
|
199
|
-
const bestPrice = new decimal_js_1.default(d.average_best_price).div(constants_1.WAD).toString();
|
|
200
|
-
const worstPrice = new decimal_js_1.default(d.average_worst_price || 0).div(constants_1.WAD).toString();
|
|
238
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
|
|
239
|
+
const { bestPrice, worstPrice } = quote;
|
|
201
240
|
const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
|
|
202
241
|
const estBorrowRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
|
|
203
242
|
// Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
|
|
@@ -208,16 +247,46 @@ const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, matu
|
|
|
208
247
|
const maxRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
|
|
209
248
|
const newUnits = new decimal_js_1.default(bestPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(bestPrice).toFixed(0);
|
|
210
249
|
const maxUnits = new decimal_js_1.default(capPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(capPrice).toFixed(0);
|
|
211
|
-
return {
|
|
212
|
-
bestPrice,
|
|
213
|
-
worstPrice,
|
|
214
|
-
estBorrowRate,
|
|
250
|
+
return Object.assign(Object.assign({}, quote), { estBorrowRate,
|
|
215
251
|
maxRate,
|
|
216
252
|
newUnits,
|
|
217
|
-
maxUnits
|
|
218
|
-
availableAssets: d.available_assets || '0',
|
|
219
|
-
availableUnits: d.available_units || '0',
|
|
220
|
-
takeableOffers: d.takeable_offers || [],
|
|
221
|
-
};
|
|
253
|
+
maxUnits });
|
|
222
254
|
});
|
|
223
255
|
exports.getMorphoMidnightBorrowQuote = getMorphoMidnightBorrowQuote;
|
|
256
|
+
/**
|
|
257
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
258
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
259
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
260
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
261
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
262
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
263
|
+
*
|
|
264
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
265
|
+
* so the guard is a floor rather than a ceiling:
|
|
266
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
267
|
+
* this when a user pins a min rate.
|
|
268
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
269
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
270
|
+
*
|
|
271
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
272
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
273
|
+
*/
|
|
274
|
+
const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
275
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', assetsRaw, slippagePercent);
|
|
276
|
+
const { bestPrice, worstPrice } = quote;
|
|
277
|
+
const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
|
|
278
|
+
const estPaybackRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
|
|
279
|
+
const capPrice = minPaybackRate !== undefined && new decimal_js_1.default(minPaybackRate).gt(0)
|
|
280
|
+
? (0, exports.midnightPriceFromApy)(minPaybackRate, ttmDays)
|
|
281
|
+
: worstPrice;
|
|
282
|
+
const minRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
|
|
283
|
+
// Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
|
|
284
|
+
// `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
|
|
285
|
+
const newUnits = new decimal_js_1.default(bestPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(bestPrice).toFixed(0, decimal_js_1.default.ROUND_DOWN);
|
|
286
|
+
const minUnits = new decimal_js_1.default(capPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(capPrice).toFixed(0, decimal_js_1.default.ROUND_DOWN);
|
|
287
|
+
return Object.assign(Object.assign({}, quote), { estPaybackRate,
|
|
288
|
+
minRate,
|
|
289
|
+
newUnits,
|
|
290
|
+
minUnits });
|
|
291
|
+
});
|
|
292
|
+
exports.getMorphoMidnightPaybackQuote = getMorphoMidnightPaybackQuote;
|
|
@@ -168,7 +168,7 @@ function _getMorphoMidnightAccountData(provider, network, account, selectedMarke
|
|
|
168
168
|
let assetsDataForApy = marketInfo.assetsData;
|
|
169
169
|
if (new decimal_js_1.default(positionInfo.debt.toString()).gt(0)) {
|
|
170
170
|
try {
|
|
171
|
-
const borrowInfo = yield (0, morphoMidnightHelpers_1.getMorphoMidnightUserBorrowInfo)(account, marketId, marketInfo.
|
|
171
|
+
const borrowInfo = yield (0, morphoMidnightHelpers_1.getMorphoMidnightUserBorrowInfo)(account, marketId, marketInfo.loanToken);
|
|
172
172
|
borrowRate = borrowInfo.borrowRate;
|
|
173
173
|
debtBase = borrowInfo.debtBase;
|
|
174
174
|
debtInterest = borrowInfo.debtInterest;
|
|
@@ -60,6 +60,16 @@ export interface MorphoMidnightMarketInfo {
|
|
|
60
60
|
utillization: string;
|
|
61
61
|
assetsData: MorphoMidnightAssetsData;
|
|
62
62
|
}
|
|
63
|
+
export type MorphoMidnightBookSide = 'bids' | 'asks';
|
|
64
|
+
export interface MorphoMidnightBookOffer {
|
|
65
|
+
rate: string;
|
|
66
|
+
liquidity: string;
|
|
67
|
+
}
|
|
68
|
+
export interface MorphoMidnightParsedBook {
|
|
69
|
+
bestRate: string;
|
|
70
|
+
totalLiquidity: string;
|
|
71
|
+
offers: MorphoMidnightBookOffer[];
|
|
72
|
+
}
|
|
63
73
|
export interface MorphoMidnightAggregatedPositionData {
|
|
64
74
|
suppliedUsd: string;
|
|
65
75
|
suppliedCollateralUsd: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Dec from 'decimal.js';
|
|
2
|
-
import { MMUsedAssets } from '../../types/common';
|
|
3
|
-
import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketInfo } from '../../types';
|
|
2
|
+
import { MMUsedAssets, NetworkNumber } from '../../types/common';
|
|
3
|
+
import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightBookSide, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
|
|
4
4
|
/**
|
|
5
5
|
* Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
|
|
6
6
|
* the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
|
|
@@ -32,6 +32,17 @@ export interface MorphoMidnightBorrowQuote {
|
|
|
32
32
|
availableUnits: string;
|
|
33
33
|
takeableOffers: any[];
|
|
34
34
|
}
|
|
35
|
+
export interface MorphoMidnightPaybackQuote {
|
|
36
|
+
bestPrice: string;
|
|
37
|
+
worstPrice: string;
|
|
38
|
+
estPaybackRate: string;
|
|
39
|
+
minRate: string;
|
|
40
|
+
newUnits: string;
|
|
41
|
+
minUnits: string;
|
|
42
|
+
availableAssets: string;
|
|
43
|
+
availableUnits: string;
|
|
44
|
+
takeableOffers: any[];
|
|
45
|
+
}
|
|
35
46
|
export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
|
|
36
47
|
export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
|
|
37
48
|
/**
|
|
@@ -52,12 +63,29 @@ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec
|
|
|
52
63
|
*/
|
|
53
64
|
export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
|
|
54
65
|
/**
|
|
55
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
56
|
-
*
|
|
57
|
-
*
|
|
66
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
67
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
68
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
69
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
70
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
71
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
58
72
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
59
73
|
*/
|
|
60
|
-
export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string,
|
|
74
|
+
export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
|
|
75
|
+
/**
|
|
76
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
77
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
78
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
79
|
+
*
|
|
80
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
81
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
82
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
83
|
+
*
|
|
84
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
85
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
86
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
87
|
+
*/
|
|
88
|
+
export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
|
|
61
89
|
/**
|
|
62
90
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
63
91
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -76,3 +104,22 @@ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId
|
|
|
76
104
|
* Compare the two before submitting and tell the user their ceiling is under the market rate.
|
|
77
105
|
*/
|
|
78
106
|
export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
|
|
107
|
+
/**
|
|
108
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
109
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
110
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
111
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
112
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
113
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
114
|
+
*
|
|
115
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
116
|
+
* so the guard is a floor rather than a ceiling:
|
|
117
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
118
|
+
* this when a user pins a min rate.
|
|
119
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
120
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
121
|
+
*
|
|
122
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
123
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
124
|
+
*/
|
|
125
|
+
export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
|
|
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import Dec from 'decimal.js';
|
|
11
|
-
import { assetAmountInEth } from '@defisaver/tokens';
|
|
11
|
+
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';
|
|
@@ -79,6 +79,9 @@ export const getMorphoMidnightAggregatedPositionData = ({ usedAssets, assetsData
|
|
|
79
79
|
// loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
|
|
80
80
|
const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
|
|
81
81
|
const nowInSeconds = () => Math.floor(Date.now() / 1000);
|
|
82
|
+
// The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
|
|
83
|
+
// enough that markets drop out of the list for no reason.
|
|
84
|
+
const MIDNIGHT_BOOK_TIMEOUT = 30000;
|
|
82
85
|
// The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
|
|
83
86
|
// place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
|
|
84
87
|
const MIDNIGHT_SLIPPAGE_MIN = 0.1;
|
|
@@ -120,38 +123,63 @@ export const midnightPriceFromApy = (ratePercent, ttmDays) => {
|
|
|
120
123
|
*/
|
|
121
124
|
export const midnightSlippageParam = (slippagePercent) => Dec.min(Dec.max(new Dec(slippagePercent), MIDNIGHT_SLIPPAGE_MIN), MIDNIGHT_SLIPPAGE_MAX).toDP(1, Dec.ROUND_DOWN).toString();
|
|
122
125
|
/**
|
|
123
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
127
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
128
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
129
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
130
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
131
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
126
132
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
127
133
|
*/
|
|
128
|
-
export const getMorphoMidnightUserBorrowInfo = (account, marketId,
|
|
129
|
-
const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/
|
|
134
|
+
export const getMorphoMidnightUserBorrowInfo = (account, marketId, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
|
+
const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
130
136
|
const json = yield res.json();
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
borrows.forEach((t) => {
|
|
136
|
-
var _a, _b;
|
|
137
|
-
const sellerAssets = new Dec(((_a = t.data) === null || _a === void 0 ? void 0 : _a.seller_assets) || 0);
|
|
138
|
-
const units = new Dec(((_b = t.data) === null || _b === void 0 ? void 0 : _b.units) || 0);
|
|
139
|
-
if (sellerAssets.lte(0) || units.lte(0))
|
|
140
|
-
return;
|
|
141
|
-
const ttmDays = midnightTimeToMaturityDays(maturity, t.created_at);
|
|
142
|
-
const apy = midnightApyFromPrice(sellerAssets.div(units), ttmDays); // price = seller_assets / units
|
|
143
|
-
sumSeller = sumSeller.add(sellerAssets);
|
|
144
|
-
sumUnits = sumUnits.add(units);
|
|
145
|
-
weightedApy = weightedApy.add(sellerAssets.mul(apy));
|
|
146
|
-
});
|
|
147
|
-
const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
|
|
148
|
-
const debtBase = assetAmountInEth(sumSeller.toFixed(0), loanTokenSymbol);
|
|
149
|
-
const debtTotal = assetAmountInEth(sumUnits.toFixed(0), loanTokenSymbol);
|
|
137
|
+
const position = ((json === null || json === void 0 ? void 0 : json.data) || []).find((p) => { var _a; return p.type === 'borrow' && ((_a = p.market_id) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === marketId.toLowerCase(); });
|
|
138
|
+
const debtTotal = assetAmountInEth((position === null || position === void 0 ? void 0 : position.debt) || '0', loanTokenSymbol);
|
|
139
|
+
const costBasisRaw = new Dec((position === null || position === void 0 ? void 0 : position.cost_basis) || 0).div(WAD); // WAD-scaled → raw base units
|
|
140
|
+
const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
|
|
150
141
|
const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
|
|
142
|
+
const borrowRate = new Dec((position === null || position === void 0 ? void 0 : position.effective_rate_wad) || 0).div(WAD).mul(100).toString();
|
|
151
143
|
return {
|
|
152
144
|
borrowRate, debtBase, debtInterest, debtTotal,
|
|
153
145
|
};
|
|
154
146
|
});
|
|
147
|
+
/**
|
|
148
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
149
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
150
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
151
|
+
*
|
|
152
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
153
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
154
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
155
|
+
*
|
|
156
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
157
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
158
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
159
|
+
*/
|
|
160
|
+
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
|
+
var _a;
|
|
162
|
+
const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
|
|
163
|
+
const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
|
|
164
|
+
if (!res.ok)
|
|
165
|
+
throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
|
|
166
|
+
const json = yield res.json();
|
|
167
|
+
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) => ({
|
|
171
|
+
rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
|
|
172
|
+
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
|
+
};
|
|
182
|
+
});
|
|
155
183
|
// The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
|
|
156
184
|
// (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
|
|
157
185
|
// Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
|
|
@@ -160,6 +188,22 @@ const midnightQuoteError = (error) => {
|
|
|
160
188
|
const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
|
|
161
189
|
return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
|
|
162
190
|
};
|
|
191
|
+
// The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
|
|
192
|
+
const fetchMorphoMidnightQuote = (marketId, side, assetsRaw, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?assets=${assetsRaw}&slippage=${midnightSlippageParam(slippagePercent)}`;
|
|
194
|
+
const res = yield fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
195
|
+
const json = yield res.json();
|
|
196
|
+
const d = json === null || json === void 0 ? void 0 : json.data;
|
|
197
|
+
if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
|
|
198
|
+
throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
|
|
199
|
+
return {
|
|
200
|
+
bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
|
|
201
|
+
worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
|
|
202
|
+
availableAssets: d.available_assets || '0',
|
|
203
|
+
availableUnits: d.available_units || '0',
|
|
204
|
+
takeableOffers: d.takeable_offers || [],
|
|
205
|
+
};
|
|
206
|
+
});
|
|
163
207
|
/**
|
|
164
208
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
165
209
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -178,14 +222,8 @@ const midnightQuoteError = (error) => {
|
|
|
178
222
|
* Compare the two before submitting and tell the user their ceiling is under the market rate.
|
|
179
223
|
*/
|
|
180
224
|
export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const json = yield res.json();
|
|
184
|
-
const d = json === null || json === void 0 ? void 0 : json.data;
|
|
185
|
-
if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
|
|
186
|
-
throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
|
|
187
|
-
const bestPrice = new Dec(d.average_best_price).div(WAD).toString();
|
|
188
|
-
const worstPrice = new Dec(d.average_worst_price || 0).div(WAD).toString();
|
|
225
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
|
|
226
|
+
const { bestPrice, worstPrice } = quote;
|
|
189
227
|
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
190
228
|
const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
191
229
|
// Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
|
|
@@ -196,15 +234,44 @@ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercen
|
|
|
196
234
|
const maxRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
197
235
|
const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
|
|
198
236
|
const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
|
|
199
|
-
return {
|
|
200
|
-
bestPrice,
|
|
201
|
-
worstPrice,
|
|
202
|
-
estBorrowRate,
|
|
237
|
+
return Object.assign(Object.assign({}, quote), { estBorrowRate,
|
|
203
238
|
maxRate,
|
|
204
239
|
newUnits,
|
|
205
|
-
maxUnits
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
maxUnits });
|
|
241
|
+
});
|
|
242
|
+
/**
|
|
243
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
244
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
245
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
246
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
247
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
248
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
249
|
+
*
|
|
250
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
251
|
+
* so the guard is a floor rather than a ceiling:
|
|
252
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
253
|
+
* this when a user pins a min rate.
|
|
254
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
255
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
256
|
+
*
|
|
257
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
258
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
259
|
+
*/
|
|
260
|
+
export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
261
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', assetsRaw, slippagePercent);
|
|
262
|
+
const { bestPrice, worstPrice } = quote;
|
|
263
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
264
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
265
|
+
const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
|
|
266
|
+
? midnightPriceFromApy(minPaybackRate, ttmDays)
|
|
267
|
+
: worstPrice;
|
|
268
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
269
|
+
// Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
|
|
270
|
+
// `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
|
|
271
|
+
const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
272
|
+
const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
273
|
+
return Object.assign(Object.assign({}, quote), { estPaybackRate,
|
|
274
|
+
minRate,
|
|
275
|
+
newUnits,
|
|
276
|
+
minUnits });
|
|
210
277
|
});
|
|
@@ -157,7 +157,7 @@ export function _getMorphoMidnightAccountData(provider, network, account, select
|
|
|
157
157
|
let assetsDataForApy = marketInfo.assetsData;
|
|
158
158
|
if (new Dec(positionInfo.debt.toString()).gt(0)) {
|
|
159
159
|
try {
|
|
160
|
-
const borrowInfo = yield getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.
|
|
160
|
+
const borrowInfo = yield getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
|
|
161
161
|
borrowRate = borrowInfo.borrowRate;
|
|
162
162
|
debtBase = borrowInfo.debtBase;
|
|
163
163
|
debtInterest = borrowInfo.debtInterest;
|
|
@@ -60,6 +60,16 @@ export interface MorphoMidnightMarketInfo {
|
|
|
60
60
|
utillization: string;
|
|
61
61
|
assetsData: MorphoMidnightAssetsData;
|
|
62
62
|
}
|
|
63
|
+
export type MorphoMidnightBookSide = 'bids' | 'asks';
|
|
64
|
+
export interface MorphoMidnightBookOffer {
|
|
65
|
+
rate: string;
|
|
66
|
+
liquidity: string;
|
|
67
|
+
}
|
|
68
|
+
export interface MorphoMidnightParsedBook {
|
|
69
|
+
bestRate: string;
|
|
70
|
+
totalLiquidity: string;
|
|
71
|
+
offers: MorphoMidnightBookOffer[];
|
|
72
|
+
}
|
|
63
73
|
export interface MorphoMidnightAggregatedPositionData {
|
|
64
74
|
suppliedUsd: string;
|
|
65
75
|
suppliedCollateralUsd: string;
|
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import Dec from 'decimal.js';
|
|
2
|
-
import { assetAmountInEth } from '@defisaver/tokens';
|
|
2
|
+
import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
|
|
3
3
|
import {
|
|
4
4
|
calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos,
|
|
5
5
|
} from '../../moneymarket';
|
|
6
6
|
import { calculateNetApy } from '../../staking';
|
|
7
7
|
import {
|
|
8
|
-
LeverageType, MMAssetsData, MMUsedAsset, MMUsedAssets,
|
|
8
|
+
LeverageType, MMAssetsData, MMUsedAsset, MMUsedAssets, NetworkNumber,
|
|
9
9
|
} from '../../types/common';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
MorphoMidnightAggregatedPositionData,
|
|
12
|
+
MorphoMidnightAssetsData,
|
|
13
|
+
MorphoMidnightBookOffer,
|
|
14
|
+
MorphoMidnightBookSide,
|
|
15
|
+
MorphoMidnightMarketData,
|
|
16
|
+
MorphoMidnightMarketInfo,
|
|
17
|
+
MorphoMidnightParsedBook,
|
|
18
|
+
} from '../../types';
|
|
11
19
|
import { SECONDS_PER_DAY, WAD } from '../../constants';
|
|
12
20
|
import { LONGER_TIMEOUT } from '../../services/utils';
|
|
13
21
|
|
|
@@ -98,16 +106,21 @@ export const getMorphoMidnightAggregatedPositionData = ({
|
|
|
98
106
|
const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
|
|
99
107
|
const nowInSeconds = () => Math.floor(Date.now() / 1000);
|
|
100
108
|
|
|
109
|
+
// The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
|
|
110
|
+
// enough that markets drop out of the list for no reason.
|
|
111
|
+
const MIDNIGHT_BOOK_TIMEOUT = 30000;
|
|
112
|
+
|
|
101
113
|
// The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
|
|
102
114
|
// place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
|
|
103
115
|
const MIDNIGHT_SLIPPAGE_MIN = 0.1;
|
|
104
116
|
const MIDNIGHT_SLIPPAGE_MAX = 100;
|
|
105
117
|
|
|
106
|
-
interface
|
|
107
|
-
event_type: string,
|
|
118
|
+
interface MidnightPosition {
|
|
108
119
|
market_id: string,
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
type: string, // 'borrow' | 'lend'
|
|
121
|
+
debt: string, // raw loan-token base units — matches MidnightView.getPositionInfo exactly
|
|
122
|
+
cost_basis: string, // WAD-scaled raw base units — outstanding principal, net of exits/liquidations
|
|
123
|
+
effective_rate_wad: string, // WAD-scaled borrow APY, e.g. 0.05e18 = 5%
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
interface MidnightApiError {
|
|
@@ -116,6 +129,11 @@ interface MidnightApiError {
|
|
|
116
129
|
details?: ({ field?: string, issue?: string })[] | null,
|
|
117
130
|
}
|
|
118
131
|
|
|
132
|
+
interface MidnightRawOffer {
|
|
133
|
+
price: string, // WAD-scaled loan-per-unit
|
|
134
|
+
assets: string, // loan-token base units available at this offer
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
interface MidnightQuoteResponse {
|
|
120
138
|
average_best_price?: string,
|
|
121
139
|
average_worst_price?: string,
|
|
@@ -125,10 +143,10 @@ interface MidnightQuoteResponse {
|
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
export interface MorphoMidnightBorrowInfo {
|
|
128
|
-
borrowRate: string, //
|
|
129
|
-
debtBase: string, //
|
|
146
|
+
borrowRate: string, // effective borrow APY as a percent
|
|
147
|
+
debtBase: string, // outstanding principal (cost_basis), loan-token units
|
|
130
148
|
debtInterest: string, // debtTotal − debtBase (interest owed at maturity), loan-token units
|
|
131
|
-
debtTotal: string, //
|
|
149
|
+
debtTotal: string, // on-chain debt at maturity, loan-token units
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
export interface MorphoMidnightBorrowQuote {
|
|
@@ -143,6 +161,18 @@ export interface MorphoMidnightBorrowQuote {
|
|
|
143
161
|
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
export interface MorphoMidnightPaybackQuote {
|
|
165
|
+
bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
|
|
166
|
+
worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
|
|
167
|
+
estPaybackRate: string, // APY the repayment retires debt at, as a percent
|
|
168
|
+
minRate: string, // APY the on-chain floor permits, i.e. `minUnits` annualized (display only)
|
|
169
|
+
newUnits: string, // debt retired at best price, raw loan-token base units
|
|
170
|
+
minUnits: string, // floor on debt retired (on-chain guard), raw loan-token base units
|
|
171
|
+
availableAssets: string,
|
|
172
|
+
availableUnits: string,
|
|
173
|
+
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
174
|
+
}
|
|
175
|
+
|
|
146
176
|
// Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
|
|
147
177
|
export const midnightTimeToMaturityDays = (maturity: number, atSeconds: number = nowInSeconds()): number => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
|
|
148
178
|
|
|
@@ -185,46 +215,76 @@ export const midnightSlippageParam = (slippagePercent: Dec.Value): string => Dec
|
|
|
185
215
|
).toDP(1, Dec.ROUND_DOWN).toString();
|
|
186
216
|
|
|
187
217
|
/**
|
|
188
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
189
|
-
*
|
|
190
|
-
*
|
|
218
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
219
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
220
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
221
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
222
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
223
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
191
224
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
192
225
|
*/
|
|
193
226
|
export const getMorphoMidnightUserBorrowInfo = async (
|
|
194
227
|
account: string,
|
|
195
228
|
marketId: string,
|
|
196
|
-
maturity: number,
|
|
197
229
|
loanTokenSymbol: string,
|
|
198
230
|
): Promise<MorphoMidnightBorrowInfo> => {
|
|
199
|
-
const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/
|
|
200
|
-
const json: { data?:
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
borrows.forEach((t) => {
|
|
208
|
-
const sellerAssets = new Dec(t.data?.seller_assets || 0);
|
|
209
|
-
const units = new Dec(t.data?.units || 0);
|
|
210
|
-
if (sellerAssets.lte(0) || units.lte(0)) return;
|
|
211
|
-
const ttmDays = midnightTimeToMaturityDays(maturity, t.created_at);
|
|
212
|
-
const apy = midnightApyFromPrice(sellerAssets.div(units), ttmDays); // price = seller_assets / units
|
|
213
|
-
sumSeller = sumSeller.add(sellerAssets);
|
|
214
|
-
sumUnits = sumUnits.add(units);
|
|
215
|
-
weightedApy = weightedApy.add(sellerAssets.mul(apy));
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
|
|
219
|
-
const debtBase = assetAmountInEth(sumSeller.toFixed(0), loanTokenSymbol);
|
|
220
|
-
const debtTotal = assetAmountInEth(sumUnits.toFixed(0), loanTokenSymbol);
|
|
231
|
+
const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
232
|
+
const json: { data?: MidnightPosition[] } = await res.json();
|
|
233
|
+
const position = (json?.data || []).find((p) => p.type === 'borrow' && p.market_id?.toLowerCase() === marketId.toLowerCase());
|
|
234
|
+
|
|
235
|
+
const debtTotal = assetAmountInEth(position?.debt || '0', loanTokenSymbol);
|
|
236
|
+
const costBasisRaw = new Dec(position?.cost_basis || 0).div(WAD); // WAD-scaled → raw base units
|
|
237
|
+
const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
|
|
221
238
|
const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
|
|
239
|
+
const borrowRate = new Dec(position?.effective_rate_wad || 0).div(WAD).mul(100).toString();
|
|
222
240
|
|
|
223
241
|
return {
|
|
224
242
|
borrowRate, debtBase, debtInterest, debtTotal,
|
|
225
243
|
};
|
|
226
244
|
};
|
|
227
245
|
|
|
246
|
+
/**
|
|
247
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
248
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
249
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
250
|
+
*
|
|
251
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
252
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
253
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
254
|
+
*
|
|
255
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
256
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
257
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
258
|
+
*/
|
|
259
|
+
export const getMorphoMidnightMarketBook = async (
|
|
260
|
+
market: MorphoMidnightMarketData,
|
|
261
|
+
network: NetworkNumber,
|
|
262
|
+
side: MorphoMidnightBookSide = 'bids',
|
|
263
|
+
): Promise<MorphoMidnightParsedBook | null> => {
|
|
264
|
+
const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
|
|
265
|
+
const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
|
|
266
|
+
if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
|
|
267
|
+
|
|
268
|
+
const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
|
|
269
|
+
const ttmDays = midnightTimeToMaturityDays(market.maturity);
|
|
270
|
+
const bestFirst = side === 'asks' ? -1 : 1;
|
|
271
|
+
|
|
272
|
+
const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || [])
|
|
273
|
+
.map((offer) => ({
|
|
274
|
+
rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
|
|
275
|
+
liquidity: assetAmountInEth(offer.assets, loanSymbol),
|
|
276
|
+
}))
|
|
277
|
+
.sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
|
|
278
|
+
|
|
279
|
+
if (offers.length === 0) return null;
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
bestRate: offers[0].rate,
|
|
283
|
+
totalLiquidity: offers.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
|
|
284
|
+
offers,
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
|
|
228
288
|
// The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
|
|
229
289
|
// (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
|
|
230
290
|
// Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
|
|
@@ -234,6 +294,36 @@ const midnightQuoteError = (error?: MidnightApiError): string => {
|
|
|
234
294
|
return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
|
|
235
295
|
};
|
|
236
296
|
|
|
297
|
+
interface MidnightParsedQuote {
|
|
298
|
+
bestPrice: string, // loan-per-unit
|
|
299
|
+
worstPrice: string, // slippage-adjusted; below best on `bids`, above it on `asks`
|
|
300
|
+
availableAssets: string,
|
|
301
|
+
availableUnits: string,
|
|
302
|
+
takeableOffers: any[],
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
|
|
306
|
+
const fetchMorphoMidnightQuote = async (
|
|
307
|
+
marketId: string,
|
|
308
|
+
side: MorphoMidnightBookSide,
|
|
309
|
+
assetsRaw: string,
|
|
310
|
+
slippagePercent: Dec.Value,
|
|
311
|
+
): Promise<MidnightParsedQuote> => {
|
|
312
|
+
const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?assets=${assetsRaw}&slippage=${midnightSlippageParam(slippagePercent)}`;
|
|
313
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
314
|
+
const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
|
|
315
|
+
const d = json?.data;
|
|
316
|
+
if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
|
|
320
|
+
worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
|
|
321
|
+
availableAssets: d.available_assets || '0',
|
|
322
|
+
availableUnits: d.available_units || '0',
|
|
323
|
+
takeableOffers: d.takeable_offers || [],
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
237
327
|
/**
|
|
238
328
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
239
329
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -258,14 +348,8 @@ export const getMorphoMidnightBorrowQuote = async (
|
|
|
258
348
|
maturity: number,
|
|
259
349
|
maxBorrowRate?: Dec.Value,
|
|
260
350
|
): Promise<MorphoMidnightBorrowQuote> => {
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
|
|
264
|
-
const d = json?.data;
|
|
265
|
-
if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
|
|
266
|
-
|
|
267
|
-
const bestPrice = new Dec(d.average_best_price).div(WAD).toString();
|
|
268
|
-
const worstPrice = new Dec(d.average_worst_price || 0).div(WAD).toString();
|
|
351
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'bids', assetsRaw, slippagePercent);
|
|
352
|
+
const { bestPrice, worstPrice } = quote;
|
|
269
353
|
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
270
354
|
const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
271
355
|
|
|
@@ -279,14 +363,58 @@ export const getMorphoMidnightBorrowQuote = async (
|
|
|
279
363
|
const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
|
|
280
364
|
|
|
281
365
|
return {
|
|
282
|
-
|
|
283
|
-
worstPrice,
|
|
366
|
+
...quote,
|
|
284
367
|
estBorrowRate,
|
|
285
368
|
maxRate,
|
|
286
369
|
newUnits,
|
|
287
370
|
maxUnits,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
371
|
+
};
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
376
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
377
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
378
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
379
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
380
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
381
|
+
*
|
|
382
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
383
|
+
* so the guard is a floor rather than a ceiling:
|
|
384
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
385
|
+
* this when a user pins a min rate.
|
|
386
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
387
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
388
|
+
*
|
|
389
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
390
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
391
|
+
*/
|
|
392
|
+
export const getMorphoMidnightPaybackQuote = async (
|
|
393
|
+
marketId: string,
|
|
394
|
+
assetsRaw: string,
|
|
395
|
+
slippagePercent: Dec.Value,
|
|
396
|
+
maturity: number,
|
|
397
|
+
minPaybackRate?: Dec.Value,
|
|
398
|
+
): Promise<MorphoMidnightPaybackQuote> => {
|
|
399
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'asks', assetsRaw, slippagePercent);
|
|
400
|
+
const { bestPrice, worstPrice } = quote;
|
|
401
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
402
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
403
|
+
|
|
404
|
+
const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
|
|
405
|
+
? midnightPriceFromApy(minPaybackRate, ttmDays)
|
|
406
|
+
: worstPrice;
|
|
407
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
408
|
+
// Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
|
|
409
|
+
// `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
|
|
410
|
+
const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
411
|
+
const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
...quote,
|
|
415
|
+
estPaybackRate,
|
|
416
|
+
minRate,
|
|
417
|
+
newUnits,
|
|
418
|
+
minUnits,
|
|
291
419
|
};
|
|
292
420
|
};
|
|
@@ -166,7 +166,7 @@ export async function _getMorphoMidnightAccountData(provider: Client, network: N
|
|
|
166
166
|
let assetsDataForApy = marketInfo.assetsData;
|
|
167
167
|
if (new Dec(positionInfo.debt.toString()).gt(0)) {
|
|
168
168
|
try {
|
|
169
|
-
const borrowInfo = await getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.
|
|
169
|
+
const borrowInfo = await getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
|
|
170
170
|
borrowRate = borrowInfo.borrowRate;
|
|
171
171
|
debtBase = borrowInfo.debtBase;
|
|
172
172
|
debtInterest = borrowInfo.debtInterest;
|
|
@@ -70,6 +70,22 @@ export interface MorphoMidnightMarketInfo {
|
|
|
70
70
|
assetsData: MorphoMidnightAssetsData,
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// Which half of the order book a caller is taking from: `bids` are the lend offers a borrower fills,
|
|
74
|
+
// `asks` the sell offers a repayer buys debt units from.
|
|
75
|
+
export type MorphoMidnightBookSide = 'bids' | 'asks';
|
|
76
|
+
|
|
77
|
+
// One resting offer on a market's order book, as an annualized rate rather than the API's raw WAD price.
|
|
78
|
+
export interface MorphoMidnightBookOffer {
|
|
79
|
+
rate: string, // fixed APY, percent
|
|
80
|
+
liquidity: string, // loan-token amount available at this rate
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface MorphoMidnightParsedBook {
|
|
84
|
+
bestRate: string, // best rate for the taker of this side (= offers[0].rate)
|
|
85
|
+
totalLiquidity: string, // Σ offers[].liquidity, loan-token units
|
|
86
|
+
offers: MorphoMidnightBookOffer[], // best-first: bids ascending by rate, asks descending
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
export interface MorphoMidnightAggregatedPositionData {
|
|
74
90
|
suppliedUsd: string,
|
|
75
91
|
suppliedCollateralUsd: string,
|