@defisaver/positions-sdk 2.1.127-midnight-11-dev → 2.1.127-midnight-13-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.
@@ -64,6 +64,7 @@ export interface MorphoMidnightPaybackUnitsQuote {
64
64
  * Rounded **down**, since a wider slippage is a looser cap than the caller asked for.
65
65
  */
66
66
  export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
67
+ export declare const morphoMidnightMarketReportsBorrowInfo: (market: Pick<MorphoMidnightMarketData, "curator"> | string) => boolean;
67
68
  /**
68
69
  * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
69
70
  * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
@@ -74,6 +75,24 @@ export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => stri
74
75
  * The caller swallows errors — a missing rate must never block position rendering.
75
76
  */
76
77
  export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
78
+ /**
79
+ * The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
80
+ * interest off the indexer, this carries that same split forward through a payback, from a debt of
81
+ * `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
82
+ * so both scale by the same fraction.
83
+ *
84
+ * Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
85
+ * `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
86
+ * in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
87
+ * payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
88
+ * payback it goes negative on a position that still owes. Callers detect that window as
89
+ * `debtBase + debtInterest !== debt`.
90
+ *
91
+ * Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
92
+ * it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
93
+ * rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
94
+ */
95
+ export declare const scaleMorphoMidnightDebtSplit: ({ debtBase, debtInterest }: Pick<MorphoMidnightBorrowInfo, "debtBase" | "debtInterest">, borrowedBefore: Dec.Value, borrowedAfter: Dec.Value) => Pick<MorphoMidnightBorrowInfo, "debtBase" | "debtInterest">;
77
96
  /**
78
97
  * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
79
98
  * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
@@ -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.getMorphoMidnightPaybackUnitsQuote = exports.getMorphoMidnightPaybackQuote = exports.getMorphoMidnightBorrowQuote = exports.getMorphoMidnightMarketBook = exports.getMorphoMidnightUserBorrowInfo = exports.midnightSlippageParam = exports.getMorphoMidnightAggregatedPositionData = exports.tenorOfferToApiOffer = exports.tenorOfferFillToApiFill = exports.tenorBookRateToApyPercent = exports.tenorBookKeyFor = exports.MIDNIGHT_DEFAULT_RATE_SLIPPAGE = exports.midnightTimeToMaturityDays = exports.midnightPriceFromApy = exports.midnightBoundPrice = exports.midnightBookBestFirst = exports.midnightApyFromPrice = exports.buildMidnightParsedBook = void 0;
15
+ exports.getMorphoMidnightPaybackUnitsQuote = exports.getMorphoMidnightPaybackQuote = exports.getMorphoMidnightBorrowQuote = exports.getMorphoMidnightMarketBook = exports.scaleMorphoMidnightDebtSplit = exports.getMorphoMidnightUserBorrowInfo = exports.morphoMidnightMarketReportsBorrowInfo = exports.midnightSlippageParam = exports.getMorphoMidnightAggregatedPositionData = exports.tenorOfferToApiOffer = exports.tenorOfferFillToApiFill = exports.tenorBookRateToApyPercent = exports.tenorBookKeyFor = exports.MIDNIGHT_DEFAULT_RATE_SLIPPAGE = exports.midnightTimeToMaturityDays = exports.midnightPriceFromApy = exports.midnightBoundPrice = exports.midnightBookBestFirst = exports.midnightApyFromPrice = exports.buildMidnightParsedBook = 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");
@@ -54,6 +54,12 @@ const getMorphoMidnightAggregatedPositionData = ({ usedAssets, assetsData, marke
54
54
  // borrowLimit = Σ collateralUsd_i * lltv_i (per-collateral lltv carried on assetsData)
55
55
  payload.borrowLimitUsd = (0, moneymarket_1.getAssetsTotal)(usedAssets, ({ isSupplied, collateral }) => isSupplied && collateral, ({ symbol, suppliedUsd }) => { var _a; return new decimal_js_1.default(suppliedUsd).mul(((_a = assetsData[symbol]) === null || _a === void 0 ? void 0 : _a.lltv) || 0); });
56
56
  payload.liquidationLimitUsd = payload.borrowLimitUsd;
57
+ // Same subtraction every other money market uses, but it does NOT mean the same thing here. Elsewhere
58
+ // `borrowedUsd` is debt at present value, so the remainder is what a borrow would pay out. Midnight
59
+ // records debt at its face value at maturity, so this is face-value headroom: borrowing it would add
60
+ // more debt than the number says, by the market's discount. Anything surfacing this as "what you can
61
+ // borrow" has to scale it by the loan-per-unit price first (`midnightPriceFromApy` off a book rate) —
62
+ // a correction that grows with the term, past 7% on a one-year market.
57
63
  const leftToBorrowUsd = new decimal_js_1.default(payload.borrowLimitUsd).sub(payload.borrowedUsd);
58
64
  payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
59
65
  const loanTokenPrice = ((_a = assetsData[marketInfo.loanToken]) === null || _a === void 0 ? void 0 : _a.price) || '0';
@@ -117,6 +123,8 @@ const MIDNIGHT_SLIPPAGE_MAX = 100;
117
123
  */
118
124
  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();
119
125
  exports.midnightSlippageParam = midnightSlippageParam;
126
+ const morphoMidnightMarketReportsBorrowInfo = (market) => (!(0, morphoMidnight_1.isTenorMidnightMarket)(market));
127
+ exports.morphoMidnightMarketReportsBorrowInfo = morphoMidnightMarketReportsBorrowInfo;
120
128
  /**
121
129
  * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
122
130
  * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
@@ -140,6 +148,33 @@ const getMorphoMidnightUserBorrowInfo = (account, marketId, loanTokenSymbol) =>
140
148
  };
141
149
  });
142
150
  exports.getMorphoMidnightUserBorrowInfo = getMorphoMidnightUserBorrowInfo;
151
+ /**
152
+ * The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
153
+ * interest off the indexer, this carries that same split forward through a payback, from a debt of
154
+ * `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
155
+ * so both scale by the same fraction.
156
+ *
157
+ * Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
158
+ * `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
159
+ * in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
160
+ * payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
161
+ * payback it goes negative on a position that still owes. Callers detect that window as
162
+ * `debtBase + debtInterest !== debt`.
163
+ *
164
+ * Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
165
+ * it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
166
+ * rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
167
+ */
168
+ const scaleMorphoMidnightDebtSplit = ({ debtBase, debtInterest }, borrowedBefore, borrowedAfter) => {
169
+ const fractionRemaining = new decimal_js_1.default(borrowedBefore).lte(0)
170
+ ? new decimal_js_1.default(0)
171
+ : decimal_js_1.default.max(0, new decimal_js_1.default(borrowedAfter)).div(borrowedBefore);
172
+ return {
173
+ debtBase: new decimal_js_1.default(debtBase).mul(fractionRemaining).toString(),
174
+ debtInterest: new decimal_js_1.default(debtInterest).mul(fractionRemaining).toString(),
175
+ };
176
+ };
177
+ exports.scaleMorphoMidnightDebtSplit = scaleMorphoMidnightDebtSplit;
143
178
  /**
144
179
  * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
145
180
  * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
@@ -22,6 +22,7 @@ const decimal_js_1 = __importDefault(require("decimal.js"));
22
22
  const tokens_1 = require("@defisaver/tokens");
23
23
  const common_1 = require("../types/common");
24
24
  const contracts_1 = require("../contracts");
25
+ const types_1 = require("../types");
25
26
  const constants_1 = require("../constants");
26
27
  const staking_1 = require("../staking");
27
28
  const utils_1 = require("../services/utils");
@@ -165,21 +166,37 @@ function _getMorphoMidnightAccountData(provider, network, account, selectedMarke
165
166
  let borrowRate = '0';
166
167
  let debtBase = debt; // fallback: treat the full on-chain debt as principal until fill history is known
167
168
  let debtInterest = '0';
169
+ let borrowInfoStatus = types_1.MorphoMidnightBorrowInfoStatus.Available;
168
170
  let assetsDataForApy = marketInfo.assetsData;
169
171
  if (new decimal_js_1.default(positionInfo.debt.toString()).gt(0)) {
170
- try {
171
- const borrowInfo = yield (0, morphoMidnightHelpers_1.getMorphoMidnightUserBorrowInfo)(account, marketId, marketInfo.loanToken);
172
- if (new decimal_js_1.default(borrowInfo.debtTotal).gt(0)) {
173
- borrowRate = borrowInfo.borrowRate;
174
- debtBase = borrowInfo.debtBase;
175
- debtInterest = borrowInfo.debtInterest;
176
- usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
177
- // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
178
- assetsDataForApy = Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.loanToken]: Object.assign(Object.assign({}, loanTokenData), { borrowRate }) });
179
- }
172
+ if (!(0, morphoMidnightHelpers_1.morphoMidnightMarketReportsBorrowInfo)(selectedMarket)) {
173
+ borrowInfoStatus = types_1.MorphoMidnightBorrowInfoStatus.Unavailable;
180
174
  }
181
- catch (err) {
182
- // Orderbook API unavailable — keep the on-chain-only fallback above.
175
+ else {
176
+ try {
177
+ const borrowInfo = yield (0, morphoMidnightHelpers_1.getMorphoMidnightUserBorrowInfo)(account, marketId, marketInfo.loanToken);
178
+ if (new decimal_js_1.default(borrowInfo.debtTotal).gt(0)) {
179
+ borrowRate = borrowInfo.borrowRate;
180
+ debtBase = borrowInfo.debtBase;
181
+ debtInterest = borrowInfo.debtInterest;
182
+ usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
183
+ // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
184
+ assetsDataForApy = Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.loanToken]: Object.assign(Object.assign({}, loanTokenData), { borrowRate }) });
185
+ const describesChainDebt = new decimal_js_1.default(borrowInfo.debtTotal).eq(debt);
186
+ const ratePriced = new decimal_js_1.default(borrowRate).gt(0) || new decimal_js_1.default(debtInterest).lte(0);
187
+ borrowInfoStatus = describesChainDebt && ratePriced
188
+ ? types_1.MorphoMidnightBorrowInfoStatus.Available
189
+ : types_1.MorphoMidnightBorrowInfoStatus.Pending;
190
+ }
191
+ else {
192
+ // No entry for the position: the API does not know it yet, rather than it having no principal.
193
+ borrowInfoStatus = types_1.MorphoMidnightBorrowInfoStatus.Pending;
194
+ }
195
+ }
196
+ catch (err) {
197
+ // Positions API unreachable — the fallback above still renders, and the next call may succeed.
198
+ borrowInfoStatus = types_1.MorphoMidnightBorrowInfoStatus.Pending;
199
+ }
183
200
  }
184
201
  }
185
202
  return Object.assign({ usedAssets,
@@ -187,7 +204,8 @@ function _getMorphoMidnightAccountData(provider, network, account, selectedMarke
187
204
  debt,
188
205
  borrowRate,
189
206
  debtBase,
190
- debtInterest, maturity: marketInfo.maturity, isMatured: marketInfo.isMatured }, (0, morphoMidnightHelpers_1.getMorphoMidnightAggregatedPositionData)({ usedAssets, assetsData: assetsDataForApy, marketInfo }));
207
+ debtInterest,
208
+ borrowInfoStatus, maturity: marketInfo.maturity, isMatured: marketInfo.isMatured }, (0, morphoMidnightHelpers_1.getMorphoMidnightAggregatedPositionData)({ usedAssets, assetsData: assetsDataForApy, marketInfo }));
191
209
  });
192
210
  }
193
211
  function getMorphoMidnightAccountData(provider, network, account, selectedMarket, marketInfo) {
@@ -113,6 +113,24 @@ export interface MorphoMidnightAggregatedPositionData {
113
113
  collLiquidationRatio?: string;
114
114
  exposure: string;
115
115
  }
116
+ /**
117
+ * How much weight `borrowRate` / `debtBase` / `debtInterest` carry on a given position. They fall back to
118
+ * `'0'` / `debt` / `'0'`, which is indistinguishable from a real 0%-interest position, so anything
119
+ * displaying them has to read this to know whether it is looking at a number or at a placeholder.
120
+ *
121
+ * - `Available` — reported and reconciled against the on-chain debt (also lenders and debt-free positions,
122
+ * which have nothing to report).
123
+ * - `Pending` — the indexer has not caught up with the chain yet: it does not know the position, or its
124
+ * split describes a different debt. Refetching resolves it; `getMorphoMidnightUserBorrowInfo` failing
125
+ * outright lands here too, since the next call may well succeed.
126
+ * - `Unavailable` — no source covers this market, so refetching changes nothing. See
127
+ * `morphoMidnightMarketReportsBorrowInfo`.
128
+ */
129
+ export declare enum MorphoMidnightBorrowInfoStatus {
130
+ Available = "available",
131
+ Pending = "pending",
132
+ Unavailable = "unavailable"
133
+ }
116
134
  export interface MorphoMidnightPositionData extends MorphoMidnightAggregatedPositionData {
117
135
  usedAssets: MMUsedAssets;
118
136
  credit: string;
@@ -120,6 +138,7 @@ export interface MorphoMidnightPositionData extends MorphoMidnightAggregatedPosi
120
138
  borrowRate: string;
121
139
  debtBase: string;
122
140
  debtInterest: string;
141
+ borrowInfoStatus: MorphoMidnightBorrowInfoStatus;
123
142
  maturity: number;
124
143
  isMatured: boolean;
125
144
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MorphoMidnightVersions = void 0;
3
+ exports.MorphoMidnightBorrowInfoStatus = exports.MorphoMidnightVersions = void 0;
4
4
  var MorphoMidnightVersions;
5
5
  (function (MorphoMidnightVersions) {
6
6
  // BASE
@@ -29,3 +29,22 @@ var MorphoMidnightVersions;
29
29
  MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20261119_Base"] = "morphomidnighttenorcbethweth_20261119_base";
30
30
  MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20261217_Base"] = "morphomidnighttenorcbethweth_20261217_base";
31
31
  })(MorphoMidnightVersions || (exports.MorphoMidnightVersions = MorphoMidnightVersions = {}));
32
+ /**
33
+ * How much weight `borrowRate` / `debtBase` / `debtInterest` carry on a given position. They fall back to
34
+ * `'0'` / `debt` / `'0'`, which is indistinguishable from a real 0%-interest position, so anything
35
+ * displaying them has to read this to know whether it is looking at a number or at a placeholder.
36
+ *
37
+ * - `Available` — reported and reconciled against the on-chain debt (also lenders and debt-free positions,
38
+ * which have nothing to report).
39
+ * - `Pending` — the indexer has not caught up with the chain yet: it does not know the position, or its
40
+ * split describes a different debt. Refetching resolves it; `getMorphoMidnightUserBorrowInfo` failing
41
+ * outright lands here too, since the next call may well succeed.
42
+ * - `Unavailable` — no source covers this market, so refetching changes nothing. See
43
+ * `morphoMidnightMarketReportsBorrowInfo`.
44
+ */
45
+ var MorphoMidnightBorrowInfoStatus;
46
+ (function (MorphoMidnightBorrowInfoStatus) {
47
+ MorphoMidnightBorrowInfoStatus["Available"] = "available";
48
+ MorphoMidnightBorrowInfoStatus["Pending"] = "pending";
49
+ MorphoMidnightBorrowInfoStatus["Unavailable"] = "unavailable";
50
+ })(MorphoMidnightBorrowInfoStatus || (exports.MorphoMidnightBorrowInfoStatus = MorphoMidnightBorrowInfoStatus = {}));
@@ -64,6 +64,7 @@ export interface MorphoMidnightPaybackUnitsQuote {
64
64
  * Rounded **down**, since a wider slippage is a looser cap than the caller asked for.
65
65
  */
66
66
  export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
67
+ export declare const morphoMidnightMarketReportsBorrowInfo: (market: Pick<MorphoMidnightMarketData, "curator"> | string) => boolean;
67
68
  /**
68
69
  * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
69
70
  * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
@@ -74,6 +75,24 @@ export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => stri
74
75
  * The caller swallows errors — a missing rate must never block position rendering.
75
76
  */
76
77
  export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
78
+ /**
79
+ * The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
80
+ * interest off the indexer, this carries that same split forward through a payback, from a debt of
81
+ * `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
82
+ * so both scale by the same fraction.
83
+ *
84
+ * Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
85
+ * `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
86
+ * in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
87
+ * payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
88
+ * payback it goes negative on a position that still owes. Callers detect that window as
89
+ * `debtBase + debtInterest !== debt`.
90
+ *
91
+ * Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
92
+ * it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
93
+ * rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
94
+ */
95
+ export declare const scaleMorphoMidnightDebtSplit: ({ debtBase, debtInterest }: Pick<MorphoMidnightBorrowInfo, "debtBase" | "debtInterest">, borrowedBefore: Dec.Value, borrowedAfter: Dec.Value) => Pick<MorphoMidnightBorrowInfo, "debtBase" | "debtInterest">;
77
96
  /**
78
97
  * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
79
98
  * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
@@ -37,6 +37,12 @@ export const getMorphoMidnightAggregatedPositionData = ({ usedAssets, assetsData
37
37
  // borrowLimit = Σ collateralUsd_i * lltv_i (per-collateral lltv carried on assetsData)
38
38
  payload.borrowLimitUsd = getAssetsTotal(usedAssets, ({ isSupplied, collateral }) => isSupplied && collateral, ({ symbol, suppliedUsd }) => { var _a; return new Dec(suppliedUsd).mul(((_a = assetsData[symbol]) === null || _a === void 0 ? void 0 : _a.lltv) || 0); });
39
39
  payload.liquidationLimitUsd = payload.borrowLimitUsd;
40
+ // Same subtraction every other money market uses, but it does NOT mean the same thing here. Elsewhere
41
+ // `borrowedUsd` is debt at present value, so the remainder is what a borrow would pay out. Midnight
42
+ // records debt at its face value at maturity, so this is face-value headroom: borrowing it would add
43
+ // more debt than the number says, by the market's discount. Anything surfacing this as "what you can
44
+ // borrow" has to scale it by the loan-per-unit price first (`midnightPriceFromApy` off a book rate) —
45
+ // a correction that grows with the term, past 7% on a one-year market.
40
46
  const leftToBorrowUsd = new Dec(payload.borrowLimitUsd).sub(payload.borrowedUsd);
41
47
  payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
42
48
  const loanTokenPrice = ((_a = assetsData[marketInfo.loanToken]) === null || _a === void 0 ? void 0 : _a.price) || '0';
@@ -98,6 +104,7 @@ const MIDNIGHT_SLIPPAGE_MAX = 100;
98
104
  * Rounded **down**, since a wider slippage is a looser cap than the caller asked for.
99
105
  */
100
106
  export const midnightSlippageParam = (slippagePercent) => Dec.min(Dec.max(new Dec(slippagePercent), MIDNIGHT_SLIPPAGE_MIN), MIDNIGHT_SLIPPAGE_MAX).toDP(1, Dec.ROUND_DOWN).toString();
107
+ export const morphoMidnightMarketReportsBorrowInfo = (market) => (!isTenorMidnightMarket(market));
101
108
  /**
102
109
  * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
103
110
  * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
@@ -120,6 +127,32 @@ export const getMorphoMidnightUserBorrowInfo = (account, marketId, loanTokenSymb
120
127
  borrowRate, debtBase, debtInterest, debtTotal,
121
128
  };
122
129
  });
130
+ /**
131
+ * The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
132
+ * interest off the indexer, this carries that same split forward through a payback, from a debt of
133
+ * `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
134
+ * so both scale by the same fraction.
135
+ *
136
+ * Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
137
+ * `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
138
+ * in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
139
+ * payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
140
+ * payback it goes negative on a position that still owes. Callers detect that window as
141
+ * `debtBase + debtInterest !== debt`.
142
+ *
143
+ * Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
144
+ * it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
145
+ * rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
146
+ */
147
+ export const scaleMorphoMidnightDebtSplit = ({ debtBase, debtInterest }, borrowedBefore, borrowedAfter) => {
148
+ const fractionRemaining = new Dec(borrowedBefore).lte(0)
149
+ ? new Dec(0)
150
+ : Dec.max(0, new Dec(borrowedAfter)).div(borrowedBefore);
151
+ return {
152
+ debtBase: new Dec(debtBase).mul(fractionRemaining).toString(),
153
+ debtInterest: new Dec(debtInterest).mul(fractionRemaining).toString(),
154
+ };
155
+ };
123
156
  /**
124
157
  * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
125
158
  * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
@@ -11,10 +11,11 @@ import Dec from 'decimal.js';
11
11
  import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
12
12
  import { NetworkNumber, } from '../types/common';
13
13
  import { DFSFeedRegistryContractViem, FeedRegistryContractViem, MorphoMidnightViewContractViem, } from '../contracts';
14
+ import { MorphoMidnightBorrowInfoStatus, } from '../types';
14
15
  import { USD_QUOTE } from '../constants';
15
16
  import { calculateNetApy } from '../staking';
16
17
  import { isMainnetNetwork, wethToEth } from '../services/utils';
17
- import { getMorphoMidnightAggregatedPositionData, getMorphoMidnightUserBorrowInfo } from '../helpers/morphoMidnightHelpers';
18
+ import { getMorphoMidnightAggregatedPositionData, getMorphoMidnightUserBorrowInfo, morphoMidnightMarketReportsBorrowInfo } from '../helpers/morphoMidnightHelpers';
18
19
  import { getChainlinkAssetAddress } from '../services/priceService';
19
20
  import { getViemProvider, setViemBlockNumber } from '../services/viem';
20
21
  const HARDCODED_USD_STABLE_PRICE = '100000000'; // $1 with 8 decimals
@@ -154,21 +155,37 @@ export function _getMorphoMidnightAccountData(provider, network, account, select
154
155
  let borrowRate = '0';
155
156
  let debtBase = debt; // fallback: treat the full on-chain debt as principal until fill history is known
156
157
  let debtInterest = '0';
158
+ let borrowInfoStatus = MorphoMidnightBorrowInfoStatus.Available;
157
159
  let assetsDataForApy = marketInfo.assetsData;
158
160
  if (new Dec(positionInfo.debt.toString()).gt(0)) {
159
- try {
160
- const borrowInfo = yield getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
161
- if (new Dec(borrowInfo.debtTotal).gt(0)) {
162
- borrowRate = borrowInfo.borrowRate;
163
- debtBase = borrowInfo.debtBase;
164
- debtInterest = borrowInfo.debtInterest;
165
- usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
166
- // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
167
- assetsDataForApy = Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.loanToken]: Object.assign(Object.assign({}, loanTokenData), { borrowRate }) });
168
- }
161
+ if (!morphoMidnightMarketReportsBorrowInfo(selectedMarket)) {
162
+ borrowInfoStatus = MorphoMidnightBorrowInfoStatus.Unavailable;
169
163
  }
170
- catch (err) {
171
- // Orderbook API unavailable — keep the on-chain-only fallback above.
164
+ else {
165
+ try {
166
+ const borrowInfo = yield getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
167
+ if (new Dec(borrowInfo.debtTotal).gt(0)) {
168
+ borrowRate = borrowInfo.borrowRate;
169
+ debtBase = borrowInfo.debtBase;
170
+ debtInterest = borrowInfo.debtInterest;
171
+ usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
172
+ // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
173
+ assetsDataForApy = Object.assign(Object.assign({}, marketInfo.assetsData), { [marketInfo.loanToken]: Object.assign(Object.assign({}, loanTokenData), { borrowRate }) });
174
+ const describesChainDebt = new Dec(borrowInfo.debtTotal).eq(debt);
175
+ const ratePriced = new Dec(borrowRate).gt(0) || new Dec(debtInterest).lte(0);
176
+ borrowInfoStatus = describesChainDebt && ratePriced
177
+ ? MorphoMidnightBorrowInfoStatus.Available
178
+ : MorphoMidnightBorrowInfoStatus.Pending;
179
+ }
180
+ else {
181
+ // No entry for the position: the API does not know it yet, rather than it having no principal.
182
+ borrowInfoStatus = MorphoMidnightBorrowInfoStatus.Pending;
183
+ }
184
+ }
185
+ catch (err) {
186
+ // Positions API unreachable — the fallback above still renders, and the next call may succeed.
187
+ borrowInfoStatus = MorphoMidnightBorrowInfoStatus.Pending;
188
+ }
172
189
  }
173
190
  }
174
191
  return Object.assign({ usedAssets,
@@ -176,7 +193,8 @@ export function _getMorphoMidnightAccountData(provider, network, account, select
176
193
  debt,
177
194
  borrowRate,
178
195
  debtBase,
179
- debtInterest, maturity: marketInfo.maturity, isMatured: marketInfo.isMatured }, getMorphoMidnightAggregatedPositionData({ usedAssets, assetsData: assetsDataForApy, marketInfo }));
196
+ debtInterest,
197
+ borrowInfoStatus, maturity: marketInfo.maturity, isMatured: marketInfo.isMatured }, getMorphoMidnightAggregatedPositionData({ usedAssets, assetsData: assetsDataForApy, marketInfo }));
180
198
  });
181
199
  }
182
200
  export function getMorphoMidnightAccountData(provider, network, account, selectedMarket, marketInfo) {
@@ -113,6 +113,24 @@ export interface MorphoMidnightAggregatedPositionData {
113
113
  collLiquidationRatio?: string;
114
114
  exposure: string;
115
115
  }
116
+ /**
117
+ * How much weight `borrowRate` / `debtBase` / `debtInterest` carry on a given position. They fall back to
118
+ * `'0'` / `debt` / `'0'`, which is indistinguishable from a real 0%-interest position, so anything
119
+ * displaying them has to read this to know whether it is looking at a number or at a placeholder.
120
+ *
121
+ * - `Available` — reported and reconciled against the on-chain debt (also lenders and debt-free positions,
122
+ * which have nothing to report).
123
+ * - `Pending` — the indexer has not caught up with the chain yet: it does not know the position, or its
124
+ * split describes a different debt. Refetching resolves it; `getMorphoMidnightUserBorrowInfo` failing
125
+ * outright lands here too, since the next call may well succeed.
126
+ * - `Unavailable` — no source covers this market, so refetching changes nothing. See
127
+ * `morphoMidnightMarketReportsBorrowInfo`.
128
+ */
129
+ export declare enum MorphoMidnightBorrowInfoStatus {
130
+ Available = "available",
131
+ Pending = "pending",
132
+ Unavailable = "unavailable"
133
+ }
116
134
  export interface MorphoMidnightPositionData extends MorphoMidnightAggregatedPositionData {
117
135
  usedAssets: MMUsedAssets;
118
136
  credit: string;
@@ -120,6 +138,7 @@ export interface MorphoMidnightPositionData extends MorphoMidnightAggregatedPosi
120
138
  borrowRate: string;
121
139
  debtBase: string;
122
140
  debtInterest: string;
141
+ borrowInfoStatus: MorphoMidnightBorrowInfoStatus;
123
142
  maturity: number;
124
143
  isMatured: boolean;
125
144
  }
@@ -26,3 +26,22 @@ export var MorphoMidnightVersions;
26
26
  MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20261119_Base"] = "morphomidnighttenorcbethweth_20261119_base";
27
27
  MorphoMidnightVersions["MorphoMidnightTenorCbETHWETH_20261217_Base"] = "morphomidnighttenorcbethweth_20261217_base";
28
28
  })(MorphoMidnightVersions || (MorphoMidnightVersions = {}));
29
+ /**
30
+ * How much weight `borrowRate` / `debtBase` / `debtInterest` carry on a given position. They fall back to
31
+ * `'0'` / `debt` / `'0'`, which is indistinguishable from a real 0%-interest position, so anything
32
+ * displaying them has to read this to know whether it is looking at a number or at a placeholder.
33
+ *
34
+ * - `Available` — reported and reconciled against the on-chain debt (also lenders and debt-free positions,
35
+ * which have nothing to report).
36
+ * - `Pending` — the indexer has not caught up with the chain yet: it does not know the position, or its
37
+ * split describes a different debt. Refetching resolves it; `getMorphoMidnightUserBorrowInfo` failing
38
+ * outright lands here too, since the next call may well succeed.
39
+ * - `Unavailable` — no source covers this market, so refetching changes nothing. See
40
+ * `morphoMidnightMarketReportsBorrowInfo`.
41
+ */
42
+ export var MorphoMidnightBorrowInfoStatus;
43
+ (function (MorphoMidnightBorrowInfoStatus) {
44
+ MorphoMidnightBorrowInfoStatus["Available"] = "available";
45
+ MorphoMidnightBorrowInfoStatus["Pending"] = "pending";
46
+ MorphoMidnightBorrowInfoStatus["Unavailable"] = "unavailable";
47
+ })(MorphoMidnightBorrowInfoStatus || (MorphoMidnightBorrowInfoStatus = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defisaver/positions-sdk",
3
- "version": "2.1.127-midnight-11-dev",
3
+ "version": "2.1.127-midnight-13-dev",
4
4
  "description": "",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.js",
@@ -74,6 +74,12 @@ export const getMorphoMidnightAggregatedPositionData = ({
74
74
  );
75
75
  payload.liquidationLimitUsd = payload.borrowLimitUsd;
76
76
 
77
+ // Same subtraction every other money market uses, but it does NOT mean the same thing here. Elsewhere
78
+ // `borrowedUsd` is debt at present value, so the remainder is what a borrow would pay out. Midnight
79
+ // records debt at its face value at maturity, so this is face-value headroom: borrowing it would add
80
+ // more debt than the number says, by the market's discount. Anything surfacing this as "what you can
81
+ // borrow" has to scale it by the loan-per-unit price first (`midnightPriceFromApy` off a book rate) —
82
+ // a correction that grows with the term, past 7% on a one-year market.
77
83
  const leftToBorrowUsd = new Dec(payload.borrowLimitUsd).sub(payload.borrowedUsd);
78
84
  payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
79
85
 
@@ -221,6 +227,10 @@ export const midnightSlippageParam = (slippagePercent: Dec.Value): string => Dec
221
227
  MIDNIGHT_SLIPPAGE_MAX,
222
228
  ).toDP(1, Dec.ROUND_DOWN).toString();
223
229
 
230
+ export const morphoMidnightMarketReportsBorrowInfo = (market: Pick<MorphoMidnightMarketData, 'curator'> | string): boolean => (
231
+ !isTenorMidnightMarket(market)
232
+ );
233
+
224
234
  /**
225
235
  * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
226
236
  * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
@@ -250,6 +260,38 @@ export const getMorphoMidnightUserBorrowInfo = async (
250
260
  };
251
261
  };
252
262
 
263
+ /**
264
+ * The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
265
+ * interest off the indexer, this carries that same split forward through a payback, from a debt of
266
+ * `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
267
+ * so both scale by the same fraction.
268
+ *
269
+ * Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
270
+ * `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
271
+ * in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
272
+ * payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
273
+ * payback it goes negative on a position that still owes. Callers detect that window as
274
+ * `debtBase + debtInterest !== debt`.
275
+ *
276
+ * Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
277
+ * it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
278
+ * rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
279
+ */
280
+ export const scaleMorphoMidnightDebtSplit = (
281
+ { debtBase, debtInterest }: Pick<MorphoMidnightBorrowInfo, 'debtBase' | 'debtInterest'>,
282
+ borrowedBefore: Dec.Value,
283
+ borrowedAfter: Dec.Value,
284
+ ): Pick<MorphoMidnightBorrowInfo, 'debtBase' | 'debtInterest'> => {
285
+ const fractionRemaining = new Dec(borrowedBefore).lte(0)
286
+ ? new Dec(0)
287
+ : Dec.max(0, new Dec(borrowedAfter)).div(borrowedBefore);
288
+
289
+ return {
290
+ debtBase: new Dec(debtBase).mul(fractionRemaining).toString(),
291
+ debtInterest: new Dec(debtInterest).mul(fractionRemaining).toString(),
292
+ };
293
+ };
294
+
253
295
  /**
254
296
  * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
255
297
  * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
@@ -8,12 +8,12 @@ import {
8
8
  DFSFeedRegistryContractViem, FeedRegistryContractViem, MorphoMidnightViewContractViem,
9
9
  } from '../contracts';
10
10
  import {
11
- MorphoMidnightAssetsData, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightPositionData,
11
+ MorphoMidnightAssetsData, MorphoMidnightBorrowInfoStatus, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightPositionData,
12
12
  } from '../types';
13
13
  import { USD_QUOTE } from '../constants';
14
14
  import { calculateNetApy } from '../staking';
15
15
  import { isMainnetNetwork, wethToEth } from '../services/utils';
16
- import { getMorphoMidnightAggregatedPositionData, getMorphoMidnightUserBorrowInfo } from '../helpers/morphoMidnightHelpers';
16
+ import { getMorphoMidnightAggregatedPositionData, getMorphoMidnightUserBorrowInfo, morphoMidnightMarketReportsBorrowInfo } from '../helpers/morphoMidnightHelpers';
17
17
  import { getChainlinkAssetAddress } from '../services/priceService';
18
18
  import { getViemProvider, setViemBlockNumber } from '../services/viem';
19
19
 
@@ -163,23 +163,37 @@ export async function _getMorphoMidnightAccountData(provider: Client, network: N
163
163
  let borrowRate = '0';
164
164
  let debtBase = debt; // fallback: treat the full on-chain debt as principal until fill history is known
165
165
  let debtInterest = '0';
166
+ let borrowInfoStatus = MorphoMidnightBorrowInfoStatus.Available;
166
167
  let assetsDataForApy = marketInfo.assetsData;
167
168
  if (new Dec(positionInfo.debt.toString()).gt(0)) {
168
- try {
169
- const borrowInfo = await getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
170
- if (new Dec(borrowInfo.debtTotal).gt(0)) {
171
- borrowRate = borrowInfo.borrowRate;
172
- debtBase = borrowInfo.debtBase;
173
- debtInterest = borrowInfo.debtInterest;
174
- usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
175
- // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
176
- assetsDataForApy = {
177
- ...marketInfo.assetsData,
178
- [marketInfo.loanToken]: { ...loanTokenData, borrowRate },
179
- };
169
+ if (!morphoMidnightMarketReportsBorrowInfo(selectedMarket)) {
170
+ borrowInfoStatus = MorphoMidnightBorrowInfoStatus.Unavailable;
171
+ } else {
172
+ try {
173
+ const borrowInfo = await getMorphoMidnightUserBorrowInfo(account, marketId, marketInfo.loanToken);
174
+ if (new Dec(borrowInfo.debtTotal).gt(0)) {
175
+ borrowRate = borrowInfo.borrowRate;
176
+ debtBase = borrowInfo.debtBase;
177
+ debtInterest = borrowInfo.debtInterest;
178
+ usedAssets[marketInfo.loanToken].borrowRate = borrowRate;
179
+ // Reflect the real borrow cost in netApy without mutating the shared marketInfo.assetsData.
180
+ assetsDataForApy = {
181
+ ...marketInfo.assetsData,
182
+ [marketInfo.loanToken]: { ...loanTokenData, borrowRate },
183
+ };
184
+ const describesChainDebt = new Dec(borrowInfo.debtTotal).eq(debt);
185
+ const ratePriced = new Dec(borrowRate).gt(0) || new Dec(debtInterest).lte(0);
186
+ borrowInfoStatus = describesChainDebt && ratePriced
187
+ ? MorphoMidnightBorrowInfoStatus.Available
188
+ : MorphoMidnightBorrowInfoStatus.Pending;
189
+ } else {
190
+ // No entry for the position: the API does not know it yet, rather than it having no principal.
191
+ borrowInfoStatus = MorphoMidnightBorrowInfoStatus.Pending;
192
+ }
193
+ } catch (err) {
194
+ // Positions API unreachable — the fallback above still renders, and the next call may succeed.
195
+ borrowInfoStatus = MorphoMidnightBorrowInfoStatus.Pending;
180
196
  }
181
- } catch (err) {
182
- // Orderbook API unavailable — keep the on-chain-only fallback above.
183
197
  }
184
198
  }
185
199
 
@@ -190,6 +204,7 @@ export async function _getMorphoMidnightAccountData(provider: Client, network: N
190
204
  borrowRate,
191
205
  debtBase,
192
206
  debtInterest,
207
+ borrowInfoStatus,
193
208
  maturity: marketInfo.maturity,
194
209
  isMatured: marketInfo.isMatured,
195
210
  ...getMorphoMidnightAggregatedPositionData({ usedAssets, assetsData: assetsDataForApy, marketInfo }),
@@ -132,18 +132,35 @@ export interface MorphoMidnightAggregatedPositionData {
132
132
  exposure: string,
133
133
  }
134
134
 
135
+ /**
136
+ * How much weight `borrowRate` / `debtBase` / `debtInterest` carry on a given position. They fall back to
137
+ * `'0'` / `debt` / `'0'`, which is indistinguishable from a real 0%-interest position, so anything
138
+ * displaying them has to read this to know whether it is looking at a number or at a placeholder.
139
+ *
140
+ * - `Available` — reported and reconciled against the on-chain debt (also lenders and debt-free positions,
141
+ * which have nothing to report).
142
+ * - `Pending` — the indexer has not caught up with the chain yet: it does not know the position, or its
143
+ * split describes a different debt. Refetching resolves it; `getMorphoMidnightUserBorrowInfo` failing
144
+ * outright lands here too, since the next call may well succeed.
145
+ * - `Unavailable` — no source covers this market, so refetching changes nothing. See
146
+ * `morphoMidnightMarketReportsBorrowInfo`.
147
+ */
148
+ export enum MorphoMidnightBorrowInfoStatus {
149
+ Available = 'available',
150
+ Pending = 'pending',
151
+ Unavailable = 'unavailable',
152
+ }
153
+
135
154
  // Fixed-rate/YTM (derived from entry price + orderbook) is intentionally absent in MVP:
136
155
  // MidnightView exposes no per-position rate, so a variable-MM-style APY would be misleading.
137
156
  export interface MorphoMidnightPositionData extends MorphoMidnightAggregatedPositionData {
138
157
  usedAssets: MMUsedAssets,
139
158
  credit: string, // lender credit units, face value at maturity (with interest); 0 for borrowers
140
159
  debt: string, // borrower debt, face value at maturity (with interest); 0 for lenders
141
- // Borrow rate + base/interest split are orderbook-derived off-chain (from the Midnight transactions API):
142
- // MidnightView only stores `debt` (= face value at maturity), so principal-vs-interest and the effective
143
- // rate are computed from the fill history. Default to '0'/`debt`/'0' for lenders or when the API is unavailable.
144
160
  borrowRate: string, // weighted-average borrow APY as a percent
145
161
  debtBase: string, // base borrowed (principal), loan-token units
146
162
  debtInterest: string, // debt − debtBase (fixed interest owed at maturity), loan-token units
163
+ borrowInfoStatus: MorphoMidnightBorrowInfoStatus,
147
164
  maturity: number,
148
165
  isMatured: boolean,
149
166
  }