@hobba-io/core 0.1.0
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/LICENSE +15 -0
- package/README.md +87 -0
- package/dist/amountUtils.d.ts +15 -0
- package/dist/amountUtils.d.ts.map +1 -0
- package/dist/amountUtils.js +59 -0
- package/dist/amountUtils.js.map +1 -0
- package/dist/constants.d.ts +148 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +231 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/executeAction.d.ts +36 -0
- package/dist/executeAction.d.ts.map +1 -0
- package/dist/executeAction.js +606 -0
- package/dist/executeAction.js.map +1 -0
- package/dist/executeDepositBorrow.d.ts +49 -0
- package/dist/executeDepositBorrow.d.ts.map +1 -0
- package/dist/executeDepositBorrow.js +444 -0
- package/dist/executeDepositBorrow.js.map +1 -0
- package/dist/idl/hobba.json +5072 -0
- package/dist/idl.d.ts +783 -0
- package/dist/idl.d.ts.map +1 -0
- package/dist/idl.js +7 -0
- package/dist/idl.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/juplend/accountResolvers.d.ts +106 -0
- package/dist/juplend/accountResolvers.d.ts.map +1 -0
- package/dist/juplend/accountResolvers.js +148 -0
- package/dist/juplend/accountResolvers.js.map +1 -0
- package/dist/juplend/operateRemaining.d.ts +11 -0
- package/dist/juplend/operateRemaining.d.ts.map +1 -0
- package/dist/juplend/operateRemaining.js +46 -0
- package/dist/juplend/operateRemaining.js.map +1 -0
- package/dist/juplend/positionInit.d.ts +34 -0
- package/dist/juplend/positionInit.d.ts.map +1 -0
- package/dist/juplend/positionInit.js +56 -0
- package/dist/juplend/positionInit.js.map +1 -0
- package/dist/juplend/positionParser.d.ts +8 -0
- package/dist/juplend/positionParser.d.ts.map +1 -0
- package/dist/juplend/positionParser.js +30 -0
- package/dist/juplend/positionParser.js.map +1 -0
- package/dist/juplend/sdkApi.d.ts +39 -0
- package/dist/juplend/sdkApi.d.ts.map +1 -0
- package/dist/juplend/sdkApi.js +68 -0
- package/dist/juplend/sdkApi.js.map +1 -0
- package/dist/juplend/tickMath.d.ts +19 -0
- package/dist/juplend/tickMath.d.ts.map +1 -0
- package/dist/juplend/tickMath.js +91 -0
- package/dist/juplend/tickMath.js.map +1 -0
- package/dist/juplend/tickUtils.d.ts +20 -0
- package/dist/juplend/tickUtils.d.ts.map +1 -0
- package/dist/juplend/tickUtils.js +115 -0
- package/dist/juplend/tickUtils.js.map +1 -0
- package/dist/juplend/vaultConfigParser.d.ts +25 -0
- package/dist/juplend/vaultConfigParser.d.ts.map +1 -0
- package/dist/juplend/vaultConfigParser.js +38 -0
- package/dist/juplend/vaultConfigParser.js.map +1 -0
- package/dist/kamino/accountResolvers.d.ts +110 -0
- package/dist/kamino/accountResolvers.d.ts.map +1 -0
- package/dist/kamino/accountResolvers.js +220 -0
- package/dist/kamino/accountResolvers.js.map +1 -0
- package/dist/kamino/obligationParser.d.ts +13 -0
- package/dist/kamino/obligationParser.d.ts.map +1 -0
- package/dist/kamino/obligationParser.js +92 -0
- package/dist/kamino/obligationParser.js.map +1 -0
- package/dist/kamino/refreshInstructions.d.ts +4 -0
- package/dist/kamino/refreshInstructions.d.ts.map +1 -0
- package/dist/kamino/refreshInstructions.js +32 -0
- package/dist/kamino/refreshInstructions.js.map +1 -0
- package/dist/limits.d.ts +87 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +174 -0
- package/dist/limits.js.map +1 -0
- package/dist/marketConfig.d.ts +35 -0
- package/dist/marketConfig.d.ts.map +1 -0
- package/dist/marketConfig.js +85 -0
- package/dist/marketConfig.js.map +1 -0
- package/dist/oracle/scopePrice.d.ts +7 -0
- package/dist/oracle/scopePrice.d.ts.map +1 -0
- package/dist/oracle/scopePrice.js +31 -0
- package/dist/oracle/scopePrice.js.map +1 -0
- package/dist/pdas.d.ts +33 -0
- package/dist/pdas.d.ts.map +1 -0
- package/dist/pdas.js +116 -0
- package/dist/pdas.js.map +1 -0
- package/dist/position.d.ts +45 -0
- package/dist/position.d.ts.map +1 -0
- package/dist/position.js +68 -0
- package/dist/position.js.map +1 -0
- package/dist/positionMetrics.d.ts +108 -0
- package/dist/positionMetrics.d.ts.map +1 -0
- package/dist/positionMetrics.js +238 -0
- package/dist/positionMetrics.js.map +1 -0
- package/dist/program.d.ts +12 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +24 -0
- package/dist/program.js.map +1 -0
- package/dist/quote.d.ts +90 -0
- package/dist/quote.d.ts.map +1 -0
- package/dist/quote.js +198 -0
- package/dist/quote.js.map +1 -0
- package/dist/rates/borrowRates.d.ts +44 -0
- package/dist/rates/borrowRates.d.ts.map +1 -0
- package/dist/rates/borrowRates.js +229 -0
- package/dist/rates/borrowRates.js.map +1 -0
- package/dist/rates/collateralInfo.d.ts +21 -0
- package/dist/rates/collateralInfo.d.ts.map +1 -0
- package/dist/rates/collateralInfo.js +104 -0
- package/dist/rates/collateralInfo.js.map +1 -0
- package/dist/rates/constants.d.ts +22 -0
- package/dist/rates/constants.d.ts.map +1 -0
- package/dist/rates/constants.js +70 -0
- package/dist/rates/constants.js.map +1 -0
- package/dist/rates/index.d.ts +11 -0
- package/dist/rates/index.d.ts.map +1 -0
- package/dist/rates/index.js +11 -0
- package/dist/rates/index.js.map +1 -0
- package/dist/rates/kaminoApi.d.ts +24 -0
- package/dist/rates/kaminoApi.d.ts.map +1 -0
- package/dist/rates/kaminoApi.js +98 -0
- package/dist/rates/kaminoApi.js.map +1 -0
- package/dist/rates/supplyRates.d.ts +23 -0
- package/dist/rates/supplyRates.d.ts.map +1 -0
- package/dist/rates/supplyRates.js +67 -0
- package/dist/rates/supplyRates.js.map +1 -0
- package/dist/transactionBuilder.d.ts +5 -0
- package/dist/transactionBuilder.d.ts.map +1 -0
- package/dist/transactionBuilder.js +287 -0
- package/dist/transactionBuilder.js.map +1 -0
- package/dist/txHelpers.d.ts +16 -0
- package/dist/txHelpers.d.ts.map +1 -0
- package/dist/txHelpers.js +82 -0
- package/dist/txHelpers.js.map +1 -0
- package/dist/userState.d.ts +21 -0
- package/dist/userState.d.ts.map +1 -0
- package/dist/userState.js +60 -0
- package/dist/userState.js.map +1 -0
- package/dist/vault/vaultAccounts.d.ts +82 -0
- package/dist/vault/vaultAccounts.d.ts.map +1 -0
- package/dist/vault/vaultAccounts.js +275 -0
- package/dist/vault/vaultAccounts.js.map +1 -0
- package/dist/wallet.d.ts +12 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +2 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +63 -0
- package/src/amountUtils.ts +69 -0
- package/src/constants.ts +263 -0
- package/src/errors.ts +33 -0
- package/src/executeAction.ts +884 -0
- package/src/executeDepositBorrow.ts +677 -0
- package/src/idl/hobba.json +5072 -0
- package/src/idl.ts +7 -0
- package/src/index.ts +21 -0
- package/src/juplend/accountResolvers.ts +207 -0
- package/src/juplend/operateRemaining.ts +81 -0
- package/src/juplend/positionInit.ts +86 -0
- package/src/juplend/positionParser.ts +41 -0
- package/src/juplend/sdkApi.ts +112 -0
- package/src/juplend/tickMath.ts +82 -0
- package/src/juplend/tickUtils.ts +163 -0
- package/src/juplend/vaultConfigParser.ts +45 -0
- package/src/kamino/accountResolvers.ts +299 -0
- package/src/kamino/obligationParser.ts +123 -0
- package/src/kamino/refreshInstructions.ts +45 -0
- package/src/limits.ts +222 -0
- package/src/marketConfig.ts +139 -0
- package/src/oracle/scopePrice.ts +31 -0
- package/src/pdas.ts +226 -0
- package/src/position.ts +131 -0
- package/src/positionMetrics.ts +387 -0
- package/src/program.ts +30 -0
- package/src/quote.ts +310 -0
- package/src/rates/borrowRates.ts +270 -0
- package/src/rates/collateralInfo.ts +136 -0
- package/src/rates/constants.ts +87 -0
- package/src/rates/index.ts +38 -0
- package/src/rates/kaminoApi.ts +134 -0
- package/src/rates/supplyRates.ts +102 -0
- package/src/transactionBuilder.ts +324 -0
- package/src/txHelpers.ts +107 -0
- package/src/userState.ts +91 -0
- package/src/vault/vaultAccounts.ts +365 -0
- package/src/wallet.ts +12 -0
package/src/quote.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { collateralDecimals, collateralLabel } from "./amountUtils";
|
|
3
|
+
import { getPosition, type HobbaPosition, type Lender } from "./position";
|
|
4
|
+
import {
|
|
5
|
+
bestMarketName,
|
|
6
|
+
fetchCollateralPrice,
|
|
7
|
+
fetchRatesPayload,
|
|
8
|
+
fetchSupplyRatesPayload,
|
|
9
|
+
type RatesPayload,
|
|
10
|
+
type SupplyRatesPayload,
|
|
11
|
+
} from "./positionMetrics";
|
|
12
|
+
|
|
13
|
+
/** Calculator defaults — same values the app falls back to when rates are empty. */
|
|
14
|
+
const FALLBACK_BORROW_APY = 5.6 / 100;
|
|
15
|
+
const FALLBACK_SUPPLY_APY = 21.59 / 100;
|
|
16
|
+
const SOL_STAKING_APY = 7 / 100;
|
|
17
|
+
const MAX_USER_LTV_PCT = 50;
|
|
18
|
+
|
|
19
|
+
export type RiskLevel = "safe" | "moderate" | "risky" | "none";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Projection quote for the borrow calculator and the deposit/manage modals —
|
|
23
|
+
* the same math app.hobba.io runs (CalculatorSection + ActionModal /
|
|
24
|
+
* DepositBorrowModal), computed against live rates so partner UIs never
|
|
25
|
+
* re-implement Hobba's logic. All *Pct values are percent numbers, all *Usd
|
|
26
|
+
* values USD.
|
|
27
|
+
*/
|
|
28
|
+
export interface HobbaQuote {
|
|
29
|
+
lender: Lender;
|
|
30
|
+
collateralPriceUsd: number;
|
|
31
|
+
liquidationLtvPct: number;
|
|
32
|
+
maxUserLtvPct: number;
|
|
33
|
+
|
|
34
|
+
/** Current position (zeros for new users). UI units. */
|
|
35
|
+
current: {
|
|
36
|
+
collateralUi: number;
|
|
37
|
+
debtUsdc: number;
|
|
38
|
+
ltvPct: number;
|
|
39
|
+
loanApyPct: number | null;
|
|
40
|
+
};
|
|
41
|
+
/** Position after applying the requested deltas. UI units. */
|
|
42
|
+
projected: {
|
|
43
|
+
collateralUi: number;
|
|
44
|
+
debtUsdc: number;
|
|
45
|
+
ltvPct: number;
|
|
46
|
+
loanApyPct: number | null;
|
|
47
|
+
liquidationPriceUsd: number | null;
|
|
48
|
+
dropToLiquidationPct: number | null;
|
|
49
|
+
riskLabel: string;
|
|
50
|
+
riskLevel: RiskLevel;
|
|
51
|
+
};
|
|
52
|
+
/** Calculator figures for the projected position. */
|
|
53
|
+
calculator: {
|
|
54
|
+
depositValueUsd: number;
|
|
55
|
+
borrowValueUsd: number;
|
|
56
|
+
borrowLimitUsd: number;
|
|
57
|
+
loanApyPct: number;
|
|
58
|
+
earnMode: boolean;
|
|
59
|
+
earnApyPct: number;
|
|
60
|
+
borrowCostPerYearUsd: number;
|
|
61
|
+
earningPerYearUsd: number;
|
|
62
|
+
stakingEarnPerYearUsd: number;
|
|
63
|
+
/** dd/mm/yyyy when the projected loan APY is negative, else null. */
|
|
64
|
+
selfRepayDate: string | null;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assessRisk(currentPrice: number, liquidationPrice: number): {
|
|
69
|
+
riskLabel: string;
|
|
70
|
+
riskLevel: RiskLevel;
|
|
71
|
+
} {
|
|
72
|
+
if (currentPrice <= 0 || liquidationPrice <= 0) return { riskLabel: "—", riskLevel: "none" };
|
|
73
|
+
const fallPct = ((currentPrice - liquidationPrice) / currentPrice) * 100;
|
|
74
|
+
if (fallPct > 70) return { riskLabel: "Very Safe", riskLevel: "safe" };
|
|
75
|
+
if (fallPct > 50) return { riskLabel: "Safe", riskLevel: "safe" };
|
|
76
|
+
if (fallPct > 40) return { riskLabel: "Moderate Risk", riskLevel: "moderate" };
|
|
77
|
+
if (fallPct > 30) return { riskLabel: "Risky", riskLevel: "moderate" };
|
|
78
|
+
if (fallPct > 15) return { riskLabel: "Very Risky", riskLevel: "risky" };
|
|
79
|
+
return { riskLabel: "Extremely Risky", riskLevel: "risky" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function selfRepayDateFor(loanApyFraction: number): string | null {
|
|
83
|
+
if (loanApyFraction >= 0) return null;
|
|
84
|
+
const yearsToRepay = 1 / Math.abs(loanApyFraction);
|
|
85
|
+
if (yearsToRepay > 10) return null;
|
|
86
|
+
const repayDate = new Date();
|
|
87
|
+
repayDate.setDate(repayDate.getDate() + Math.ceil(yearsToRepay * 365));
|
|
88
|
+
return `${repayDate.getDate().toString().padStart(2, "0")}/${(repayDate.getMonth() + 1)
|
|
89
|
+
.toString()
|
|
90
|
+
.padStart(2, "0")}/${repayDate.getFullYear()}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface GetQuoteParams {
|
|
94
|
+
connection: Connection;
|
|
95
|
+
collateralMint: PublicKey;
|
|
96
|
+
/** Wallet whose existing position the deltas apply to; omit for a fresh calculator quote. */
|
|
97
|
+
owner?: PublicKey;
|
|
98
|
+
/** Deltas in base units (collateral base units / micro-USDC), decimal strings. */
|
|
99
|
+
depositAmount?: string;
|
|
100
|
+
borrowAmount?: string;
|
|
101
|
+
repayAmount?: string;
|
|
102
|
+
withdrawAmount?: string;
|
|
103
|
+
apiBaseUrl?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function getQuote(params: GetQuoteParams): Promise<HobbaQuote> {
|
|
107
|
+
const { connection, collateralMint } = params;
|
|
108
|
+
const base = (params.apiBaseUrl ?? "").replace(/\/+$/, "");
|
|
109
|
+
const label = collateralLabel(collateralMint);
|
|
110
|
+
|
|
111
|
+
const [position, rates, supplyRates, price] = await Promise.all([
|
|
112
|
+
params.owner
|
|
113
|
+
? getPosition({ connection, owner: params.owner, collateralMint, apiBaseUrl: base })
|
|
114
|
+
: Promise.resolve<HobbaPosition | null>(null),
|
|
115
|
+
fetchRatesPayload(base),
|
|
116
|
+
fetchSupplyRatesPayload(base),
|
|
117
|
+
fetchCollateralPrice(base, label),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
return computeQuote({
|
|
121
|
+
position,
|
|
122
|
+
rates,
|
|
123
|
+
supplyRates,
|
|
124
|
+
collateralPriceUsd: price,
|
|
125
|
+
collateralMint,
|
|
126
|
+
depositAmount: params.depositAmount,
|
|
127
|
+
borrowAmount: params.borrowAmount,
|
|
128
|
+
repayAmount: params.repayAmount,
|
|
129
|
+
withdrawAmount: params.withdrawAmount,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Inputs for the pure quote computation. The app feeds these from its cached
|
|
135
|
+
* hooks so calculator/modal math stays synchronous per keystroke;
|
|
136
|
+
* `getQuote` feeds them from the backend fetches above. Same math either way.
|
|
137
|
+
*/
|
|
138
|
+
export interface QuoteInputs {
|
|
139
|
+
position: HobbaPosition | null;
|
|
140
|
+
rates: RatesPayload;
|
|
141
|
+
supplyRates: SupplyRatesPayload;
|
|
142
|
+
collateralPriceUsd: number;
|
|
143
|
+
collateralMint: PublicKey;
|
|
144
|
+
/** Deltas in base units (collateral base units / micro-USDC), decimal strings. */
|
|
145
|
+
depositAmount?: string;
|
|
146
|
+
borrowAmount?: string;
|
|
147
|
+
repayAmount?: string;
|
|
148
|
+
withdrawAmount?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Optional position-rate overrides (percent). When set they replace the
|
|
151
|
+
* internal market/vault lookups for the loan-APY math — the app passes its
|
|
152
|
+
* page-level figures so modal projections match what is already on screen
|
|
153
|
+
* (and xStocks markets, which the internal lookup doesn't resolve, price
|
|
154
|
+
* correctly). Headless callers normally omit them.
|
|
155
|
+
*/
|
|
156
|
+
positionBorrowApyPct?: number;
|
|
157
|
+
positionVaultApyPct?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function computeQuote(inputs: QuoteInputs): HobbaQuote {
|
|
161
|
+
const { position, rates, supplyRates, collateralPriceUsd: price, collateralMint } = inputs;
|
|
162
|
+
const label = collateralLabel(collateralMint);
|
|
163
|
+
const decimals = collateralDecimals(collateralMint);
|
|
164
|
+
const divisor = Math.pow(10, decimals);
|
|
165
|
+
const isSol = label === "SOL";
|
|
166
|
+
const params = inputs;
|
|
167
|
+
|
|
168
|
+
// Best live rates with the app's hardcoded fallbacks (CalculatorSection).
|
|
169
|
+
const bestBorrowAPY = (() => {
|
|
170
|
+
if (rates.markets.length === 0) return FALLBACK_BORROW_APY;
|
|
171
|
+
let best = Infinity;
|
|
172
|
+
for (const m of rates.markets) if (m.borrowAPY < best) best = m.borrowAPY;
|
|
173
|
+
return best < Infinity ? best / 100 : FALLBACK_BORROW_APY;
|
|
174
|
+
})();
|
|
175
|
+
const bestSupplyAPY = (() => {
|
|
176
|
+
if (supplyRates.vaults.length === 0) return FALLBACK_SUPPLY_APY;
|
|
177
|
+
let best = -Infinity;
|
|
178
|
+
for (const v of supplyRates.vaults) if (v.supplyAPY > best) best = v.supplyAPY;
|
|
179
|
+
return best > -Infinity ? best / 100 : FALLBACK_SUPPLY_APY;
|
|
180
|
+
})();
|
|
181
|
+
|
|
182
|
+
const lender: Lender = position?.exists
|
|
183
|
+
? position.lender
|
|
184
|
+
: bestMarketName(rates) === "JupLend"
|
|
185
|
+
? "juplend"
|
|
186
|
+
: "kamino";
|
|
187
|
+
const isJuplendLender = lender === "juplend";
|
|
188
|
+
|
|
189
|
+
const ltvSource = isJuplendLender
|
|
190
|
+
? rates.juplendCollateralLtvs[label]
|
|
191
|
+
: rates.collateralLtvs[label];
|
|
192
|
+
const liquidationLtvPct =
|
|
193
|
+
ltvSource?.liquidationThresholdPct ?? (label === "cbBTC" ? 80 : 75);
|
|
194
|
+
|
|
195
|
+
// Current position in UI units
|
|
196
|
+
const curCollateralUi = position ? Number(position.collateral) / divisor : 0;
|
|
197
|
+
const curDebtUsdc = position ? Number(position.userDebtUsdc) / 1e6 : 0;
|
|
198
|
+
const actualOperatorBorrowed = position ? Number(position.depositedToVault) / 1e6 : 0;
|
|
199
|
+
|
|
200
|
+
// Deltas in UI units
|
|
201
|
+
const depositUi = Number(params.depositAmount ?? "0") / divisor;
|
|
202
|
+
const withdrawUi = Number(params.withdrawAmount ?? "0") / divisor;
|
|
203
|
+
const borrowUsdc = Number(params.borrowAmount ?? "0") / 1e6;
|
|
204
|
+
const repayUsdc = Number(params.repayAmount ?? "0") / 1e6;
|
|
205
|
+
|
|
206
|
+
const projCollateralUi = Math.max(0, curCollateralUi + depositUi - withdrawUi);
|
|
207
|
+
const projDebtUsdc = Math.max(0, curDebtUsdc + borrowUsdc - repayUsdc);
|
|
208
|
+
|
|
209
|
+
const curCollateralUsd = curCollateralUi * price;
|
|
210
|
+
const projCollateralUsd = projCollateralUi * price;
|
|
211
|
+
const curLtvPct = curCollateralUsd > 0 ? (curDebtUsdc / curCollateralUsd) * 100 : 0;
|
|
212
|
+
const projLtvPct = projCollateralUsd > 0 ? (projDebtUsdc / projCollateralUsd) * 100 : 0;
|
|
213
|
+
|
|
214
|
+
// Loan APY for a position — ported from ActionModal.computeLoanAPY: the
|
|
215
|
+
// operator borrows up to the per-asset target LTV whenever vault yield beats
|
|
216
|
+
// borrow cost; rates follow the user's actual lender + vault (Dashboard).
|
|
217
|
+
const TARGET_LTV = isSol ? 0.6 : 0.65;
|
|
218
|
+
const borrowMarketNameForRates = !position?.exists
|
|
219
|
+
? "Kamino Main Market"
|
|
220
|
+
: isJuplendLender
|
|
221
|
+
? "JupLend"
|
|
222
|
+
: "Kamino Main Market";
|
|
223
|
+
const positionBorrowAPY =
|
|
224
|
+
params.positionBorrowApyPct ??
|
|
225
|
+
(rates.markets.find((m) => m.name === borrowMarketNameForRates)?.borrowAPY ??
|
|
226
|
+
bestBorrowAPY * 100);
|
|
227
|
+
const perenaVaultAPY = supplyRates.vaults.find((v) => v.name === "Perena USDC")?.supplyAPY ?? 0;
|
|
228
|
+
const positionVaultAPY =
|
|
229
|
+
params.positionVaultApyPct ?? (perenaVaultAPY > 0 ? perenaVaultAPY : bestSupplyAPY * 100);
|
|
230
|
+
|
|
231
|
+
const DUST_DEBT = 0.01;
|
|
232
|
+
const computeLoanApyPct = (userDebt: number, collateralUsd: number): number | null => {
|
|
233
|
+
if (userDebt < DUST_DEBT) return null;
|
|
234
|
+
let opDebt = actualOperatorBorrowed;
|
|
235
|
+
if (collateralUsd > 0 && positionVaultAPY > positionBorrowAPY) {
|
|
236
|
+
const projected = Math.max(0, collateralUsd * TARGET_LTV - userDebt);
|
|
237
|
+
opDebt = Math.max(actualOperatorBorrowed, projected);
|
|
238
|
+
} else if (positionVaultAPY <= positionBorrowAPY) {
|
|
239
|
+
opDebt = 0;
|
|
240
|
+
}
|
|
241
|
+
return ((userDebt + opDebt) * positionBorrowAPY - opDebt * positionVaultAPY) / userDebt;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const currentLoanApyPct = computeLoanApyPct(curDebtUsdc, curCollateralUsd);
|
|
245
|
+
const projectedLoanApyPct = computeLoanApyPct(projDebtUsdc, projCollateralUsd);
|
|
246
|
+
|
|
247
|
+
const projLiqPrice =
|
|
248
|
+
projCollateralUsd > 0 && projDebtUsdc > 0 && liquidationLtvPct > 0
|
|
249
|
+
? price * (projLtvPct / liquidationLtvPct)
|
|
250
|
+
: 0;
|
|
251
|
+
const projBuffer =
|
|
252
|
+
price > 0 && projLiqPrice > 0 ? Math.max(0, ((price - projLiqPrice) / price) * 100) : null;
|
|
253
|
+
const { riskLabel, riskLevel } = assessRisk(price, projLiqPrice);
|
|
254
|
+
|
|
255
|
+
// --- Calculator math (ported from CalculatorSection) on projected values ---
|
|
256
|
+
const depositValueUsd = projCollateralUsd;
|
|
257
|
+
const borrowValueUsd = projDebtUsdc; // USDC ≈ $1
|
|
258
|
+
const borrowLimitUsd = depositValueUsd * (MAX_USER_LTV_PCT / 100);
|
|
259
|
+
const earningPerYearUsd =
|
|
260
|
+
depositValueUsd * TARGET_LTV > borrowValueUsd
|
|
261
|
+
? (depositValueUsd * TARGET_LTV - borrowValueUsd) * (bestSupplyAPY - bestBorrowAPY)
|
|
262
|
+
: 0;
|
|
263
|
+
const borrowCostPerYearUsd = borrowValueUsd * bestBorrowAPY;
|
|
264
|
+
const calcLoanApy =
|
|
265
|
+
depositValueUsd > 0 && borrowValueUsd > 0
|
|
266
|
+
? (borrowCostPerYearUsd - earningPerYearUsd) / borrowValueUsd
|
|
267
|
+
: 0;
|
|
268
|
+
const stakingEarnPerYearUsd = isSol ? depositValueUsd * SOL_STAKING_APY : 0;
|
|
269
|
+
const hasSpreadEarn =
|
|
270
|
+
depositValueUsd > 0 && borrowValueUsd <= 0 && bestSupplyAPY > bestBorrowAPY;
|
|
271
|
+
const earnMode =
|
|
272
|
+
depositValueUsd > 0 && borrowValueUsd <= 0 && (hasSpreadEarn || stakingEarnPerYearUsd > 0);
|
|
273
|
+
const totalEarnPerYear = (hasSpreadEarn ? earningPerYearUsd : 0) + stakingEarnPerYearUsd;
|
|
274
|
+
const earnApy = earnMode && depositValueUsd > 0 ? totalEarnPerYear / depositValueUsd : 0;
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
lender,
|
|
278
|
+
collateralPriceUsd: price,
|
|
279
|
+
liquidationLtvPct,
|
|
280
|
+
maxUserLtvPct: MAX_USER_LTV_PCT,
|
|
281
|
+
current: {
|
|
282
|
+
collateralUi: curCollateralUi,
|
|
283
|
+
debtUsdc: curDebtUsdc,
|
|
284
|
+
ltvPct: curLtvPct,
|
|
285
|
+
loanApyPct: currentLoanApyPct,
|
|
286
|
+
},
|
|
287
|
+
projected: {
|
|
288
|
+
collateralUi: projCollateralUi,
|
|
289
|
+
debtUsdc: projDebtUsdc,
|
|
290
|
+
ltvPct: projLtvPct,
|
|
291
|
+
loanApyPct: projectedLoanApyPct,
|
|
292
|
+
liquidationPriceUsd: projLiqPrice > 0 ? projLiqPrice : null,
|
|
293
|
+
dropToLiquidationPct: projBuffer,
|
|
294
|
+
riskLabel,
|
|
295
|
+
riskLevel,
|
|
296
|
+
},
|
|
297
|
+
calculator: {
|
|
298
|
+
depositValueUsd,
|
|
299
|
+
borrowValueUsd,
|
|
300
|
+
borrowLimitUsd,
|
|
301
|
+
loanApyPct: calcLoanApy * 100,
|
|
302
|
+
earnMode,
|
|
303
|
+
earnApyPct: earnApy * 100,
|
|
304
|
+
borrowCostPerYearUsd,
|
|
305
|
+
earningPerYearUsd,
|
|
306
|
+
stakingEarnPerYearUsd,
|
|
307
|
+
selfRepayDate: selfRepayDateFor(calcLoanApy),
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared borrow rate computation functions.
|
|
3
|
+
* Extracted from /api/rates/route.ts to be reused by /api/market-data and the rebalancer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
|
|
8
|
+
const SF_SCALE = BigInt("1152921504606846976"); // 2^60
|
|
9
|
+
const SLOTS_PER_YEAR = 63072000;
|
|
10
|
+
const SLOTS_PER_SECOND = 2;
|
|
11
|
+
const DEFAULT_SLOT_DURATION_MS = 400;
|
|
12
|
+
const JUPLEND_EXCHANGE_PRECISION = BigInt("1000000000000"); // 1e12
|
|
13
|
+
const JUPLEND_SECONDS_PER_YEAR = 31536000;
|
|
14
|
+
|
|
15
|
+
/** Measure actual slot duration from recent performance samples. */
|
|
16
|
+
export async function getRecentSlotDurationMs(connection: { getRecentPerformanceSamples: (limit: number) => Promise<Array<{ numSlots: number; samplePeriodSecs: number }>> }): Promise<number> {
|
|
17
|
+
try {
|
|
18
|
+
const samples = await connection.getRecentPerformanceSamples(10);
|
|
19
|
+
if (samples.length === 0) return DEFAULT_SLOT_DURATION_MS;
|
|
20
|
+
let totalSlots = 0, totalSecs = 0;
|
|
21
|
+
for (const s of samples) {
|
|
22
|
+
totalSlots += s.numSlots;
|
|
23
|
+
totalSecs += s.samplePeriodSecs;
|
|
24
|
+
}
|
|
25
|
+
if (totalSlots === 0) return DEFAULT_SLOT_DURATION_MS;
|
|
26
|
+
const measured = (totalSecs * 1000) / totalSlots;
|
|
27
|
+
// Sanity check: local validators report unrealistic durations
|
|
28
|
+
if (measured < 200 || measured > 600) return DEFAULT_SLOT_DURATION_MS;
|
|
29
|
+
return measured;
|
|
30
|
+
} catch {
|
|
31
|
+
return DEFAULT_SLOT_DURATION_MS;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CurvePoint {
|
|
36
|
+
utilizationRateBps: number;
|
|
37
|
+
borrowRateBps: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BorrowRateResult {
|
|
41
|
+
availableLiquidity: number;
|
|
42
|
+
totalBorrowed: number;
|
|
43
|
+
utilization: number; // percentage (0–100)
|
|
44
|
+
borrowAPR: number; // percentage
|
|
45
|
+
borrowAPY: number; // percentage
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CollateralInfo {
|
|
49
|
+
totalDeposited: number;
|
|
50
|
+
depositLimit: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function decodeReserve(data: Buffer) {
|
|
54
|
+
// Lazy + server-only by design: resolves klend-sdk (an optional peer) from
|
|
55
|
+
// the consumer's node_modules at call time, keeping the heavy SDK out of
|
|
56
|
+
// browser bundles and out of load-time module evaluation. createRequire is
|
|
57
|
+
// invoked inside the function so importing this subpath never throws in
|
|
58
|
+
// environments without the optional peer installed.
|
|
59
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
60
|
+
const { Reserve } = nodeRequire(
|
|
61
|
+
"@kamino-finance/klend-sdk/dist/@codegen/klend/accounts/Reserve"
|
|
62
|
+
);
|
|
63
|
+
return Reserve.decode(data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function computeKaminoBorrowRate(reserve: any, recentSlotDurationMs = DEFAULT_SLOT_DURATION_MS): BorrowRateResult {
|
|
67
|
+
const availableAmount = Number(reserve.liquidity.availableAmount);
|
|
68
|
+
const borrowedAmount = Number(BigInt(reserve.liquidity.borrowedAmountSf.toString()) / SF_SCALE);
|
|
69
|
+
const protocolFees = Number(BigInt(reserve.liquidity.accumulatedProtocolFeesSf.toString()) / SF_SCALE);
|
|
70
|
+
const referrerFees = Number(BigInt(reserve.liquidity.accumulatedReferrerFeesSf.toString()) / SF_SCALE);
|
|
71
|
+
const pendingFees = Number(BigInt(reserve.liquidity.pendingReferrerFeesSf.toString()) / SF_SCALE);
|
|
72
|
+
|
|
73
|
+
const totalSupply = availableAmount + borrowedAmount - protocolFees - referrerFees - pendingFees;
|
|
74
|
+
const utilization = totalSupply > 0 ? borrowedAmount / totalSupply : 0;
|
|
75
|
+
|
|
76
|
+
const curve: CurvePoint[] = reserve.config.borrowRateCurve.points;
|
|
77
|
+
const utilBps = utilization * 10000;
|
|
78
|
+
let borrowRateBps = 0;
|
|
79
|
+
for (let i = 1; i < curve.length; i++) {
|
|
80
|
+
if (curve[i].utilizationRateBps === 0 && curve[i].borrowRateBps === 0 && i > 1) break;
|
|
81
|
+
if (utilBps <= curve[i].utilizationRateBps) {
|
|
82
|
+
const x0 = curve[i - 1].utilizationRateBps;
|
|
83
|
+
const x1 = curve[i].utilizationRateBps;
|
|
84
|
+
const y0 = curve[i - 1].borrowRateBps;
|
|
85
|
+
const y1 = curve[i].borrowRateBps;
|
|
86
|
+
borrowRateBps = y0 + ((utilBps - x0) * (y1 - y0)) / (x1 - x0);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const slotAdjustment = 1000 / SLOTS_PER_SECOND / recentSlotDurationMs;
|
|
92
|
+
const adjustedRate = (borrowRateBps / 10000) * slotAdjustment;
|
|
93
|
+
const hostFixedRate = (reserve.config.hostFixedInterestRateBps / 10000) * slotAdjustment;
|
|
94
|
+
const borrowAPR = adjustedRate + hostFixedRate;
|
|
95
|
+
const borrowAPY = Math.pow(1 + borrowAPR / SLOTS_PER_YEAR, SLOTS_PER_YEAR) - 1;
|
|
96
|
+
|
|
97
|
+
const mintDecimals = Number(reserve.liquidity.mintDecimals);
|
|
98
|
+
const divisor = Math.pow(10, mintDecimals);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
availableLiquidity: availableAmount / divisor,
|
|
102
|
+
totalBorrowed: borrowedAmount / divisor,
|
|
103
|
+
utilization: utilization * 100,
|
|
104
|
+
borrowAPR: borrowAPR * 100,
|
|
105
|
+
borrowAPY: borrowAPY * 100,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function computeKaminoSupplyRate(reserve: any, recentSlotDurationMs = DEFAULT_SLOT_DURATION_MS): { supplyAPY: number } {
|
|
110
|
+
const availableAmount = Number(reserve.liquidity.availableAmount);
|
|
111
|
+
const borrowedAmount = Number(BigInt(reserve.liquidity.borrowedAmountSf.toString()) / SF_SCALE);
|
|
112
|
+
const protocolFees = Number(BigInt(reserve.liquidity.accumulatedProtocolFeesSf.toString()) / SF_SCALE);
|
|
113
|
+
const referrerFees = Number(BigInt(reserve.liquidity.accumulatedReferrerFeesSf.toString()) / SF_SCALE);
|
|
114
|
+
const pendingFees = Number(BigInt(reserve.liquidity.pendingReferrerFeesSf.toString()) / SF_SCALE);
|
|
115
|
+
|
|
116
|
+
const totalSupply = availableAmount + borrowedAmount - protocolFees - referrerFees - pendingFees;
|
|
117
|
+
const utilization = totalSupply > 0 ? borrowedAmount / totalSupply : 0;
|
|
118
|
+
|
|
119
|
+
const curve: CurvePoint[] = reserve.config.borrowRateCurve.points;
|
|
120
|
+
const utilBps = utilization * 10000;
|
|
121
|
+
let borrowRateBps = 0;
|
|
122
|
+
for (let i = 1; i < curve.length; i++) {
|
|
123
|
+
if (curve[i].utilizationRateBps === 0 && curve[i].borrowRateBps === 0 && i > 1) break;
|
|
124
|
+
if (utilBps <= curve[i].utilizationRateBps) {
|
|
125
|
+
const x0 = curve[i - 1].utilizationRateBps;
|
|
126
|
+
const x1 = curve[i].utilizationRateBps;
|
|
127
|
+
const y0 = curve[i - 1].borrowRateBps;
|
|
128
|
+
const y1 = curve[i].borrowRateBps;
|
|
129
|
+
borrowRateBps = y0 + ((utilBps - x0) * (y1 - y0)) / (x1 - x0);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const slotAdjustment = 1000 / SLOTS_PER_SECOND / recentSlotDurationMs;
|
|
135
|
+
const adjustedRate = (borrowRateBps / 10000) * slotAdjustment;
|
|
136
|
+
const hostFixedRate = (reserve.config.hostFixedInterestRateBps / 10000) * slotAdjustment;
|
|
137
|
+
const borrowAPR = adjustedRate + hostFixedRate;
|
|
138
|
+
|
|
139
|
+
// Protocol take rate: fraction of interest kept by protocol (in bps)
|
|
140
|
+
const protocolTakeRatePct = (reserve.config.protocolTakeRatePct ?? 0) / 100;
|
|
141
|
+
const supplyAPR = borrowAPR * utilization * (1 - protocolTakeRatePct);
|
|
142
|
+
const supplyAPY = Math.pow(1 + supplyAPR / SLOTS_PER_YEAR, SLOTS_PER_YEAR) - 1;
|
|
143
|
+
|
|
144
|
+
return { supplyAPY: supplyAPY * 100 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface CollateralLtvInfo {
|
|
148
|
+
loanToValuePct: number; // max LTV for borrowing (e.g. 65)
|
|
149
|
+
liquidationThresholdPct: number; // liquidation LTV (e.g. 85)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getCollateralLtvInfo(reserve: any): CollateralLtvInfo {
|
|
153
|
+
return {
|
|
154
|
+
loanToValuePct: reserve.config.loanToValuePct ?? 0,
|
|
155
|
+
liquidationThresholdPct: reserve.config.liquidationThresholdPct ?? 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getCollateralInfo(reserve: any): CollateralInfo {
|
|
160
|
+
const availableAmount = Number(reserve.liquidity.availableAmount);
|
|
161
|
+
const borrowedAmount = Number(BigInt(reserve.liquidity.borrowedAmountSf.toString()) / SF_SCALE);
|
|
162
|
+
const mintDecimals = Number(reserve.liquidity.mintDecimals);
|
|
163
|
+
const divisor = Math.pow(10, mintDecimals);
|
|
164
|
+
|
|
165
|
+
const totalDeposited = (availableAmount + borrowedAmount) / divisor;
|
|
166
|
+
const depositLimit = Number(BigInt(reserve.config.depositLimit.toString())) / divisor;
|
|
167
|
+
|
|
168
|
+
return { totalDeposited, depositLimit };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function getJuplendCollateralLtvInfo(vaultConfigData: Buffer): CollateralLtvInfo {
|
|
172
|
+
// VaultConfig layout (packed C):
|
|
173
|
+
// 8 (anchor disc) + 2 vaultId + 2 supplyRateMagnifier (i16) + 2 borrowRateMagnifier (i16)
|
|
174
|
+
// + 2 collateralFactor (u16) + 2 liquidationThreshold (u16)
|
|
175
|
+
// Both factor fields are permille — divide by 10 to get percent. (See JupLend SDK
|
|
176
|
+
// lend.*.mjs liquidationThresholdRatio = ratio * threshold / 1000.)
|
|
177
|
+
const collateralFactor = vaultConfigData.readUInt16LE(8 + 2 + 2 + 2);
|
|
178
|
+
const liquidationThreshold = vaultConfigData.readUInt16LE(8 + 2 + 2 + 2 + 2);
|
|
179
|
+
return {
|
|
180
|
+
loanToValuePct: collateralFactor / 10,
|
|
181
|
+
liquidationThresholdPct: liquidationThreshold / 10,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function getJuplendCollateral(vaultStateData: Buffer): CollateralInfo {
|
|
186
|
+
let off = 8; // discriminator
|
|
187
|
+
off += 2 + 1 + 4 + 4 + 4; // vaultId + branchLiquidated + topmostTick + currentBranchId + totalBranchId
|
|
188
|
+
const totalSupplyRaw = Number(vaultStateData.readBigUInt64LE(off));
|
|
189
|
+
const totalDeposited = totalSupplyRaw / 1e8; // cbBTC has 8 decimals
|
|
190
|
+
return { totalDeposited, depositLimit: 0 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function computeJuplendBorrowRate(data: Buffer): BorrowRateResult {
|
|
194
|
+
let off = 8; // skip discriminator
|
|
195
|
+
off += 32; // mint
|
|
196
|
+
off += 32; // vault
|
|
197
|
+
const borrowRateBps = data.readUInt16LE(off); off += 2;
|
|
198
|
+
off += 2; // feeOnInterest
|
|
199
|
+
off += 2; // lastUtilization
|
|
200
|
+
off += 8; // lastUpdateTimestamp
|
|
201
|
+
const supplyExchangePrice = data.readBigUInt64LE(off); off += 8;
|
|
202
|
+
const borrowExchangePrice = data.readBigUInt64LE(off); off += 8;
|
|
203
|
+
off += 2; // maxUtilization
|
|
204
|
+
const totalSupplyWithInterest = data.readBigUInt64LE(off); off += 8;
|
|
205
|
+
const totalSupplyInterestFree = data.readBigUInt64LE(off); off += 8;
|
|
206
|
+
const totalBorrowWithInterest = data.readBigUInt64LE(off); off += 8;
|
|
207
|
+
const totalBorrowInterestFree = data.readBigUInt64LE(off); off += 8;
|
|
208
|
+
|
|
209
|
+
const totalSupply =
|
|
210
|
+
Number(totalSupplyWithInterest * supplyExchangePrice / JUPLEND_EXCHANGE_PRECISION) +
|
|
211
|
+
Number(totalSupplyInterestFree);
|
|
212
|
+
const totalBorrow =
|
|
213
|
+
Number(totalBorrowWithInterest * borrowExchangePrice / JUPLEND_EXCHANGE_PRECISION) +
|
|
214
|
+
Number(totalBorrowInterestFree);
|
|
215
|
+
|
|
216
|
+
const USDC_DIVISOR = 1e6;
|
|
217
|
+
const availableLiquidity = (totalSupply - totalBorrow) / USDC_DIVISOR;
|
|
218
|
+
const totalBorrowed = totalBorrow / USDC_DIVISOR;
|
|
219
|
+
const utilization = totalSupply > 0 ? (totalBorrow / totalSupply) * 100 : 0;
|
|
220
|
+
|
|
221
|
+
const borrowAPR = borrowRateBps / 100; // percent
|
|
222
|
+
const apr = borrowRateBps / 10000; // as fraction
|
|
223
|
+
const borrowAPY = (Math.pow(1 + apr / JUPLEND_SECONDS_PER_YEAR, JUPLEND_SECONDS_PER_YEAR) - 1) * 100;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
availableLiquidity,
|
|
227
|
+
totalBorrowed,
|
|
228
|
+
utilization,
|
|
229
|
+
borrowAPR,
|
|
230
|
+
borrowAPY,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Supply (deposit) APY for a JupLend liquidity reserve — the lender rate earned
|
|
235
|
+
// by collateral supplied into that reserve. Decodes the same tokenReserve struct
|
|
236
|
+
// as computeJuplendBorrowRate and mirrors the SDK's getLiquidityAssetSupplyRate:
|
|
237
|
+
// supplyRate = borrowRate * (1e4 - fee) * borrowWithInterest
|
|
238
|
+
// / (supplyWithInterest * 1e4)
|
|
239
|
+
// (the JupLend analog of Kamino's borrowAPR * utilization * (1 - fee)).
|
|
240
|
+
export function computeJuplendSupplyRate(data: Buffer): { supplyAPY: number } {
|
|
241
|
+
let off = 8; // skip discriminator
|
|
242
|
+
off += 32; // mint
|
|
243
|
+
off += 32; // vault
|
|
244
|
+
const borrowRateBps = data.readUInt16LE(off); off += 2;
|
|
245
|
+
const feeOnInterestBps = data.readUInt16LE(off); off += 2;
|
|
246
|
+
off += 2; // lastUtilization
|
|
247
|
+
off += 8; // lastUpdateTimestamp
|
|
248
|
+
const supplyExchangePrice = data.readBigUInt64LE(off); off += 8;
|
|
249
|
+
const borrowExchangePrice = data.readBigUInt64LE(off); off += 8;
|
|
250
|
+
off += 2; // maxUtilization
|
|
251
|
+
const totalSupplyWithInterest = data.readBigUInt64LE(off); off += 8;
|
|
252
|
+
off += 8; // totalSupplyInterestFree
|
|
253
|
+
const totalBorrowWithInterest = data.readBigUInt64LE(off); off += 8;
|
|
254
|
+
|
|
255
|
+
const supplyWithInterest =
|
|
256
|
+
(totalSupplyWithInterest * supplyExchangePrice) / JUPLEND_EXCHANGE_PRECISION;
|
|
257
|
+
const borrowWithInterest =
|
|
258
|
+
(totalBorrowWithInterest * borrowExchangePrice) / JUPLEND_EXCHANGE_PRECISION;
|
|
259
|
+
if (supplyWithInterest <= BigInt(0)) return { supplyAPY: 0 };
|
|
260
|
+
|
|
261
|
+
const supplyRateBps =
|
|
262
|
+
borrowRateBps *
|
|
263
|
+
(1 - feeOnInterestBps / 10000) *
|
|
264
|
+
(Number(borrowWithInterest) / Number(supplyWithInterest));
|
|
265
|
+
|
|
266
|
+
const apr = supplyRateBps / 10000; // as fraction
|
|
267
|
+
const supplyAPY =
|
|
268
|
+
(Math.pow(1 + apr / JUPLEND_SECONDS_PER_YEAR, JUPLEND_SECONDS_PER_YEAR) - 1) * 100;
|
|
269
|
+
return { supplyAPY };
|
|
270
|
+
}
|