@defisaver/positions-sdk 2.1.144-dev → 2.1.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/cjs/config/contracts.d.ts +249 -0
  2. package/cjs/config/contracts.js +11 -1
  3. package/cjs/constants/index.d.ts +1 -0
  4. package/cjs/constants/index.js +2 -1
  5. package/cjs/contracts.d.ts +1510 -0
  6. package/cjs/contracts.js +3 -2
  7. package/cjs/fluid/index.js +6 -2
  8. package/cjs/helpers/fluidHelpers/index.js +5 -4
  9. package/cjs/helpers/index.d.ts +1 -0
  10. package/cjs/helpers/index.js +2 -1
  11. package/cjs/helpers/morphoMidnightHelpers/headroom.d.ts +48 -0
  12. package/cjs/helpers/morphoMidnightHelpers/headroom.js +52 -0
  13. package/cjs/helpers/morphoMidnightHelpers/index.d.ts +113 -0
  14. package/cjs/helpers/morphoMidnightHelpers/index.js +298 -0
  15. package/cjs/helpers/morphoMidnightHelpers/rate.d.ts +15 -0
  16. package/cjs/helpers/morphoMidnightHelpers/rate.js +62 -0
  17. package/cjs/helpers/morphoMidnightHelpers/tenor.d.ts +129 -0
  18. package/cjs/helpers/morphoMidnightHelpers/tenor.js +254 -0
  19. package/cjs/index.d.ts +2 -1
  20. package/cjs/index.js +3 -1
  21. package/cjs/markets/index.d.ts +1 -0
  22. package/cjs/markets/index.js +6 -1
  23. package/cjs/markets/morphoMidnight/index.d.ts +60 -0
  24. package/cjs/markets/morphoMidnight/index.js +659 -0
  25. package/cjs/morphoMidnight/index.d.ts +14 -0
  26. package/cjs/morphoMidnight/index.js +261 -0
  27. package/cjs/portfolio/discovery.js +4 -0
  28. package/cjs/portfolio/index.js +44 -0
  29. package/cjs/services/viem.d.ts +11 -11
  30. package/cjs/staking/eligibility.d.ts +15 -1
  31. package/cjs/staking/eligibility.js +12 -1
  32. package/cjs/staking/staking.js +18 -22
  33. package/cjs/types/index.d.ts +1 -0
  34. package/cjs/types/index.js +1 -0
  35. package/cjs/types/morphoMidnight.d.ts +141 -0
  36. package/cjs/types/morphoMidnight.js +47 -0
  37. package/cjs/types/portfolio.d.ts +4 -0
  38. package/esm/config/contracts.d.ts +249 -0
  39. package/esm/config/contracts.js +9 -0
  40. package/esm/constants/index.d.ts +1 -0
  41. package/esm/constants/index.js +1 -0
  42. package/esm/contracts.d.ts +1510 -0
  43. package/esm/contracts.js +1 -0
  44. package/esm/fluid/index.js +6 -2
  45. package/esm/helpers/fluidHelpers/index.js +6 -5
  46. package/esm/helpers/index.d.ts +1 -0
  47. package/esm/helpers/index.js +1 -0
  48. package/esm/helpers/morphoMidnightHelpers/headroom.d.ts +48 -0
  49. package/esm/helpers/morphoMidnightHelpers/headroom.js +45 -0
  50. package/esm/helpers/morphoMidnightHelpers/index.d.ts +113 -0
  51. package/esm/helpers/morphoMidnightHelpers/index.js +271 -0
  52. package/esm/helpers/morphoMidnightHelpers/rate.d.ts +15 -0
  53. package/esm/helpers/morphoMidnightHelpers/rate.js +50 -0
  54. package/esm/helpers/morphoMidnightHelpers/tenor.d.ts +129 -0
  55. package/esm/helpers/morphoMidnightHelpers/tenor.js +238 -0
  56. package/esm/index.d.ts +2 -1
  57. package/esm/index.js +2 -1
  58. package/esm/markets/index.d.ts +1 -0
  59. package/esm/markets/index.js +1 -0
  60. package/esm/markets/morphoMidnight/index.d.ts +60 -0
  61. package/esm/markets/morphoMidnight/index.js +631 -0
  62. package/esm/morphoMidnight/index.d.ts +14 -0
  63. package/esm/morphoMidnight/index.js +248 -0
  64. package/esm/portfolio/discovery.js +5 -1
  65. package/esm/portfolio/index.js +45 -1
  66. package/esm/services/viem.d.ts +11 -11
  67. package/esm/staking/eligibility.d.ts +15 -1
  68. package/esm/staking/eligibility.js +10 -0
  69. package/esm/staking/staking.js +19 -23
  70. package/esm/types/index.d.ts +1 -0
  71. package/esm/types/index.js +1 -0
  72. package/esm/types/morphoMidnight.d.ts +141 -0
  73. package/esm/types/morphoMidnight.js +44 -0
  74. package/esm/types/portfolio.d.ts +4 -0
  75. package/package.json +1 -1
  76. package/src/config/contracts.ts +9 -0
  77. package/src/constants/index.ts +1 -0
  78. package/src/contracts.ts +1 -0
  79. package/src/fluid/index.ts +6 -2
  80. package/src/helpers/fluidHelpers/index.ts +6 -5
  81. package/src/helpers/index.ts +1 -0
  82. package/src/helpers/morphoMidnightHelpers/headroom.ts +82 -0
  83. package/src/helpers/morphoMidnightHelpers/index.ts +485 -0
  84. package/src/helpers/morphoMidnightHelpers/rate.ts +71 -0
  85. package/src/helpers/morphoMidnightHelpers/tenor.ts +407 -0
  86. package/src/index.ts +2 -0
  87. package/src/markets/index.ts +3 -0
  88. package/src/markets/morphoMidnight/index.ts +675 -0
  89. package/src/morphoMidnight/index.ts +276 -0
  90. package/src/portfolio/discovery.ts +6 -0
  91. package/src/portfolio/index.ts +44 -0
  92. package/src/staking/eligibility.ts +29 -2
  93. package/src/staking/staking.ts +20 -22
  94. package/src/types/index.ts +1 -0
  95. package/src/types/morphoMidnight.ts +163 -0
  96. package/src/types/portfolio.ts +4 -0
package/esm/contracts.js CHANGED
@@ -94,6 +94,7 @@ export const getYearnV3VaultContractViem = (client, address) => {
94
94
  });
