@defisaver/positions-sdk 2.1.127-midnight-3-dev → 2.1.127-midnight-5-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 +76 -12
- package/cjs/helpers/morphoMidnightHelpers/index.js +124 -54
- package/cjs/morphoMidnight/index.js +1 -1
- package/cjs/types/morphoMidnight.d.ts +1 -0
- package/esm/helpers/morphoMidnightHelpers/index.d.ts +76 -12
- package/esm/helpers/morphoMidnightHelpers/index.js +121 -53
- package/esm/morphoMidnight/index.js +1 -1
- package/esm/types/morphoMidnight.d.ts +1 -0
- package/package.json +1 -1
- package/src/helpers/morphoMidnightHelpers/index.ts +203 -59
- package/src/morphoMidnight/index.ts +1 -1
- package/src/types/morphoMidnight.ts +8 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Dec from 'decimal.js';
|
|
2
2
|
import { MMUsedAssets, NetworkNumber } from '../../types/common';
|
|
3
|
-
import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
|
|
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,28 @@ 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
|
+
}
|
|
46
|
+
export interface MorphoMidnightPaybackUnitsQuote {
|
|
47
|
+
bestPrice: string;
|
|
48
|
+
worstPrice: string;
|
|
49
|
+
estPaybackRate: string;
|
|
50
|
+
minRate: string;
|
|
51
|
+
newAssets: string;
|
|
52
|
+
maxAssets: string;
|
|
53
|
+
availableAssets: string;
|
|
54
|
+
availableUnits: string;
|
|
55
|
+
takeableOffers: any[];
|
|
56
|
+
}
|
|
35
57
|
export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
|
|
36
58
|
export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
|
|
37
59
|
/**
|
|
@@ -52,22 +74,29 @@ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec
|
|
|
52
74
|
*/
|
|
53
75
|
export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
|
|
54
76
|
/**
|
|
55
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
56
|
-
*
|
|
57
|
-
*
|
|
77
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
78
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
79
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
80
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
81
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
82
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
58
83
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
59
84
|
*/
|
|
60
|
-
export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string,
|
|
85
|
+
export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
|
|
61
86
|
/**
|
|
62
|
-
*
|
|
63
|
-
* each price against time-to-maturity gives the rate a
|
|
64
|
-
* against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
87
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
88
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
89
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
90
|
+
*
|
|
91
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
92
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
93
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
65
94
|
*
|
|
66
|
-
* Returns `null` for an empty
|
|
67
|
-
*
|
|
68
|
-
*
|
|
95
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
96
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
97
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
69
98
|
*/
|
|
70
|
-
export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber) => Promise<MorphoMidnightParsedBook | null>;
|
|
99
|
+
export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
|
|
71
100
|
/**
|
|
72
101
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
73
102
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -86,3 +115,38 @@ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketD
|
|
|
86
115
|
* Compare the two before submitting and tell the user their ceiling is under the market rate.
|
|
87
116
|
*/
|
|
88
117
|
export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
|
|
118
|
+
/**
|
|
119
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
120
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
121
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
122
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
123
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
124
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
125
|
+
*
|
|
126
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
127
|
+
* so the guard is a floor rather than a ceiling:
|
|
128
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
129
|
+
* this when a user pins a min rate.
|
|
130
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
131
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
132
|
+
*
|
|
133
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
134
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
135
|
+
*/
|
|
136
|
+
export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
|
|
137
|
+
/**
|
|
138
|
+
* Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
|
|
139
|
+
* me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
|
|
140
|
+
* side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
|
|
141
|
+
* a ceiling on assets spent rather than a floor on units bought.
|
|
142
|
+
*
|
|
143
|
+
* This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
|
|
144
|
+
* maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
|
|
145
|
+
* close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
|
|
146
|
+
* than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
|
|
147
|
+
* directly, rather than through an action contract that sweeps the remainder back — need this quote.
|
|
148
|
+
*
|
|
149
|
+
* A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
|
|
150
|
+
* buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
|
|
151
|
+
*/
|
|
152
|
+
export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackUnitsQuote>;
|
|
@@ -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.getMorphoMidnightMarketBook = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.midnightPriceFromApy = exports.midnightApyFromPrice = exports.midnightTimeToMaturityDays = exports.getMorphoMidnightAggregatedPositionData = void 0;
|
|
15
|
+
exports.getMorphoMidnightPaybackUnitsQuote = 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");
|
|
@@ -134,49 +134,42 @@ exports.midnightPriceFromApy = midnightPriceFromApy;
|
|
|
134
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();
|
|
135
135
|
exports.midnightSlippageParam = midnightSlippageParam;
|
|
136
136
|
/**
|
|
137
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
138
|
-
*
|
|
139
|
-
*
|
|
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.
|
|
140
143
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
141
144
|
*/
|
|
142
|
-
const getMorphoMidnightUserBorrowInfo = (account, marketId,
|
|
143
|
-
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) });
|
|
144
147
|
const json = yield res.json();
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
borrows.forEach((t) => {
|
|
150
|
-
var _a, _b;
|
|
151
|
-
const sellerAssets = new decimal_js_1.default(((_a = t.data) === null || _a === void 0 ? void 0 : _a.seller_assets) || 0);
|
|
152
|
-
const units = new decimal_js_1.default(((_b = t.data) === null || _b === void 0 ? void 0 : _b.units) || 0);
|
|
153
|
-
if (sellerAssets.lte(0) || units.lte(0))
|
|
154
|
-
return;
|
|
155
|
-
const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity, t.created_at);
|
|
156
|
-
const apy = (0, exports.midnightApyFromPrice)(sellerAssets.div(units), ttmDays); // price = seller_assets / units
|
|
157
|
-
sumSeller = sumSeller.add(sellerAssets);
|
|
158
|
-
sumUnits = sumUnits.add(units);
|
|
159
|
-
weightedApy = weightedApy.add(sellerAssets.mul(apy));
|
|
160
|
-
});
|
|
161
|
-
const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
|
|
162
|
-
const debtBase = (0, tokens_1.assetAmountInEth)(sumSeller.toFixed(0), loanTokenSymbol);
|
|
163
|
-
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();
|
|
164
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();
|
|
165
154
|
return {
|
|
166
155
|
borrowRate, debtBase, debtInterest, debtTotal,
|
|
167
156
|
};
|
|
168
157
|
});
|
|
169
158
|
exports.getMorphoMidnightUserBorrowInfo = getMorphoMidnightUserBorrowInfo;
|
|
170
159
|
/**
|
|
171
|
-
*
|
|
172
|
-
* each price against time-to-maturity gives the rate a
|
|
173
|
-
* against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
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`.
|
|
174
167
|
*
|
|
175
|
-
* Returns `null` for an empty
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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.
|
|
178
171
|
*/
|
|
179
|
-
const getMorphoMidnightMarketBook = (
|
|
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') {
|
|
180
173
|
var _a;
|
|
181
174
|
const loanSymbol = (0, tokens_1.getAssetInfoByAddress)(market.loanToken, network).symbol;
|
|
182
175
|
const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
|
|
@@ -184,12 +177,13 @@ const getMorphoMidnightMarketBook = (market, network) => __awaiter(void 0, void
|
|
|
184
177
|
throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
|
|
185
178
|
const json = yield res.json();
|
|
186
179
|
const ttmDays = (0, exports.midnightTimeToMaturityDays)(market.maturity);
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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),
|
|
191
185
|
}))
|
|
192
|
-
.sort((a, b) => new decimal_js_1.default(a.rate).minus(b.rate).toNumber());
|
|
186
|
+
.sort((a, b) => new decimal_js_1.default(a.rate).minus(b.rate).mul(bestFirst).toNumber());
|
|
193
187
|
if (offers.length === 0)
|
|
194
188
|
return null;
|
|
195
189
|
return {
|
|
@@ -207,6 +201,23 @@ const midnightQuoteError = (error) => {
|
|
|
207
201
|
const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
|
|
208
202
|
return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
|
|
209
203
|
};
|
|
204
|
+
// The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
|
|
205
|
+
const fetchMorphoMidnightQuote = (marketId, side, size, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
|
|
207
|
+
const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${(0, exports.midnightSlippageParam)(slippagePercent)}`;
|
|
208
|
+
const res = yield fetch(url, { signal: AbortSignal.timeout(utils_1.LONGER_TIMEOUT) });
|
|
209
|
+
const json = yield res.json();
|
|
210
|
+
const d = json === null || json === void 0 ? void 0 : json.data;
|
|
211
|
+
if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
|
|
212
|
+
throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
|
|
213
|
+
return {
|
|
214
|
+
bestPrice: new decimal_js_1.default(d.average_best_price).div(constants_1.WAD).toString(),
|
|
215
|
+
worstPrice: new decimal_js_1.default(d.average_worst_price || 0).div(constants_1.WAD).toString(),
|
|
216
|
+
availableAssets: d.available_assets || '0',
|
|
217
|
+
availableUnits: d.available_units || '0',
|
|
218
|
+
takeableOffers: d.takeable_offers || [],
|
|
219
|
+
};
|
|
220
|
+
});
|
|
210
221
|
/**
|
|
211
222
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
212
223
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -225,14 +236,8 @@ const midnightQuoteError = (error) => {
|
|
|
225
236
|
* Compare the two before submitting and tell the user their ceiling is under the market rate.
|
|
226
237
|
*/
|
|
227
238
|
const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
const json = yield res.json();
|
|
231
|
-
const d = json === null || json === void 0 ? void 0 : json.data;
|
|
232
|
-
if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
|
|
233
|
-
throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
|
|
234
|
-
const bestPrice = new decimal_js_1.default(d.average_best_price).div(constants_1.WAD).toString();
|
|
235
|
-
const worstPrice = new decimal_js_1.default(d.average_worst_price || 0).div(constants_1.WAD).toString();
|
|
239
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
|
|
240
|
+
const { bestPrice, worstPrice } = quote;
|
|
236
241
|
const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
|
|
237
242
|
const estBorrowRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
|
|
238
243
|
// Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
|
|
@@ -243,16 +248,81 @@ const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, matu
|
|
|
243
248
|
const maxRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
|
|
244
249
|
const newUnits = new decimal_js_1.default(bestPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(bestPrice).toFixed(0);
|
|
245
250
|
const maxUnits = new decimal_js_1.default(capPrice).lte(0) ? '0' : new decimal_js_1.default(assetsRaw).div(capPrice).toFixed(0);
|
|
246
|
-
return {
|
|
247
|
-
bestPrice,
|
|
248
|
-
worstPrice,
|
|
249
|
-
estBorrowRate,
|
|
251
|
+
return Object.assign(Object.assign({}, quote), { estBorrowRate,
|
|
250
252
|
maxRate,
|
|
251
253
|
newUnits,
|
|
252
|
-
maxUnits
|
|
253
|
-
availableAssets: d.available_assets || '0',
|
|
254
|
-
availableUnits: d.available_units || '0',
|
|
255
|
-
takeableOffers: d.takeable_offers || [],
|
|
256
|
-
};
|
|
254
|
+
maxUnits });
|
|
257
255
|
});
|
|
258
256
|
exports.getMorphoMidnightBorrowQuote = getMorphoMidnightBorrowQuote;
|
|
257
|
+
/**
|
|
258
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
259
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
260
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
261
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
262
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
263
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
264
|
+
*
|
|
265
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
266
|
+
* so the guard is a floor rather than a ceiling:
|
|
267
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
268
|
+
* this when a user pins a min rate.
|
|
269
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
270
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
271
|
+
*
|
|
272
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
273
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
274
|
+
*/
|
|
275
|
+
const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
276
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
|
|
277
|
+
const { bestPrice, worstPrice } = quote;
|
|
278
|
+
const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
|
|
279
|
+
const estPaybackRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
|
|
280
|
+
const capPrice = minPaybackRate !== undefined && new decimal_js_1.default(minPaybackRate).gt(0)
|
|
281
|
+
? (0, exports.midnightPriceFromApy)(minPaybackRate, ttmDays)
|
|
282
|
+
: worstPrice;
|
|
283
|
+
const minRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
|
|
284
|
+
// Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
|
|
285
|
+
// `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
|
|
286
|
+
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);
|
|
287
|
+
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);
|
|
288
|
+
return Object.assign(Object.assign({}, quote), { estPaybackRate,
|
|
289
|
+
minRate,
|
|
290
|
+
newUnits,
|
|
291
|
+
minUnits });
|
|
292
|
+
});
|
|
293
|
+
exports.getMorphoMidnightPaybackQuote = getMorphoMidnightPaybackQuote;
|
|
294
|
+
/**
|
|
295
|
+
* Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
|
|
296
|
+
* me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
|
|
297
|
+
* side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
|
|
298
|
+
* a ceiling on assets spent rather than a floor on units bought.
|
|
299
|
+
*
|
|
300
|
+
* This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
|
|
301
|
+
* maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
|
|
302
|
+
* close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
|
|
303
|
+
* than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
|
|
304
|
+
* directly, rather than through an action contract that sweeps the remainder back — need this quote.
|
|
305
|
+
*
|
|
306
|
+
* A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
|
|
307
|
+
* buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
|
|
308
|
+
*/
|
|
309
|
+
const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
310
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
|
|
311
|
+
const { bestPrice, worstPrice } = quote;
|
|
312
|
+
const ttmDays = (0, exports.midnightTimeToMaturityDays)(maturity);
|
|
313
|
+
const estPaybackRate = (0, exports.midnightApyFromPrice)(bestPrice, ttmDays);
|
|
314
|
+
const capPrice = minPaybackRate !== undefined && new decimal_js_1.default(minPaybackRate).gt(0)
|
|
315
|
+
? (0, exports.midnightPriceFromApy)(minPaybackRate, ttmDays)
|
|
316
|
+
: worstPrice;
|
|
317
|
+
const minRate = (0, exports.midnightApyFromPrice)(capPrice, ttmDays);
|
|
318
|
+
// Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
|
|
319
|
+
// what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
|
|
320
|
+
// buy short of the units it was asked for.
|
|
321
|
+
const newAssets = new decimal_js_1.default(unitsRaw).mul(bestPrice).toFixed(0, decimal_js_1.default.ROUND_UP);
|
|
322
|
+
const maxAssets = new decimal_js_1.default(unitsRaw).mul(capPrice).toFixed(0, decimal_js_1.default.ROUND_UP);
|
|
323
|
+
return Object.assign(Object.assign({}, quote), { estPaybackRate,
|
|
324
|
+
minRate,
|
|
325
|
+
newAssets,
|
|
326
|
+
maxAssets });
|
|
327
|
+
});
|
|
328
|
+
exports.getMorphoMidnightPaybackUnitsQuote = getMorphoMidnightPaybackUnitsQuote;
|
|
@@ -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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Dec from 'decimal.js';
|
|
2
2
|
import { MMUsedAssets, NetworkNumber } from '../../types/common';
|
|
3
|
-
import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
|
|
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,28 @@ 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
|
+
}
|
|
46
|
+
export interface MorphoMidnightPaybackUnitsQuote {
|
|
47
|
+
bestPrice: string;
|
|
48
|
+
worstPrice: string;
|
|
49
|
+
estPaybackRate: string;
|
|
50
|
+
minRate: string;
|
|
51
|
+
newAssets: string;
|
|
52
|
+
maxAssets: string;
|
|
53
|
+
availableAssets: string;
|
|
54
|
+
availableUnits: string;
|
|
55
|
+
takeableOffers: any[];
|
|
56
|
+
}
|
|
35
57
|
export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
|
|
36
58
|
export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
|
|
37
59
|
/**
|
|
@@ -52,22 +74,29 @@ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec
|
|
|
52
74
|
*/
|
|
53
75
|
export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
|
|
54
76
|
/**
|
|
55
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
56
|
-
*
|
|
57
|
-
*
|
|
77
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
78
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
79
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
80
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
81
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
82
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
58
83
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
59
84
|
*/
|
|
60
|
-
export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string,
|
|
85
|
+
export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
|
|
61
86
|
/**
|
|
62
|
-
*
|
|
63
|
-
* each price against time-to-maturity gives the rate a
|
|
64
|
-
* against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
87
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
88
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
89
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
90
|
+
*
|
|
91
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
92
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
93
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
65
94
|
*
|
|
66
|
-
* Returns `null` for an empty
|
|
67
|
-
*
|
|
68
|
-
*
|
|
95
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
96
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
97
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
69
98
|
*/
|
|
70
|
-
export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber) => Promise<MorphoMidnightParsedBook | null>;
|
|
99
|
+
export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
|
|
71
100
|
/**
|
|
72
101
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
73
102
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -86,3 +115,38 @@ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketD
|
|
|
86
115
|
* Compare the two before submitting and tell the user their ceiling is under the market rate.
|
|
87
116
|
*/
|
|
88
117
|
export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
|
|
118
|
+
/**
|
|
119
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
120
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
121
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
122
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
123
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
124
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
125
|
+
*
|
|
126
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
127
|
+
* so the guard is a floor rather than a ceiling:
|
|
128
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
129
|
+
* this when a user pins a min rate.
|
|
130
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
131
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
132
|
+
*
|
|
133
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
134
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
135
|
+
*/
|
|
136
|
+
export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
|
|
137
|
+
/**
|
|
138
|
+
* Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
|
|
139
|
+
* me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
|
|
140
|
+
* side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
|
|
141
|
+
* a ceiling on assets spent rather than a floor on units bought.
|
|
142
|
+
*
|
|
143
|
+
* This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
|
|
144
|
+
* maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
|
|
145
|
+
* close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
|
|
146
|
+
* than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
|
|
147
|
+
* directly, rather than through an action contract that sweeps the remainder back — need this quote.
|
|
148
|
+
*
|
|
149
|
+
* A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
|
|
150
|
+
* buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
|
|
151
|
+
*/
|
|
152
|
+
export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value) => Promise<MorphoMidnightPaybackUnitsQuote>;
|
|
@@ -123,48 +123,41 @@ export const midnightPriceFromApy = (ratePercent, ttmDays) => {
|
|
|
123
123
|
*/
|
|
124
124
|
export const midnightSlippageParam = (slippagePercent) => Dec.min(Dec.max(new Dec(slippagePercent), MIDNIGHT_SLIPPAGE_MIN), MIDNIGHT_SLIPPAGE_MAX).toDP(1, Dec.ROUND_DOWN).toString();
|
|
125
125
|
/**
|
|
126
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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.
|
|
129
132
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
130
133
|
*/
|
|
131
|
-
export const getMorphoMidnightUserBorrowInfo = (account, marketId,
|
|
132
|
-
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) });
|
|
133
136
|
const json = yield res.json();
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
borrows.forEach((t) => {
|
|
139
|
-
var _a, _b;
|
|
140
|
-
const sellerAssets = new Dec(((_a = t.data) === null || _a === void 0 ? void 0 : _a.seller_assets) || 0);
|
|
141
|
-
const units = new Dec(((_b = t.data) === null || _b === void 0 ? void 0 : _b.units) || 0);
|
|
142
|
-
if (sellerAssets.lte(0) || units.lte(0))
|
|
143
|
-
return;
|
|
144
|
-
const ttmDays = midnightTimeToMaturityDays(maturity, t.created_at);
|
|
145
|
-
const apy = midnightApyFromPrice(sellerAssets.div(units), ttmDays); // price = seller_assets / units
|
|
146
|
-
sumSeller = sumSeller.add(sellerAssets);
|
|
147
|
-
sumUnits = sumUnits.add(units);
|
|
148
|
-
weightedApy = weightedApy.add(sellerAssets.mul(apy));
|
|
149
|
-
});
|
|
150
|
-
const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
|
|
151
|
-
const debtBase = assetAmountInEth(sumSeller.toFixed(0), loanTokenSymbol);
|
|
152
|
-
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();
|
|
153
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();
|
|
154
143
|
return {
|
|
155
144
|
borrowRate, debtBase, debtInterest, debtTotal,
|
|
156
145
|
};
|
|
157
146
|
});
|
|
158
147
|
/**
|
|
159
|
-
*
|
|
160
|
-
* each price against time-to-maturity gives the rate a
|
|
161
|
-
* against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
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`.
|
|
162
155
|
*
|
|
163
|
-
* Returns `null` for an empty
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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.
|
|
166
159
|
*/
|
|
167
|
-
export const getMorphoMidnightMarketBook = (
|
|
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') {
|
|
168
161
|
var _a;
|
|
169
162
|
const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
|
|
170
163
|
const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
|
|
@@ -172,12 +165,13 @@ export const getMorphoMidnightMarketBook = (market, network) => __awaiter(void 0
|
|
|
172
165
|
throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
|
|
173
166
|
const json = yield res.json();
|
|
174
167
|
const ttmDays = midnightTimeToMaturityDays(market.maturity);
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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),
|
|
179
173
|
}))
|
|
180
|
-
.sort((a, b) => new Dec(a.rate).minus(b.rate).toNumber());
|
|
174
|
+
.sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
|
|
181
175
|
if (offers.length === 0)
|
|
182
176
|
return null;
|
|
183
177
|
return {
|
|
@@ -194,6 +188,23 @@ const midnightQuoteError = (error) => {
|
|
|
194
188
|
const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
|
|
195
189
|
return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
|
|
196
190
|
};
|
|
191
|
+
// The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
|
|
192
|
+
const fetchMorphoMidnightQuote = (marketId, side, size, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
|
|
194
|
+
const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${midnightSlippageParam(slippagePercent)}`;
|
|
195
|
+
const res = yield fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
196
|
+
const json = yield res.json();
|
|
197
|
+
const d = json === null || json === void 0 ? void 0 : json.data;
|
|
198
|
+
if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
|
|
199
|
+
throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
|
|
200
|
+
return {
|
|
201
|
+
bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
|
|
202
|
+
worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
|
|
203
|
+
availableAssets: d.available_assets || '0',
|
|
204
|
+
availableUnits: d.available_units || '0',
|
|
205
|
+
takeableOffers: d.takeable_offers || [],
|
|
206
|
+
};
|
|
207
|
+
});
|
|
197
208
|
/**
|
|
198
209
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
199
210
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -212,14 +223,8 @@ const midnightQuoteError = (error) => {
|
|
|
212
223
|
* Compare the two before submitting and tell the user their ceiling is under the market rate.
|
|
213
224
|
*/
|
|
214
225
|
export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const json = yield res.json();
|
|
218
|
-
const d = json === null || json === void 0 ? void 0 : json.data;
|
|
219
|
-
if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
|
|
220
|
-
throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
|
|
221
|
-
const bestPrice = new Dec(d.average_best_price).div(WAD).toString();
|
|
222
|
-
const worstPrice = new Dec(d.average_worst_price || 0).div(WAD).toString();
|
|
226
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
|
|
227
|
+
const { bestPrice, worstPrice } = quote;
|
|
223
228
|
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
224
229
|
const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
225
230
|
// Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
|
|
@@ -230,15 +235,78 @@ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercen
|
|
|
230
235
|
const maxRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
231
236
|
const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
|
|
232
237
|
const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
|
|
233
|
-
return {
|
|
234
|
-
bestPrice,
|
|
235
|
-
worstPrice,
|
|
236
|
-
estBorrowRate,
|
|
238
|
+
return Object.assign(Object.assign({}, quote), { estBorrowRate,
|
|
237
239
|
maxRate,
|
|
238
240
|
newUnits,
|
|
239
|
-
maxUnits
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
maxUnits });
|
|
242
|
+
});
|
|
243
|
+
/**
|
|
244
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
245
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
246
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
247
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
248
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
249
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
250
|
+
*
|
|
251
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
252
|
+
* so the guard is a floor rather than a ceiling:
|
|
253
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
254
|
+
* this when a user pins a min rate.
|
|
255
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
256
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
257
|
+
*
|
|
258
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
259
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
260
|
+
*/
|
|
261
|
+
export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
262
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
|
|
263
|
+
const { bestPrice, worstPrice } = quote;
|
|
264
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
265
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
266
|
+
const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
|
|
267
|
+
? midnightPriceFromApy(minPaybackRate, ttmDays)
|
|
268
|
+
: worstPrice;
|
|
269
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
270
|
+
// Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
|
|
271
|
+
// `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
|
|
272
|
+
const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
273
|
+
const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
274
|
+
return Object.assign(Object.assign({}, quote), { estPaybackRate,
|
|
275
|
+
minRate,
|
|
276
|
+
newUnits,
|
|
277
|
+
minUnits });
|
|
278
|
+
});
|
|
279
|
+
/**
|
|
280
|
+
* Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
|
|
281
|
+
* me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
|
|
282
|
+
* side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
|
|
283
|
+
* a ceiling on assets spent rather than a floor on units bought.
|
|
284
|
+
*
|
|
285
|
+
* This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
|
|
286
|
+
* maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
|
|
287
|
+
* close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
|
|
288
|
+
* than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
|
|
289
|
+
* directly, rather than through an action contract that sweeps the remainder back — need this quote.
|
|
290
|
+
*
|
|
291
|
+
* A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
|
|
292
|
+
* buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
|
|
293
|
+
*/
|
|
294
|
+
export const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate) => __awaiter(void 0, void 0, void 0, function* () {
|
|
295
|
+
const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
|
|
296
|
+
const { bestPrice, worstPrice } = quote;
|
|
297
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
298
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
299
|
+
const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
|
|
300
|
+
? midnightPriceFromApy(minPaybackRate, ttmDays)
|
|
301
|
+
: worstPrice;
|
|
302
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
303
|
+
// Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
|
|
304
|
+
// what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
|
|
305
|
+
// buy short of the units it was asked for.
|
|
306
|
+
const newAssets = new Dec(unitsRaw).mul(bestPrice).toFixed(0, Dec.ROUND_UP);
|
|
307
|
+
const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
|
|
308
|
+
return Object.assign(Object.assign({}, quote), { estPaybackRate,
|
|
309
|
+
minRate,
|
|
310
|
+
newAssets,
|
|
311
|
+
maxAssets });
|
|
244
312
|
});
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
MorphoMidnightAggregatedPositionData,
|
|
12
12
|
MorphoMidnightAssetsData,
|
|
13
13
|
MorphoMidnightBookOffer,
|
|
14
|
+
MorphoMidnightBookSide,
|
|
14
15
|
MorphoMidnightMarketData,
|
|
15
16
|
MorphoMidnightMarketInfo,
|
|
16
17
|
MorphoMidnightParsedBook,
|
|
@@ -114,11 +115,12 @@ const MIDNIGHT_BOOK_TIMEOUT = 30000;
|
|
|
114
115
|
const MIDNIGHT_SLIPPAGE_MIN = 0.1;
|
|
115
116
|
const MIDNIGHT_SLIPPAGE_MAX = 100;
|
|
116
117
|
|
|
117
|
-
interface
|
|
118
|
-
event_type: string,
|
|
118
|
+
interface MidnightPosition {
|
|
119
119
|
market_id: string,
|
|
120
|
-
|
|
121
|
-
|
|
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%
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
interface MidnightApiError {
|
|
@@ -127,7 +129,7 @@ interface MidnightApiError {
|
|
|
127
129
|
details?: ({ field?: string, issue?: string })[] | null,
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
interface
|
|
132
|
+
interface MidnightRawOffer {
|
|
131
133
|
price: string, // WAD-scaled loan-per-unit
|
|
132
134
|
assets: string, // loan-token base units available at this offer
|
|
133
135
|
}
|
|
@@ -141,10 +143,10 @@ interface MidnightQuoteResponse {
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
export interface MorphoMidnightBorrowInfo {
|
|
144
|
-
borrowRate: string, //
|
|
145
|
-
debtBase: string, //
|
|
146
|
+
borrowRate: string, // effective borrow APY as a percent
|
|
147
|
+
debtBase: string, // outstanding principal (cost_basis), loan-token units
|
|
146
148
|
debtInterest: string, // debtTotal − debtBase (interest owed at maturity), loan-token units
|
|
147
|
-
debtTotal: string, //
|
|
149
|
+
debtTotal: string, // on-chain debt at maturity, loan-token units
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
export interface MorphoMidnightBorrowQuote {
|
|
@@ -159,6 +161,32 @@ export interface MorphoMidnightBorrowQuote {
|
|
|
159
161
|
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
160
162
|
}
|
|
161
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
|
+
|
|
176
|
+
// Payback quoted the other way round: the caller names the debt units to retire, and the quote prices
|
|
177
|
+
// what buying them costs. Same rate guard as the assets-target quote, expressed as a spend ceiling.
|
|
178
|
+
export interface MorphoMidnightPaybackUnitsQuote {
|
|
179
|
+
bestPrice: string, // average_best_price, loan-per-unit — the cheapest units on the ask side
|
|
180
|
+
worstPrice: string, // average_worst_price, slippage-adjusted (ABOVE best: a dearer unit)
|
|
181
|
+
estPaybackRate: string, // APY the repayment retires debt at, as a percent
|
|
182
|
+
minRate: string, // APY the on-chain ceiling permits, i.e. `maxAssets` annualized (display only)
|
|
183
|
+
newAssets: string, // assets the target units cost at best price, raw loan-token base units
|
|
184
|
+
maxAssets: string, // ceiling on assets spent (on-chain guard), raw loan-token base units
|
|
185
|
+
availableAssets: string,
|
|
186
|
+
availableUnits: string,
|
|
187
|
+
takeableOffers: any[], // opaque orderbook offers, forwarded verbatim to on-chain execution
|
|
188
|
+
}
|
|
189
|
+
|
|
162
190
|
// Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
|
|
163
191
|
export const midnightTimeToMaturityDays = (maturity: number, atSeconds: number = nowInSeconds()): number => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
|
|
164
192
|
|
|
@@ -201,40 +229,28 @@ export const midnightSlippageParam = (slippagePercent: Dec.Value): string => Dec
|
|
|
201
229
|
).toDP(1, Dec.ROUND_DOWN).toString();
|
|
202
230
|
|
|
203
231
|
/**
|
|
204
|
-
* Current borrower rate + debt breakdown from the Midnight
|
|
205
|
-
*
|
|
206
|
-
*
|
|
232
|
+
* Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
|
|
233
|
+
* `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
|
|
234
|
+
* early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
|
|
235
|
+
* has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
|
|
236
|
+
* `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
|
|
237
|
+
* WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
|
|
207
238
|
* The caller swallows errors — a missing rate must never block position rendering.
|
|
208
239
|
*/
|
|
209
240
|
export const getMorphoMidnightUserBorrowInfo = async (
|
|
210
241
|
account: string,
|
|
211
242
|
marketId: string,
|
|
212
|
-
maturity: number,
|
|
213
243
|
loanTokenSymbol: string,
|
|
214
244
|
): Promise<MorphoMidnightBorrowInfo> => {
|
|
215
|
-
const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/
|
|
216
|
-
const json: { data?:
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
borrows.forEach((t) => {
|
|
224
|
-
const sellerAssets = new Dec(t.data?.seller_assets || 0);
|
|
225
|
-
const units = new Dec(t.data?.units || 0);
|
|
226
|
-
if (sellerAssets.lte(0) || units.lte(0)) return;
|
|
227
|
-
const ttmDays = midnightTimeToMaturityDays(maturity, t.created_at);
|
|
228
|
-
const apy = midnightApyFromPrice(sellerAssets.div(units), ttmDays); // price = seller_assets / units
|
|
229
|
-
sumSeller = sumSeller.add(sellerAssets);
|
|
230
|
-
sumUnits = sumUnits.add(units);
|
|
231
|
-
weightedApy = weightedApy.add(sellerAssets.mul(apy));
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
const borrowRate = sumSeller.lte(0) ? '0' : weightedApy.div(sumSeller).toString();
|
|
235
|
-
const debtBase = assetAmountInEth(sumSeller.toFixed(0), loanTokenSymbol);
|
|
236
|
-
const debtTotal = assetAmountInEth(sumUnits.toFixed(0), loanTokenSymbol);
|
|
245
|
+
const res = await fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
246
|
+
const json: { data?: MidnightPosition[] } = await res.json();
|
|
247
|
+
const position = (json?.data || []).find((p) => p.type === 'borrow' && p.market_id?.toLowerCase() === marketId.toLowerCase());
|
|
248
|
+
|
|
249
|
+
const debtTotal = assetAmountInEth(position?.debt || '0', loanTokenSymbol);
|
|
250
|
+
const costBasisRaw = new Dec(position?.cost_basis || 0).div(WAD); // WAD-scaled → raw base units
|
|
251
|
+
const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
|
|
237
252
|
const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
|
|
253
|
+
const borrowRate = new Dec(position?.effective_rate_wad || 0).div(WAD).mul(100).toString();
|
|
238
254
|
|
|
239
255
|
return {
|
|
240
256
|
borrowRate, debtBase, debtInterest, debtTotal,
|
|
@@ -242,31 +258,37 @@ export const getMorphoMidnightUserBorrowInfo = async (
|
|
|
242
258
|
};
|
|
243
259
|
|
|
244
260
|
/**
|
|
245
|
-
*
|
|
246
|
-
* each price against time-to-maturity gives the rate a
|
|
247
|
-
* against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
261
|
+
* One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
|
|
262
|
+
* prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
|
|
263
|
+
* — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
|
|
264
|
+
*
|
|
265
|
+
* `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
|
|
266
|
+
* sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
|
|
267
|
+
* *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
|
|
248
268
|
*
|
|
249
|
-
* Returns `null` for an empty
|
|
250
|
-
*
|
|
251
|
-
*
|
|
269
|
+
* Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
|
|
270
|
+
* rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
|
|
271
|
+
* JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
|
|
252
272
|
*/
|
|
253
273
|
export const getMorphoMidnightMarketBook = async (
|
|
254
274
|
market: MorphoMidnightMarketData,
|
|
255
275
|
network: NetworkNumber,
|
|
276
|
+
side: MorphoMidnightBookSide = 'bids',
|
|
256
277
|
): Promise<MorphoMidnightParsedBook | null> => {
|
|
257
278
|
const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
|
|
258
279
|
const res = await fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
|
|
259
280
|
if (!res.ok) throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
|
|
260
281
|
|
|
261
|
-
const json: { data?:
|
|
282
|
+
const json: { data?: Partial<Record<MorphoMidnightBookSide, MidnightRawOffer[]>> } = await res.json();
|
|
262
283
|
const ttmDays = midnightTimeToMaturityDays(market.maturity);
|
|
284
|
+
const bestFirst = side === 'asks' ? -1 : 1;
|
|
263
285
|
|
|
264
|
-
const offers: MorphoMidnightBookOffer[] = (json?.data?.
|
|
265
|
-
.map((
|
|
266
|
-
rate: midnightApyFromPrice(new Dec(
|
|
267
|
-
liquidity: assetAmountInEth(
|
|
286
|
+
const offers: MorphoMidnightBookOffer[] = (json?.data?.[side] || [])
|
|
287
|
+
.map((offer) => ({
|
|
288
|
+
rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
|
|
289
|
+
liquidity: assetAmountInEth(offer.assets, loanSymbol),
|
|
268
290
|
}))
|
|
269
|
-
.sort((a, b) => new Dec(a.rate).minus(b.rate).toNumber());
|
|
291
|
+
.sort((a, b) => new Dec(a.rate).minus(b.rate).mul(bestFirst).toNumber());
|
|
270
292
|
|
|
271
293
|
if (offers.length === 0) return null;
|
|
272
294
|
|
|
@@ -286,6 +308,44 @@ const midnightQuoteError = (error?: MidnightApiError): string => {
|
|
|
286
308
|
return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
|
|
287
309
|
};
|
|
288
310
|
|
|
311
|
+
interface MidnightParsedQuote {
|
|
312
|
+
bestPrice: string, // loan-per-unit
|
|
313
|
+
worstPrice: string, // slippage-adjusted; below best on `bids`, above it on `asks`
|
|
314
|
+
availableAssets: string,
|
|
315
|
+
availableUnits: string,
|
|
316
|
+
takeableOffers: any[],
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* How much of the book to quote. The endpoint takes exactly one of the two — it rejects a request with
|
|
321
|
+
* neither ("Either assets or units must be provided") — and answers the same `takeable_offers` list either
|
|
322
|
+
* way, since that list is the whole in-band depth rather than the slice this size consumes.
|
|
323
|
+
*/
|
|
324
|
+
type MidnightQuoteSize = { assets: string } | { units: string };
|
|
325
|
+
|
|
326
|
+
// The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
|
|
327
|
+
const fetchMorphoMidnightQuote = async (
|
|
328
|
+
marketId: string,
|
|
329
|
+
side: MorphoMidnightBookSide,
|
|
330
|
+
size: MidnightQuoteSize,
|
|
331
|
+
slippagePercent: Dec.Value,
|
|
332
|
+
): Promise<MidnightParsedQuote> => {
|
|
333
|
+
const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
|
|
334
|
+
const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${midnightSlippageParam(slippagePercent)}`;
|
|
335
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
|
|
336
|
+
const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
|
|
337
|
+
const d = json?.data;
|
|
338
|
+
if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
|
|
342
|
+
worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
|
|
343
|
+
availableAssets: d.available_assets || '0',
|
|
344
|
+
availableUnits: d.available_units || '0',
|
|
345
|
+
takeableOffers: d.takeable_offers || [],
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
|
|
289
349
|
/**
|
|
290
350
|
* Quote a prospective borrow against the Midnight order book: the estimated rate, the debt units it adds,
|
|
291
351
|
* and the `maxUnits` cap sent on-chain to protect the user if better offers get filled first. `assetsRaw`
|
|
@@ -310,14 +370,8 @@ export const getMorphoMidnightBorrowQuote = async (
|
|
|
310
370
|
maturity: number,
|
|
311
371
|
maxBorrowRate?: Dec.Value,
|
|
312
372
|
): Promise<MorphoMidnightBorrowQuote> => {
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const json: { data?: MidnightQuoteResponse, error?: MidnightApiError } = await res.json();
|
|
316
|
-
const d = json?.data;
|
|
317
|
-
if (!d?.average_best_price) throw new Error(midnightQuoteError(json?.error));
|
|
318
|
-
|
|
319
|
-
const bestPrice = new Dec(d.average_best_price).div(WAD).toString();
|
|
320
|
-
const worstPrice = new Dec(d.average_worst_price || 0).div(WAD).toString();
|
|
373
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
|
|
374
|
+
const { bestPrice, worstPrice } = quote;
|
|
321
375
|
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
322
376
|
const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
323
377
|
|
|
@@ -331,14 +385,104 @@ export const getMorphoMidnightBorrowQuote = async (
|
|
|
331
385
|
const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
|
|
332
386
|
|
|
333
387
|
return {
|
|
334
|
-
|
|
335
|
-
worstPrice,
|
|
388
|
+
...quote,
|
|
336
389
|
estBorrowRate,
|
|
337
390
|
maxRate,
|
|
338
391
|
newUnits,
|
|
339
392
|
maxUnits,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Quote a prospective payback against the ask side of the Midnight order book: the rate the repayment
|
|
398
|
+
* retires debt at, the debt units it buys, and the `minUnits` floor sent on-chain to protect the user if
|
|
399
|
+
* the cheap offers get taken first. `assetsRaw` is the amount **spent** (raw loan-token base units) —
|
|
400
|
+
* matching `MidnightPaybackFromOrders`, whose `amount` is what leaves the wallet; the units bought, and
|
|
401
|
+
* therefore the debt retired, exceed it because a unit costs less than one loan token before maturity.
|
|
402
|
+
* Throws if the book can't fill the amount (caller handles).
|
|
403
|
+
*
|
|
404
|
+
* The mirror image of the borrow quote in every respect. A repayer wants a *high* rate, i.e. cheap units,
|
|
405
|
+
* so the guard is a floor rather than a ceiling:
|
|
406
|
+
* - `minPaybackRate` — an absolute APY floor, honoured **exactly** via `midnightPriceFromApy`. Prefer
|
|
407
|
+
* this when a user pins a min rate.
|
|
408
|
+
* - otherwise `slippagePercent`, the API's own price-level knob, whose APY effect is amplified by the
|
|
409
|
+
* annualisation factor near maturity. `minRate` reports what the floor actually permits.
|
|
410
|
+
*
|
|
411
|
+
* A `minPaybackRate` above `estPaybackRate` yields `minUnits > newUnits` — the payback would revert
|
|
412
|
+
* on-chain. Compare the two before submitting and tell the user their floor is over the market rate.
|
|
413
|
+
*/
|
|
414
|
+
export const getMorphoMidnightPaybackQuote = async (
|
|
415
|
+
marketId: string,
|
|
416
|
+
assetsRaw: string,
|
|
417
|
+
slippagePercent: Dec.Value,
|
|
418
|
+
maturity: number,
|
|
419
|
+
minPaybackRate?: Dec.Value,
|
|
420
|
+
): Promise<MorphoMidnightPaybackQuote> => {
|
|
421
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
|
|
422
|
+
const { bestPrice, worstPrice } = quote;
|
|
423
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
424
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
425
|
+
|
|
426
|
+
const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
|
|
427
|
+
? midnightPriceFromApy(minPaybackRate, ttmDays)
|
|
428
|
+
: worstPrice;
|
|
429
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
430
|
+
// Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
|
|
431
|
+
// `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
|
|
432
|
+
const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
433
|
+
const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
...quote,
|
|
437
|
+
estPaybackRate,
|
|
438
|
+
minRate,
|
|
439
|
+
newUnits,
|
|
440
|
+
minUnits,
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Payback quoted against a **units** target instead of a spend: "retire exactly these debt units, and tell
|
|
446
|
+
* me what that costs". The sibling of `getMorphoMidnightPaybackQuote` in every other respect — same ask
|
|
447
|
+
* side, same rate floor, same offer list — but with the two amounts swapped, so the on-chain guard becomes
|
|
448
|
+
* a ceiling on assets spent rather than a floor on units bought.
|
|
449
|
+
*
|
|
450
|
+
* This is what a **full close** must be sized with. Retiring N units costs less than N loan tokens before
|
|
451
|
+
* maturity, so quoting the close as a spend of the debt's face value asks the book for more depth than the
|
|
452
|
+
* close needs (and can be refused for liquidity that is in fact there), and caps the taker's spend at more
|
|
453
|
+
* than the position is worth. Callers that cannot refund an overspend — a taker calling the bundler
|
|
454
|
+
* directly, rather than through an action contract that sweeps the remainder back — need this quote.
|
|
455
|
+
*
|
|
456
|
+
* A `minPaybackRate` above `estPaybackRate` yields `maxAssets < newAssets`: the ceiling is under what the
|
|
457
|
+
* buy costs and it would revert on-chain, the mirror of the assets-target quote's `minUnits > newUnits`.
|
|
458
|
+
*/
|
|
459
|
+
export const getMorphoMidnightPaybackUnitsQuote = async (
|
|
460
|
+
marketId: string,
|
|
461
|
+
unitsRaw: string,
|
|
462
|
+
slippagePercent: Dec.Value,
|
|
463
|
+
maturity: number,
|
|
464
|
+
minPaybackRate?: Dec.Value,
|
|
465
|
+
): Promise<MorphoMidnightPaybackUnitsQuote> => {
|
|
466
|
+
const quote = await fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
|
|
467
|
+
const { bestPrice, worstPrice } = quote;
|
|
468
|
+
const ttmDays = midnightTimeToMaturityDays(maturity);
|
|
469
|
+
const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
|
|
470
|
+
|
|
471
|
+
const capPrice = minPaybackRate !== undefined && new Dec(minPaybackRate).gt(0)
|
|
472
|
+
? midnightPriceFromApy(minPaybackRate, ttmDays)
|
|
473
|
+
: worstPrice;
|
|
474
|
+
const minRate = midnightApyFromPrice(capPrice, ttmDays);
|
|
475
|
+
// Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
|
|
476
|
+
// what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
|
|
477
|
+
// buy short of the units it was asked for.
|
|
478
|
+
const newAssets = new Dec(unitsRaw).mul(bestPrice).toFixed(0, Dec.ROUND_UP);
|
|
479
|
+
const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
...quote,
|
|
483
|
+
estPaybackRate,
|
|
484
|
+
minRate,
|
|
485
|
+
newAssets,
|
|
486
|
+
maxAssets,
|
|
343
487
|
};
|
|
344
488
|
};
|
|
@@ -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,16 +70,20 @@ export interface MorphoMidnightMarketInfo {
|
|
|
70
70
|
assetsData: MorphoMidnightAssetsData,
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
//
|
|
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.
|
|
74
78
|
export interface MorphoMidnightBookOffer {
|
|
75
|
-
rate: string, // fixed
|
|
79
|
+
rate: string, // fixed APY, percent
|
|
76
80
|
liquidity: string, // loan-token amount available at this rate
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
export interface MorphoMidnightParsedBook {
|
|
80
|
-
bestRate: string, //
|
|
84
|
+
bestRate: string, // best rate for the taker of this side (= offers[0].rate)
|
|
81
85
|
totalLiquidity: string, // Σ offers[].liquidity, loan-token units
|
|
82
|
-
offers: MorphoMidnightBookOffer[], // ascending by rate
|
|
86
|
+
offers: MorphoMidnightBookOffer[], // best-first: bids ascending by rate, asks descending
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
export interface MorphoMidnightAggregatedPositionData {
|