@defisaver/positions-sdk 2.1.144-dev → 2.1.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/config/contracts.d.ts +249 -0
- package/cjs/config/contracts.js +11 -1
- package/cjs/constants/index.d.ts +1 -0
- package/cjs/constants/index.js +2 -1
- package/cjs/contracts.d.ts +1510 -0
- package/cjs/contracts.js +3 -2
- package/cjs/fluid/index.js +6 -2
- package/cjs/helpers/fluidHelpers/index.js +5 -4
- package/cjs/helpers/index.d.ts +1 -0
- package/cjs/helpers/index.js +2 -1
- package/cjs/helpers/morphoMidnightHelpers/headroom.d.ts +48 -0
- package/cjs/helpers/morphoMidnightHelpers/headroom.js +52 -0
- package/cjs/helpers/morphoMidnightHelpers/index.d.ts +113 -0
- package/cjs/helpers/morphoMidnightHelpers/index.js +298 -0
- package/cjs/helpers/morphoMidnightHelpers/rate.d.ts +15 -0
- package/cjs/helpers/morphoMidnightHelpers/rate.js +62 -0
- package/cjs/helpers/morphoMidnightHelpers/tenor.d.ts +129 -0
- package/cjs/helpers/morphoMidnightHelpers/tenor.js +254 -0
- package/cjs/index.d.ts +2 -1
- package/cjs/index.js +3 -1
- package/cjs/markets/index.d.ts +1 -0
- package/cjs/markets/index.js +6 -1
- package/cjs/markets/morphoMidnight/index.d.ts +60 -0
- package/cjs/markets/morphoMidnight/index.js +659 -0
- package/cjs/morphoMidnight/index.d.ts +14 -0
- package/cjs/morphoMidnight/index.js +261 -0
- package/cjs/portfolio/discovery.js +4 -0
- package/cjs/portfolio/index.js +44 -0
- package/cjs/services/viem.d.ts +11 -11
- package/cjs/staking/eligibility.d.ts +15 -1
- package/cjs/staking/eligibility.js +12 -1
- package/cjs/staking/staking.js +18 -22
- package/cjs/types/index.d.ts +1 -0
- package/cjs/types/index.js +1 -0
- package/cjs/types/morphoMidnight.d.ts +141 -0
- package/cjs/types/morphoMidnight.js +47 -0
- package/cjs/types/portfolio.d.ts +4 -0
- package/esm/config/contracts.d.ts +249 -0
- package/esm/config/contracts.js +9 -0
- package/esm/constants/index.d.ts +1 -0
- package/esm/constants/index.js +1 -0
- package/esm/contracts.d.ts +1510 -0
- package/esm/contracts.js +1 -0
- package/esm/fluid/index.js +6 -2
- package/esm/helpers/fluidHelpers/index.js +6 -5
- package/esm/helpers/index.d.ts +1 -0
- package/esm/helpers/index.js +1 -0
- package/esm/helpers/morphoMidnightHelpers/headroom.d.ts +48 -0
- package/esm/helpers/morphoMidnightHelpers/headroom.js +45 -0
- package/esm/helpers/morphoMidnightHelpers/index.d.ts +113 -0
- package/esm/helpers/morphoMidnightHelpers/index.js +271 -0
- package/esm/helpers/morphoMidnightHelpers/rate.d.ts +15 -0
- package/esm/helpers/morphoMidnightHelpers/rate.js +50 -0
- package/esm/helpers/morphoMidnightHelpers/tenor.d.ts +129 -0
- package/esm/helpers/morphoMidnightHelpers/tenor.js +238 -0
- package/esm/index.d.ts +2 -1
- package/esm/index.js +2 -1
- package/esm/markets/index.d.ts +1 -0
- package/esm/markets/index.js +1 -0
- package/esm/markets/morphoMidnight/index.d.ts +60 -0
- package/esm/markets/morphoMidnight/index.js +631 -0
- package/esm/morphoMidnight/index.d.ts +14 -0
- package/esm/morphoMidnight/index.js +248 -0
- package/esm/portfolio/discovery.js +5 -1
- package/esm/portfolio/index.js +45 -1
- package/esm/services/viem.d.ts +11 -11
- package/esm/staking/eligibility.d.ts +15 -1
- package/esm/staking/eligibility.js +10 -0
- package/esm/staking/staking.js +19 -23
- package/esm/types/index.d.ts +1 -0
- package/esm/types/index.js +1 -0
- package/esm/types/morphoMidnight.d.ts +141 -0
- package/esm/types/morphoMidnight.js +44 -0
- package/esm/types/portfolio.d.ts +4 -0
- package/package.json +1 -1
- package/src/config/contracts.ts +9 -0
- package/src/constants/index.ts +1 -0
- package/src/contracts.ts +1 -0
- package/src/fluid/index.ts +6 -2
- package/src/helpers/fluidHelpers/index.ts +6 -5
- package/src/helpers/index.ts +1 -0
- package/src/helpers/morphoMidnightHelpers/headroom.ts +82 -0
- package/src/helpers/morphoMidnightHelpers/index.ts +485 -0
- package/src/helpers/morphoMidnightHelpers/rate.ts +71 -0
- package/src/helpers/morphoMidnightHelpers/tenor.ts +407 -0
- package/src/index.ts +2 -0
- package/src/markets/index.ts +3 -0
- package/src/markets/morphoMidnight/index.ts +675 -0
- package/src/morphoMidnight/index.ts +276 -0
- package/src/portfolio/discovery.ts +6 -0
- package/src/portfolio/index.ts +44 -0
- package/src/staking/eligibility.ts +29 -2
- package/src/staking/staking.ts +20 -22
- package/src/types/index.ts +1 -0
- package/src/types/morphoMidnight.ts +163 -0
- package/src/types/portfolio.ts +4 -0
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');
|
package/esm/fluid/index.js
CHANGED
|
@@ -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
|
|
1260
|
-
|
|
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,
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
:
|
|
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');
|
package/esm/helpers/index.d.ts
CHANGED
|
@@ -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';
|
package/esm/helpers/index.js
CHANGED
|
@@ -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
|
+
};
|