95
95
  };
96
96
  export const MorphoBlueViewContractViem = createViemContractFromConfigFunc('MorphoBlueView');
97
+ export const MorphoMidnightViewContractViem = createViemContractFromConfigFunc('MidnightView');
97
98
  export const AaveLoanInfoV2ContractViem = createViemContractFromConfigFunc('AaveLoanInfoV2');
98
99
  export const AaveV3ViewContractViem = createViemContractFromConfigFunc('AaveV3View');
99
100
  export const AaveIncentiveDataProviderV3ContractViem = createViemContractFromConfigFunc('AaveUiIncentiveDataProviderV3');
@@ -1256,8 +1256,12 @@ export const _getAllUserEarnPositionsWithFTokens = (provider, network, user) =>
1256
1256
  export const getAllUserEarnPositionsWithFTokens = (provider, network, user) => __awaiter(void 0, void 0, void 0, function* () { return _getAllUserEarnPositionsWithFTokens(getViemProvider(provider, network), network, user); });
1257
1257
  export const _getUserPositions = (provider, network, user) => __awaiter(void 0, void 0, void 0, function* () {
1258
1258
  const view = FluidViewContractViem(provider, network);
1259
- const data = yield view.read.getUserPositions([user]);
1260
- const parsedMarketData = (yield Promise.all(data[1].map((vaultData) => __awaiter(void 0, void 0, void 0, function* () { return parseMarketData(provider, vaultData, network); }))));
1259
+ const [data, merklCampaigns] = yield Promise.all([
1260
+ view.read.getUserPositions([user]),
1261
+ getFluidMerklCampaigns(network),
1262
+ ]);
1263
+ const parsedMarketData = (yield Promise.all(data[1].map((vaultData) => __awaiter(void 0, void 0, void 0, function* () { return parseMarketData(provider, vaultData, network); }))))
1264
+ .map(marketData => (marketData ? attachFluidMerklIncentives(marketData, merklCampaigns) : marketData));
1261
1265
  const userData = data[0].map((position, i) => (parsedMarketData[i] && Object.assign({}, parseUserData(position, parsedMarketData[i]))));
1262
1266
  return parsedMarketData.map((market, i) => ({
1263
1267
  marketData: market,
@@ -2,7 +2,7 @@ import Dec from 'decimal.js';
2
2
  import { assetAmountInEth } from '@defisaver/tokens';
3
3
  import { FluidVaultType, IncentiveSource, } from '../../types';
4
4
  import { calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos, } from '../../moneymarket';
5
- import { calculateInterestEarned, calculateNetApy, EligibilityMapping } from '../../staking';
5
+ import { calculateInterestEarned, calculateNetApy, getIncentiveEligibility } from '../../staking';
6
6
  import { IncentiveSide, LeverageType } from '../../types/common';
7
7
  import { getEthAmountForDecimals } from '../../services/utils';
8
8
  export const getUniqueFluidMerklIncentives = (assetsData, side) => {
@@ -16,10 +16,11 @@ export const getUniqueFluidMerklIncentives = (assetsData, side) => {
16
16
  ])).values()];
17
17
  };
18
18
  const getMerklIncentiveInterest = (incentives, principal, usedAssets) => incentives.reduce((total, { apy, eligibilityId }) => {
19
- const eligibilityCheck = eligibilityId ? EligibilityMapping[eligibilityId] : null;
20
- const { isEligible, eligibleUSDAmount } = eligibilityCheck
21
- ? eligibilityCheck(usedAssets, undefined)
22
- : { isEligible: true, eligibleUSDAmount: principal };
19
+ const { isEligible, eligibleUSDAmount } = getIncentiveEligibility({
20
+ incentive: { eligibilityId },
21
+ usedAssets,
22
+ defaultEligibleUSDAmount: principal,
23
+ });
23
24
  const incentiveInterest = isEligible ? calculateInterestEarned(eligibleUSDAmount, apy, 'year', true) : 0;
24
25
  return new Dec(total).add(incentiveInterest).toString();
25
26
  }, '0');
@@ -4,6 +4,7 @@ export * as sparkHelpers from './sparkHelpers';
4
4
  export * as curveUsdHelpers from './curveUsdHelpers';
5
5
  export * as makerHelpers from './makerHelpers';
6
6
  export * as morphoBlueHelpers from './morphoBlueHelpers';
7
+ export * as morphoMidnightHelpers from './morphoMidnightHelpers';
7
8
  export * as llamaLendHelpers from './llamaLendHelpers';
8
9
  export * as liquityV2Helpers from './liquityV2Helpers';
9
10
  export * as fluidHelpers from './fluidHelpers';
@@ -4,6 +4,7 @@ export * as sparkHelpers from './sparkHelpers';
4
4
  export * as curveUsdHelpers from './curveUsdHelpers';
5
5
  export * as makerHelpers from './makerHelpers';
6
6
  export * as morphoBlueHelpers from './morphoBlueHelpers';
7
+ export * as morphoMidnightHelpers from './morphoMidnightHelpers';
7
8
  export * as llamaLendHelpers from './llamaLendHelpers';
8
9
  export * as liquityV2Helpers from './liquityV2Helpers';
9
10
  export * as fluidHelpers from './fluidHelpers';
@@ -0,0 +1,48 @@
1
+ import Dec from 'decimal.js';
2
+ /**
3
+ * Safety margin every Midnight position is held at. On Midnight the lltv is *both* the borrow cap and the
4
+ * liquidation threshold (`liquidationLimitUsd === borrowLimitUsd` in the aggregate), so a position borrowed
5
+ * to its full limit opens at a health ratio of exactly 1 — immediately liquidatable. This is what holds it
6
+ * off that line, and what absorbs swap price impact on a boost.
7
+ */
8
+ export declare const MIDNIGHT_MIN_HEALTH_RATIO = 1.025;
9
+ export interface MidnightBorrowHeadroomParams {
10
+ borrowLimitUsd: Dec.Value;
11
+ borrowedUsd: Dec.Value;
12
+ /** Principal a unit of borrow power is worth (loan-per-unit price, ≤ 1), off a book rate. */
13
+ principalPerUnit?: Dec.Value;
14
+ /** Depth the book can fill, in loan tokens, from the same quote. Omitted leaves the answer uncapped. */
15
+ availableAssets?: Dec.Value;
16
+ loanTokenPrice?: Dec.Value;
17
+ /** Loan token decimals. Omitted skips the quantisation, which only a caller ignoring `leftToBorrow` wants. */
18
+ loanTokenDecimals?: number;
19
+ isMatured?: boolean;
20
+ minHealthRatio?: Dec.Value;
21
+ }
22
+ export interface MidnightBorrowHeadroom {
23
+ /** Face-value headroom, before the order-book discount. What a *boost* solve scales itself. */
24
+ headroomUsd: string;
25
+ /** The borrowable amount in loan tokens — the Borrow action's Max, quantised exactly as it is typed in. */
26
+ leftToBorrow: string;
27
+ /** `leftToBorrow · price`. Derived FROM the token amount, never computed alongside it — see below. */
28
+ leftToBorrowUsd: string;
29
+ borrowPowerUsed: string;
30
+ }
31
+ /**
32
+ * The one answer to "how much more can this position borrow". Every surface that sizes a Midnight borrow
33
+ * derives from here — the max-borrow getter, the position overview's headroom rows, and the max-boost
34
+ * solve, which scales `headroomUsd` by its own leverage factors — so a max and the figure shown next to it
35
+ * cannot drift into two different formulas. They did: the overview used to price
36
+ * the whole headroom off the top-of-book offer (often dust) with no rate pad and no depth cap, while the
37
+ * max walked the book for its real size, which is what put the two 1–2% apart.
38
+ *
39
+ * Three corrections separate this from a plain `limit − debt`, and dropping any one overstates the answer:
40
+ * `minHealthRatio` reserves the margin, `principalPerUnit` turns face-value headroom into the principal a
41
+ * borrow actually receives (a correction that grows with the term, past 7% on a one-year market), and
42
+ * `availableAssets` caps it at what the book can currently fill.
43
+ *
44
+ * `leftToBorrowUsd` is deliberately `leftToBorrow · price` rather than the USD figure the token amount was
45
+ * derived from: the two differ by the round-down to token decimals, and a UI showing one next to an input
46
+ * holding the other would be reporting a difference the user cannot spend. Everything quantises once, here.
47
+ */
48
+ export declare const getMorphoMidnightBorrowHeadroom: ({ borrowLimitUsd, borrowedUsd, principalPerUnit, availableAssets, loanTokenPrice, loanTokenDecimals, isMatured, minHealthRatio, }: MidnightBorrowHeadroomParams) => MidnightBorrowHeadroom;
@@ -0,0 +1,45 @@
1
+ import Dec from 'decimal.js';
2
+ /**
3
+ * Safety margin every Midnight position is held at. On Midnight the lltv is *both* the borrow cap and the
4
+ * liquidation threshold (`liquidationLimitUsd === borrowLimitUsd` in the aggregate), so a position borrowed
5
+ * to its full limit opens at a health ratio of exactly 1 — immediately liquidatable. This is what holds it
6
+ * off that line, and what absorbs swap price impact on a boost.
7
+ */
8
+ export const MIDNIGHT_MIN_HEALTH_RATIO = 1.025;
9
+ /**
10
+ * The one answer to "how much more can this position borrow". Every surface that sizes a Midnight borrow
11
+ * derives from here — the max-borrow getter, the position overview's headroom rows, and the max-boost
12
+ * solve, which scales `headroomUsd` by its own leverage factors — so a max and the figure shown next to it
13
+ * cannot drift into two different formulas. They did: the overview used to price
14
+ * the whole headroom off the top-of-book offer (often dust) with no rate pad and no depth cap, while the
15
+ * max walked the book for its real size, which is what put the two 1–2% apart.
16
+ *
17
+ * Three corrections separate this from a plain `limit − debt`, and dropping any one overstates the answer:
18
+ * `minHealthRatio` reserves the margin, `principalPerUnit` turns face-value headroom into the principal a
19
+ * borrow actually receives (a correction that grows with the term, past 7% on a one-year market), and
20
+ * `availableAssets` caps it at what the book can currently fill.
21
+ *
22
+ * `leftToBorrowUsd` is deliberately `leftToBorrow · price` rather than the USD figure the token amount was
23
+ * derived from: the two differ by the round-down to token decimals, and a UI showing one next to an input
24
+ * holding the other would be reporting a difference the user cannot spend. Everything quantises once, here.
25
+ */
26
+ export const getMorphoMidnightBorrowHeadroom = ({ borrowLimitUsd, borrowedUsd, principalPerUnit = 1, availableAssets, loanTokenPrice = 0, loanTokenDecimals, isMatured = false, minHealthRatio = MIDNIGHT_MIN_HEALTH_RATIO, }) => {
27
+ const usableLimitUsd = new Dec(borrowLimitUsd).div(minHealthRatio);
28
+ const headroomUsd = isMatured ? new Dec(0) : Dec.max(0, usableLimitUsd.sub(borrowedUsd));
29
+ const price = new Dec(loanTokenPrice);
30
+ // Capped in loan tokens rather than in USD: `availableAssets` is quoted in tokens, and converting it to
31
+ // USD only to divide the result back out again would re-round the one number that has to land exactly.
32
+ const payout = price.lte(0) ? new Dec(0) : headroomUsd.mul(principalPerUnit).div(price);
33
+ const capped = availableAssets === undefined ? payout : Dec.min(payout, availableAssets);
34
+ // Round down: rounding the last decimal up would eat into the margin we just reserved.
35
+ const leftToBorrow = loanTokenDecimals === undefined ? capped : capped.toDP(loanTokenDecimals, Dec.ROUND_DOWN);
36
+ return {
37
+ headroomUsd: headroomUsd.toString(),
38
+ leftToBorrow: leftToBorrow.toString(),
39
+ leftToBorrowUsd: leftToBorrow.mul(price).toString(),
40
+ // Measured against the margined limit rather than the raw one, so it reads 100% at max borrow instead
41
+ // of 97.56%, and can exceed 100% inside the reserved band. Deliberately left uncapped by book depth:
42
+ // how much a position has borrowed is a property of the position, not of what the book can fill today.
43
+ borrowPowerUsed: usableLimitUsd.lte(0) ? '0' : new Dec(borrowedUsd).div(usableLimitUsd).mul(100).toString(),
44
+ };
45
+ };
@@ -0,0 +1,113 @@
1
+ import Dec from 'decimal.js';
2
+ import { MMUsedAssets, NetworkNumber } from '../../types/common';
3
+ import { MorphoMidnightAggregatedPositionData, MorphoMidnightAssetsData, MorphoMidnightBookSide, MorphoMidnightMarketData, MorphoMidnightMarketInfo, MorphoMidnightParsedBook } from '../../types';
4
+ export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightBoundPrice, midnightPriceFromApy, midnightTimeToMaturityDays, MIDNIGHT_DEFAULT_RATE_SLIPPAGE, } from './rate';
5
+ export { getMorphoMidnightBorrowHeadroom, MIDNIGHT_MIN_HEALTH_RATIO, } from './headroom';
6
+ export type { MidnightBorrowHeadroom, MidnightBorrowHeadroomParams } from './headroom';
7
+ export { tenorBookKeyFor, tenorBookRateToApyPercent, tenorOfferFillToApiFill, tenorOfferToApiOffer, } from './tenor';
8
+ /**
9
+ * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
10
+ * the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
11
+ *
12
+ * Note on amounts: `borrowedUsd` is derived from the position's `debt`, which is the face value owed at
13
+ * maturity (principal + fixed interest). Health is therefore measured against the full maturity debt,
14
+ * matching how MidnightView computes `ratio`. Fixed-rate APY is not derived on-chain in MVP, so
15
+ * `netApy` reflects the `'0'` rates in `assetsData` (see the module getter).
16
+ */
17
+ export declare const getMorphoMidnightAggregatedPositionData: ({ usedAssets, assetsData, marketInfo, }: {
18
+ usedAssets: MMUsedAssets;
19
+ assetsData: MorphoMidnightAssetsData;
20
+ marketInfo: MorphoMidnightMarketInfo;
21
+ }) => MorphoMidnightAggregatedPositionData;
22
+ export interface MorphoMidnightBorrowInfo {
23
+ borrowRate: string;
24
+ debtBase: string;
25
+ debtInterest: string;
26
+ debtTotal: string;
27
+ }
28
+ export interface MorphoMidnightBorrowQuote {
29
+ bestPrice: string;
30
+ worstPrice: string;
31
+ estBorrowRate: string;
32
+ maxRate: string;
33
+ newUnits: string;
34
+ maxUnits: string;
35
+ availableAssets: string;
36
+ availableUnits: string;
37
+ takeableOffers: any[];
38
+ }
39
+ export interface MorphoMidnightPaybackQuote {
40
+ bestPrice: string;
41
+ worstPrice: string;
42
+ estPaybackRate: string;
43
+ minRate: string;
44
+ newUnits: string;
45
+ minUnits: string;
46
+ availableAssets: string;
47
+ availableUnits: string;
48
+ takeableOffers: any[];
49
+ }
50
+ export interface MorphoMidnightPaybackUnitsQuote {
51
+ bestPrice: string;
52
+ worstPrice: string;
53
+ estPaybackRate: string;
54
+ minRate: string;
55
+ newAssets: string;
56
+ maxAssets: string;
57
+ availableAssets: string;
58
+ availableUnits: string;
59
+ takeableOffers: any[];
60
+ }
61
+ /**
62
+ * Coerce a slippage into what the quote endpoint accepts: 0.1–100 with at most one decimal place. The
63
+ * validation is lexical, so a computed value (`4.15066671050631467`) is rejected outright — without this
64
+ * the request 400s and the quote looks unavailable.
65
+ *
66
+ * Rounded **down**, since a wider slippage is a looser cap than the caller asked for.
67
+ */
68
+ export declare const midnightSlippageParam: (slippagePercent: Dec.Value) => string;
69
+ /**
70
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
71
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
72
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
73
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
74
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
75
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
76
+ * The caller swallows errors — a missing rate must never block position rendering.
77
+ */
78
+ export declare const getMorphoMidnightUserBorrowInfo: (account: string, marketId: string, loanTokenSymbol: string) => Promise<MorphoMidnightBorrowInfo>;
79
+ /**
80
+ * The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
81
+ * interest off the indexer, this carries that same split forward through a payback, from a debt of
82
+ * `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
83
+ * so both scale by the same fraction.
84
+ *
85
+ * Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
86
+ * `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
87
+ * in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
88
+ * payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
89
+ * payback it goes negative on a position that still owes. Callers detect that window as
90
+ * `debtBase + debtInterest !== debt`.
91
+ *
92
+ * Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
93
+ * it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
94
+ * rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
95
+ */
96
+ export declare const scaleMorphoMidnightDebtSplit: ({ debtBase, debtInterest }: Pick<MorphoMidnightBorrowInfo, "debtBase" | "debtInterest">, borrowedBefore: Dec.Value, borrowedAfter: Dec.Value) => Pick<MorphoMidnightBorrowInfo, "debtBase" | "debtInterest">;
97
+ /**
98
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
99
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
100
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
101
+ *
102
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
103
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
104
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
105
+ *
106
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
107
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
108
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
109
+ */
110
+ export declare const getMorphoMidnightMarketBook: (market: MorphoMidnightMarketData, network: NetworkNumber, side?: MorphoMidnightBookSide) => Promise<MorphoMidnightParsedBook | null>;
111
+ export declare const getMorphoMidnightBorrowQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, maxBorrowRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value) => Promise<MorphoMidnightBorrowQuote>;
112
+ export declare const getMorphoMidnightPaybackQuote: (marketId: string, assetsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value) => Promise<MorphoMidnightPaybackQuote>;
113
+ export declare const getMorphoMidnightPaybackUnitsQuote: (marketId: string, unitsRaw: string, slippagePercent: Dec.Value, maturity: number, minPaybackRate?: Dec.Value, taker?: string, rateSlippagePercent?: Dec.Value) => Promise<MorphoMidnightPaybackUnitsQuote>;
@@ -0,0 +1,271 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import Dec from 'decimal.js';
11
+ import { assetAmountInEth, getAssetInfoByAddress } from '@defisaver/tokens';
12
+ import { calcLeverageLiqPrice, getAssetsTotal, getExposure, isLeveragedPos, } from '../../moneymarket';
13
+ import { calculateNetApy } from '../../staking';
14
+ import { LeverageType, } from '../../types/common';
15
+ import { WAD } from '../../constants';
16
+ import { LONGER_TIMEOUT } from '../../services/utils';
17
+ import { isTenorMidnightMarket } from '../../markets/morphoMidnight';
18
+ import { buildMidnightParsedBook, midnightApyFromPrice, midnightBoundPrice, midnightTimeToMaturityDays, } from './rate';
19
+ import { getTenorBorrowQuote, getTenorMarketBook, getTenorPaybackQuote, getTenorPaybackUnitsQuote, } from './tenor';
20
+ export { buildMidnightParsedBook, midnightApyFromPrice, midnightBookBestFirst, midnightBoundPrice, midnightPriceFromApy, midnightTimeToMaturityDays, MIDNIGHT_DEFAULT_RATE_SLIPPAGE, } from './rate';
21
+ export { getMorphoMidnightBorrowHeadroom, MIDNIGHT_MIN_HEALTH_RATIO, } from './headroom';
22
+ export { tenorBookKeyFor, tenorBookRateToApyPercent, tenorOfferFillToApiFill, tenorOfferToApiOffer, } from './tenor';
23
+ /**
24
+ * Aggregate a Morpho Midnight position. Midnight markets are multi-collateral, so the borrow limit is
25
+ * the sum of each collateral's USD value times its own lltv (Aave-v4 style), rather than a single pair.
26
+ *
27
+ * Note on amounts: `borrowedUsd` is derived from the position's `debt`, which is the face value owed at
28
+ * maturity (principal + fixed interest). Health is therefore measured against the full maturity debt,
29
+ * matching how MidnightView computes `ratio`. Fixed-rate APY is not derived on-chain in MVP, so
30
+ * `netApy` reflects the `'0'` rates in `assetsData` (see the module getter).
31
+ */
32
+ export const getMorphoMidnightAggregatedPositionData = ({ usedAssets, assetsData, marketInfo, }) => {
33
+ var _a;
34
+ const payload = {};
35
+ payload.suppliedUsd = getAssetsTotal(usedAssets, ({ isSupplied }) => isSupplied, ({ suppliedUsd }) => suppliedUsd);
36
+ payload.suppliedCollateralUsd = getAssetsTotal(usedAssets, ({ isSupplied, collateral }) => isSupplied && collateral, ({ suppliedUsd }) => suppliedUsd);
37
+ payload.borrowedUsd = getAssetsTotal(usedAssets, ({ isBorrowed }) => isBorrowed, ({ borrowedUsd }) => borrowedUsd);
38
+ // borrowLimit = Σ collateralUsd_i * lltv_i (per-collateral lltv carried on assetsData)
39
+ 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); });
40
+ payload.liquidationLimitUsd = payload.borrowLimitUsd;
41
+ // Same subtraction every other money market uses, but it does NOT mean the same thing here. Elsewhere
42
+ // `borrowedUsd` is debt at present value, so the remainder is what a borrow would pay out. Midnight
43
+ // records debt at its face value at maturity, so this is face-value headroom: borrowing it would add
44
+ // more debt than the number says, by the market's discount. It also reserves no safety margin, and this
45
+ // aggregate is synchronous so it has no book rate to discount by.
46
+ //
47
+ // DO NOT SURFACE THIS AS "what you can borrow" — use `getMorphoMidnightBorrowHeadroom` instead, which
48
+ // applies both corrections (the margin, and the loan-per-unit price off a book rate) and caps on the
49
+ // depth the book can fill. The discount alone grows past 7% on a one-year market.
50
+ const leftToBorrowUsd = new Dec(payload.borrowLimitUsd).sub(payload.borrowedUsd);
51
+ payload.leftToBorrowUsd = leftToBorrowUsd.lte('0') ? '0' : leftToBorrowUsd.toString();
52
+ const loanTokenPrice = ((_a = assetsData[marketInfo.loanToken]) === null || _a === void 0 ? void 0 : _a.price) || '0';
53
+ payload.leftToBorrow = new Dec(loanTokenPrice).eq(0) ? '0' : new Dec(payload.leftToBorrowUsd).div(loanTokenPrice).toString();
54
+ const { netApy, incentiveUsd, totalInterestUsd } = calculateNetApy({ usedAssets, assetsData: assetsData });
55
+ payload.netApy = netApy;
56
+ payload.incentiveUsd = incentiveUsd;
57
+ payload.totalInterestUsd = totalInterestUsd;
58
+ payload.ltv = new Dec(payload.suppliedCollateralUsd).eq(0) ? '0' : new Dec(payload.borrowedUsd).div(payload.suppliedCollateralUsd).toString();
59
+ payload.ratio = new Dec(payload.borrowedUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.borrowedUsd).mul(100).toString();
60
+ payload.healthRatio = new Dec(payload.borrowedUsd).eq(0) ? 'Infinity' : new Dec(payload.liquidationLimitUsd).div(payload.borrowedUsd).toDP(4).toString();
61
+ const { leveragedType, leveragedAsset } = isLeveragedPos(usedAssets);
62
+ payload.leveragedType = leveragedType;
63
+ payload.liquidationPrice = '';
64
+ if (leveragedType !== '') {
65
+ payload.leveragedAsset = leveragedAsset;
66
+ let assetPrice = assetsData[leveragedAsset].price;
67
+ if (leveragedType === LeverageType.VolatilePair) {
68
+ const borrowedAsset = Object.values(usedAssets).find(({ borrowedUsd }) => +borrowedUsd > 0);
69
+ const borrowedAssetPrice = assetsData[borrowedAsset.symbol].price;
70
+ const leveragedAssetPrice = assetsData[leveragedAsset].price;
71
+ const isReverse = new Dec(leveragedAssetPrice).lt(borrowedAssetPrice);
72
+ if (isReverse) {
73
+ payload.leveragedType = LeverageType.VolatilePairReverse;
74
+ payload.currentVolatilePairRatio = new Dec(borrowedAssetPrice).div(leveragedAssetPrice).toDP(18).toString();
75
+ assetPrice = new Dec(borrowedAssetPrice).div(assetPrice).toString();
76
+ }
77
+ else {
78
+ assetPrice = new Dec(assetPrice).div(borrowedAssetPrice).toString();
79
+ payload.currentVolatilePairRatio = new Dec(leveragedAssetPrice).div(borrowedAssetPrice).toDP(18).toString();
80
+ }
81
+ }
82
+ payload.liquidationPrice = calcLeverageLiqPrice(payload.leveragedType, assetPrice, payload.borrowedUsd, payload.liquidationLimitUsd);
83
+ }
84
+ payload.minCollRatio = new Dec(payload.borrowLimitUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.borrowLimitUsd).mul(100).toString();
85
+ payload.collLiquidationRatio = new Dec(payload.liquidationLimitUsd).eq(0) ? '0' : new Dec(payload.suppliedCollateralUsd).div(payload.liquidationLimitUsd).mul(100).toString();
86
+ payload.exposure = getExposure(payload.borrowedUsd, payload.suppliedUsd);
87
+ return payload;
88
+ };
89
+ // ── Off-chain order-book rate helpers ──────────────────────────────────────────────────────────────
90
+ // notion: https://app.notion.com/p/defisaver/Estimate-borrow-rate-and-slippage-before-execution-3a70be682adc80c783c8c11fdb761dd2
91
+ // the borrow rate is not exposed on-chain (MidnightView only stores total debt at maturity in `units`).
92
+ // derive the rate + interest from Morpho's public keyless Midnight API
93
+ // Quote prices are WAD-scaled
94
+ // loan-per-unit ratios (< 1 for a discounted fixed-term borrow); annualizing them yields the borrow APY.
95
+ const MIDNIGHT_API_BASE = 'https://api.morpho.org/v0/midnight';
96
+ // The book endpoint is markedly slower than the rest of the API — `LONGER_TIMEOUT` (5s) aborts it often
97
+ // enough that markets drop out of the list for no reason.
98
+ const MIDNIGHT_BOOK_TIMEOUT = 30000;
99
+ // The quote endpoint's `slippage` query param is validated as a string: 0.1–100, at most one decimal
100
+ // place (`0.50` is rejected even though `0.5` passes). See `midnightSlippageParam`.
101
+ const MIDNIGHT_SLIPPAGE_MIN = 0.1;
102
+ const MIDNIGHT_SLIPPAGE_MAX = 100;
103
+ /**
104
+ * Coerce a slippage into what the quote endpoint accepts: 0.1–100 with at most one decimal place. The
105
+ * validation is lexical, so a computed value (`4.15066671050631467`) is rejected outright — without this
106
+ * the request 400s and the quote looks unavailable.
107
+ *
108
+ * Rounded **down**, since a wider slippage is a looser cap than the caller asked for.
109
+ */
110
+ export const midnightSlippageParam = (slippagePercent) => Dec.min(Dec.max(new Dec(slippagePercent), MIDNIGHT_SLIPPAGE_MIN), MIDNIGHT_SLIPPAGE_MAX).toDP(1, Dec.ROUND_DOWN).toString();
111
+ /**
112
+ * Current borrower rate + debt breakdown from the Midnight positions API. Reconstructing this from the raw
113
+ * `/transactions` fill history only sums `borrow` fills, so it overstates debt for any position with an
114
+ * early exit or partial liquidation (`exit_borrow_primary`, `partial_liquidation`, ... — the fill history
115
+ * has no exhaustive list of debt-reducing event types). `/positions` instead reports the already-netted
116
+ * `debt`, matching `MidnightView.getPositionInfo` exactly, plus `cost_basis` (outstanding principal,
117
+ * WAD-scaled raw base units) and `effective_rate_wad` (borrow APY, WAD-scaled) for the base/interest split.
118
+ * The caller swallows errors — a missing rate must never block position rendering.
119
+ */
120
+ export const getMorphoMidnightUserBorrowInfo = (account, marketId, loanTokenSymbol) => __awaiter(void 0, void 0, void 0, function* () {
121
+ const res = yield fetch(`${MIDNIGHT_API_BASE}/users/${account}/positions`, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
122
+ const json = yield res.json();
123
+ 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(); });
124
+ const debtTotal = assetAmountInEth((position === null || position === void 0 ? void 0 : position.debt) || '0', loanTokenSymbol);
125
+ const costBasisRaw = new Dec((position === null || position === void 0 ? void 0 : position.cost_basis) || 0).div(WAD); // WAD-scaled → raw base units
126
+ const debtBase = Dec.min(assetAmountInEth(costBasisRaw.toString(), loanTokenSymbol), debtTotal).toString();
127
+ const debtInterest = Dec.max(new Dec(debtTotal).sub(debtBase), 0).toString();
128
+ const borrowRate = new Dec((position === null || position === void 0 ? void 0 : position.effective_rate_wad) || 0).div(WAD).mul(100).toString();
129
+ return {
130
+ borrowRate, debtBase, debtInterest, debtTotal,
131
+ };
132
+ });
133
+ /**
134
+ * The inverse of the split above: where `getMorphoMidnightUserBorrowInfo` reads a position's principal and
135
+ * interest off the indexer, this carries that same split forward through a payback, from a debt of
136
+ * `borrowedBefore` down to one of `borrowedAfter`. Retiring units retires principal and interest pro rata,
137
+ * so both scale by the same fraction.
138
+ *
139
+ * Deriving the interest as `borrowedAfter − debtBase` instead reads it off two different sources —
140
+ * `borrowed` is the chain's debt, `debtBase` the indexer's principal — so the entire gap between them lands
141
+ * in the interest whenever they disagree, which they do until the indexer catches up with a fresh borrow or
142
+ * payback: behind a borrow it inflates the interest by the debt the indexer has not seen yet, behind a
143
+ * payback it goes negative on a position that still owes. Callers detect that window as
144
+ * `debtBase + debtInterest !== debt`.
145
+ *
146
+ * Pro rata is also what keeps the answer sane when a payback retires debt at a different rate than the one
147
+ * it was opened at. Mirroring a borrow instead — principal growing by the assets received, interest by the
148
+ * rest — hands back a negative interest as soon as the book sells units back cheaper than they were bought.
149
+ */
150
+ export const scaleMorphoMidnightDebtSplit = ({ debtBase, debtInterest }, borrowedBefore, borrowedAfter) => {
151
+ const fractionRemaining = new Dec(borrowedBefore).lte(0)
152
+ ? new Dec(0)
153
+ : Dec.max(0, new Dec(borrowedAfter)).div(borrowedBefore);
154
+ return {
155
+ debtBase: new Dec(debtBase).mul(fractionRemaining).toString(),
156
+ debtInterest: new Dec(debtInterest).mul(fractionRemaining).toString(),
157
+ };
158
+ };
159
+ /**
160
+ * One side of a market's resting order book, as rates rather than the API's WAD-scaled loan-per-unit
161
+ * prices. Annualizing each price against time-to-maturity gives the rate a taker filling that offer gets
162
+ * — verified against Morpho's fixed-market UI, where per-offer rates match to the cent.
163
+ *
164
+ * `bids` are the lend offers a borrower fills, so the best of them is the *lowest* rate; `asks` are the
165
+ * sell offers a repayer buys debt units from, where a lower price buys more units, so the best is the
166
+ * *highest* rate. Either way `offers` comes back best-first and `bestRate` is `offers[0].rate`.
167
+ *
168
+ * Returns `null` for an empty side: there is nothing to take, so a market listing should skip the market
169
+ * rather than advertise it at a 0% rate. Throws when the request fails — an error response is rarely
170
+ * JSON, so without the `res.ok` check it parses as an empty book and the market silently vanishes.
171
+ */
172
+ 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') {
173
+ var _a;
174
+ if (isTenorMidnightMarket(market)) {
175
+ return getTenorMarketBook(market, network, side);
176
+ }
177
+ const loanSymbol = getAssetInfoByAddress(market.loanToken, network).symbol;
178
+ const res = yield fetch(`${MIDNIGHT_API_BASE}/books/${market.marketId}`, { signal: AbortSignal.timeout(MIDNIGHT_BOOK_TIMEOUT) });
179
+ if (!res.ok)
180
+ throw new Error(`Midnight book request failed for ${market.value} (${res.status})`);
181
+ const json = yield res.json();
182
+ const ttmDays = midnightTimeToMaturityDays(market.maturity);
183
+ const offers = (((_a = json === null || json === void 0 ? void 0 : json.data) === null || _a === void 0 ? void 0 : _a[side]) || []).map((offer) => ({
184
+ rate: midnightApyFromPrice(new Dec(offer.price).div(WAD), ttmDays),
185
+ liquidity: assetAmountInEth(offer.assets, loanSymbol),
186
+ }));
187
+ return buildMidnightParsedBook(offers, side);
188
+ });
189
+ // The API says why a quote failed — NOT_FOUND (market matured or not open yet), INSUFFICIENT_LIQUIDITY
190
+ // (book can't fill the size), VALIDATION_ERROR (bad param, with the offending field in `details`).
191
+ // Callers surface this to the user, so keep the reason rather than collapsing everything into one string.
192
+ const midnightQuoteError = (error) => {
193
+ const detail = ((error === null || error === void 0 ? void 0 : error.details) || []).map(({ issue }) => issue).filter(Boolean).join('; ');
194
+ const reason = detail || (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.code);
195
+ return reason ? `Morpho Midnight quote unavailable: ${reason}` : 'Morpho Midnight quote unavailable';
196
+ };
197
+ // The raw quote both sides share: prices descaled from WAD, everything else forwarded verbatim.
198
+ const fetchMorphoMidnightQuote = (marketId, side, size, slippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
199
+ const [sizeParam, sizeValue] = 'assets' in size ? ['assets', size.assets] : ['units', size.units];
200
+ const url = `${MIDNIGHT_API_BASE}/books/${marketId}/${side}/quote?${sizeParam}=${sizeValue}&slippage=${midnightSlippageParam(slippagePercent)}`;
201
+ const res = yield fetch(url, { signal: AbortSignal.timeout(LONGER_TIMEOUT) });
202
+ const json = yield res.json();
203
+ const d = json === null || json === void 0 ? void 0 : json.data;
204
+ if (!(d === null || d === void 0 ? void 0 : d.average_best_price))
205
+ throw new Error(midnightQuoteError(json === null || json === void 0 ? void 0 : json.error));
206
+ return {
207
+ bestPrice: new Dec(d.average_best_price).div(WAD).toString(),
208
+ worstPrice: new Dec(d.average_worst_price || 0).div(WAD).toString(),
209
+ availableAssets: d.available_assets || '0',
210
+ availableUnits: d.available_units || '0',
211
+ takeableOffers: d.takeable_offers || [],
212
+ };
213
+ });
214
+ export const getMorphoMidnightBorrowQuote = (marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
215
+ if (isTenorMidnightMarket(marketId)) {
216
+ return getTenorBorrowQuote(marketId, assetsRaw, slippagePercent, maturity, maxBorrowRate, taker, rateSlippagePercent);
217
+ }
218
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'bids', { assets: assetsRaw }, slippagePercent);
219
+ const { bestPrice } = quote;
220
+ const ttmDays = midnightTimeToMaturityDays(maturity);
221
+ const estBorrowRate = midnightApyFromPrice(bestPrice, ttmDays);
222
+ // Price the cap sits at, and the rate that price represents — one derivation, so `maxRate` and
223
+ // `maxUnits` can never disagree about what the user is protected at.
224
+ const capPrice = midnightBoundPrice(estBorrowRate, ttmDays, 'ceiling', maxBorrowRate, rateSlippagePercent);
225
+ const maxRate = midnightApyFromPrice(capPrice, ttmDays);
226
+ const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0);
227
+ const maxUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0);
228
+ return Object.assign(Object.assign({}, quote), { estBorrowRate,
229
+ maxRate,
230
+ newUnits,
231
+ maxUnits });
232
+ });
233
+ export const getMorphoMidnightPaybackQuote = (marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
234
+ if (isTenorMidnightMarket(marketId)) {
235
+ return getTenorPaybackQuote(marketId, assetsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
236
+ }
237
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { assets: assetsRaw }, slippagePercent);
238
+ const { bestPrice } = quote;
239
+ const ttmDays = midnightTimeToMaturityDays(maturity);
240
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
241
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
242
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
243
+ // Rounded down on both counts: `newUnits` must not overstate the debt the user sees retired, and a
244
+ // `minUnits` rounded up would be a stricter floor than asked for and revert a payback that was fine.
245
+ const newUnits = new Dec(bestPrice).lte(0) ? '0' : new Dec(assetsRaw).div(bestPrice).toFixed(0, Dec.ROUND_DOWN);
246
+ const minUnits = new Dec(capPrice).lte(0) ? '0' : new Dec(assetsRaw).div(capPrice).toFixed(0, Dec.ROUND_DOWN);
247
+ return Object.assign(Object.assign({}, quote), { estPaybackRate,
248
+ minRate,
249
+ newUnits,
250
+ minUnits });
251
+ });
252
+ export const getMorphoMidnightPaybackUnitsQuote = (marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent) => __awaiter(void 0, void 0, void 0, function* () {
253
+ if (isTenorMidnightMarket(marketId)) {
254
+ return getTenorPaybackUnitsQuote(marketId, unitsRaw, slippagePercent, maturity, minPaybackRate, taker, rateSlippagePercent);
255
+ }
256
+ const quote = yield fetchMorphoMidnightQuote(marketId, 'asks', { units: unitsRaw }, slippagePercent);
257
+ const { bestPrice } = quote;
258
+ const ttmDays = midnightTimeToMaturityDays(maturity);
259
+ const estPaybackRate = midnightApyFromPrice(bestPrice, ttmDays);
260
+ const capPrice = midnightBoundPrice(estPaybackRate, ttmDays, 'floor', minPaybackRate, rateSlippagePercent);
261
+ const minRate = midnightApyFromPrice(capPrice, ttmDays);
262
+ // Rounded UP on both counts — the mirror of the assets-target quote's ROUND_DOWN. Here the figures are
263
+ // what the taker SPENDS, so a value rounded down understates the cost by a base unit and would leave the
264
+ // buy short of the units it was asked for.
265
+ const newAssets = new Dec(unitsRaw).mul(bestPrice).toFixed(0, Dec.ROUND_UP);
266
+ const maxAssets = new Dec(unitsRaw).mul(capPrice).toFixed(0, Dec.ROUND_UP);
267
+ return Object.assign(Object.assign({}, quote), { estPaybackRate,
268
+ minRate,
269
+ newAssets,
270
+ maxAssets });
271
+ });
@@ -0,0 +1,15 @@
1
+ import Dec from 'decimal.js';
2
+ import { MorphoMidnightBookOffer, MorphoMidnightBookSide, MorphoMidnightParsedBook } from '../../types';
3
+ export declare const midnightTimeToMaturityDays: (maturity: number, atSeconds?: number) => number;
4
+ export declare const midnightApyFromPrice: (price: Dec.Value, ttmDays: Dec.Value) => string;
5
+ /**
6
+ * Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
7
+ * price = (1 + rate)^(−ttmDays / 365).
8
+ */
9
+ export declare const midnightPriceFromApy: (ratePercent: Dec.Value, ttmDays: Dec.Value) => string;
10
+ export declare const MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
11
+ /** Which side of the estimate a guard sits on: a ceiling for borrows, a floor for paybacks. */
12
+ export type MidnightRateBoundKind = 'ceiling' | 'floor';
13
+ export declare const midnightBoundPrice: (estRate: Dec.Value, ttmDays: Dec.Value, kind: MidnightRateBoundKind, boundRate?: Dec.Value, rateSlippagePercent?: Dec.Value) => string;
14
+ export declare const midnightBookBestFirst: (side: MorphoMidnightBookSide) => 1 | -1;
15
+ export declare const buildMidnightParsedBook: (offers: MorphoMidnightBookOffer[], side: MorphoMidnightBookSide) => MorphoMidnightParsedBook | null;
@@ -0,0 +1,50 @@
1
+ import Dec from 'decimal.js';
2
+ import { SECONDS_PER_DAY } from '../../constants';
3
+ const nowInSeconds = () => Math.floor(Date.now() / 1000);
4
+ // Days remaining until maturity, optionally measured at a past timestamp (for historical fills).
5
+ export const midnightTimeToMaturityDays = (maturity, atSeconds = nowInSeconds()) => new Dec(maturity).sub(atSeconds).div(SECONDS_PER_DAY).toNumber();
6
+ // Annualize a fixed-term discount price into an APY percent: (1 / price)^(365 / ttmDays) − 1.
7
+ // `price` is loan-per-unit (assets received / units owed), so 1/price ≥ 1.
8
+ export const midnightApyFromPrice = (price, ttmDays) => {
9
+ const p = new Dec(price);
10
+ const ttm = new Dec(ttmDays);
11
+ if (p.lte(0) || ttm.lte(0))
12
+ return '0';
13
+ return new Dec(1).div(p).pow(new Dec(365).div(ttm)).sub(1)
14
+ .mul(100)
15
+ .toString();
16
+ };
17
+ /**
18
+ * Inverse of `midnightApyFromPrice`: the loan-per-unit price a borrow APY implies,
19
+ * price = (1 + rate)^(−ttmDays / 365).
20
+ */
21
+ export const midnightPriceFromApy = (ratePercent, ttmDays) => {
22
+ const rate = new Dec(ratePercent);
23
+ const ttm = new Dec(ttmDays);
24
+ if (rate.lte(0) || ttm.lte(0))
25
+ return '1';
26
+ return new Dec(1).div(new Dec(1).add(rate.div(100)).pow(ttm.div(365))).toString();
27
+ };
28
+ export const MIDNIGHT_DEFAULT_RATE_SLIPPAGE = 0.1;
29
+ const MIDNIGHT_MIN_FLOOR_RATIO = 0.5;
30
+ export const midnightBoundPrice = (estRate, ttmDays, kind, boundRate, rateSlippagePercent = MIDNIGHT_DEFAULT_RATE_SLIPPAGE) => {
31
+ if (boundRate !== undefined && new Dec(boundRate).gt(0))
32
+ return midnightPriceFromApy(boundRate, ttmDays);
33
+ const est = new Dec(estRate);
34
+ const slippage = new Dec(rateSlippagePercent);
35
+ const bound = kind === 'ceiling'
36
+ ? est.add(slippage)
37
+ : Dec.max(est.sub(slippage), est.mul(MIDNIGHT_MIN_FLOOR_RATIO));
38
+ return midnightPriceFromApy(bound, ttmDays);
39
+ };
40
+ export const midnightBookBestFirst = (side) => (side === 'asks' ? -1 : 1);
41
+ export const buildMidnightParsedBook = (offers, side) => {
42
+ const bestFirst = [...offers].sort((a, b) => new Dec(a.rate).minus(b.rate).mul(midnightBookBestFirst(side)).toNumber());
43
+ if (bestFirst.length === 0)
44
+ return null;
45
+ return {
46
+ bestRate: bestFirst[0].rate,
47
+ totalLiquidity: bestFirst.reduce((sum, offer) => sum.add(offer.liquidity), new Dec(0)).toString(),
48
+ offers: bestFirst,
49
+ };
50
+ };