@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.
Files changed (190) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +87 -0
  3. package/dist/amountUtils.d.ts +15 -0
  4. package/dist/amountUtils.d.ts.map +1 -0
  5. package/dist/amountUtils.js +59 -0
  6. package/dist/amountUtils.js.map +1 -0
  7. package/dist/constants.d.ts +148 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +231 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/errors.d.ts +7 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +29 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/executeAction.d.ts +36 -0
  16. package/dist/executeAction.d.ts.map +1 -0
  17. package/dist/executeAction.js +606 -0
  18. package/dist/executeAction.js.map +1 -0
  19. package/dist/executeDepositBorrow.d.ts +49 -0
  20. package/dist/executeDepositBorrow.d.ts.map +1 -0
  21. package/dist/executeDepositBorrow.js +444 -0
  22. package/dist/executeDepositBorrow.js.map +1 -0
  23. package/dist/idl/hobba.json +5072 -0
  24. package/dist/idl.d.ts +783 -0
  25. package/dist/idl.d.ts.map +1 -0
  26. package/dist/idl.js +7 -0
  27. package/dist/idl.js.map +1 -0
  28. package/dist/index.d.ts +17 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +22 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/juplend/accountResolvers.d.ts +106 -0
  33. package/dist/juplend/accountResolvers.d.ts.map +1 -0
  34. package/dist/juplend/accountResolvers.js +148 -0
  35. package/dist/juplend/accountResolvers.js.map +1 -0
  36. package/dist/juplend/operateRemaining.d.ts +11 -0
  37. package/dist/juplend/operateRemaining.d.ts.map +1 -0
  38. package/dist/juplend/operateRemaining.js +46 -0
  39. package/dist/juplend/operateRemaining.js.map +1 -0
  40. package/dist/juplend/positionInit.d.ts +34 -0
  41. package/dist/juplend/positionInit.d.ts.map +1 -0
  42. package/dist/juplend/positionInit.js +56 -0
  43. package/dist/juplend/positionInit.js.map +1 -0
  44. package/dist/juplend/positionParser.d.ts +8 -0
  45. package/dist/juplend/positionParser.d.ts.map +1 -0
  46. package/dist/juplend/positionParser.js +30 -0
  47. package/dist/juplend/positionParser.js.map +1 -0
  48. package/dist/juplend/sdkApi.d.ts +39 -0
  49. package/dist/juplend/sdkApi.d.ts.map +1 -0
  50. package/dist/juplend/sdkApi.js +68 -0
  51. package/dist/juplend/sdkApi.js.map +1 -0
  52. package/dist/juplend/tickMath.d.ts +19 -0
  53. package/dist/juplend/tickMath.d.ts.map +1 -0
  54. package/dist/juplend/tickMath.js +91 -0
  55. package/dist/juplend/tickMath.js.map +1 -0
  56. package/dist/juplend/tickUtils.d.ts +20 -0
  57. package/dist/juplend/tickUtils.d.ts.map +1 -0
  58. package/dist/juplend/tickUtils.js +115 -0
  59. package/dist/juplend/tickUtils.js.map +1 -0
  60. package/dist/juplend/vaultConfigParser.d.ts +25 -0
  61. package/dist/juplend/vaultConfigParser.d.ts.map +1 -0
  62. package/dist/juplend/vaultConfigParser.js +38 -0
  63. package/dist/juplend/vaultConfigParser.js.map +1 -0
  64. package/dist/kamino/accountResolvers.d.ts +110 -0
  65. package/dist/kamino/accountResolvers.d.ts.map +1 -0
  66. package/dist/kamino/accountResolvers.js +220 -0
  67. package/dist/kamino/accountResolvers.js.map +1 -0
  68. package/dist/kamino/obligationParser.d.ts +13 -0
  69. package/dist/kamino/obligationParser.d.ts.map +1 -0
  70. package/dist/kamino/obligationParser.js +92 -0
  71. package/dist/kamino/obligationParser.js.map +1 -0
  72. package/dist/kamino/refreshInstructions.d.ts +4 -0
  73. package/dist/kamino/refreshInstructions.d.ts.map +1 -0
  74. package/dist/kamino/refreshInstructions.js +32 -0
  75. package/dist/kamino/refreshInstructions.js.map +1 -0
  76. package/dist/limits.d.ts +87 -0
  77. package/dist/limits.d.ts.map +1 -0
  78. package/dist/limits.js +174 -0
  79. package/dist/limits.js.map +1 -0
  80. package/dist/marketConfig.d.ts +35 -0
  81. package/dist/marketConfig.d.ts.map +1 -0
  82. package/dist/marketConfig.js +85 -0
  83. package/dist/marketConfig.js.map +1 -0
  84. package/dist/oracle/scopePrice.d.ts +7 -0
  85. package/dist/oracle/scopePrice.d.ts.map +1 -0
  86. package/dist/oracle/scopePrice.js +31 -0
  87. package/dist/oracle/scopePrice.js.map +1 -0
  88. package/dist/pdas.d.ts +33 -0
  89. package/dist/pdas.d.ts.map +1 -0
  90. package/dist/pdas.js +116 -0
  91. package/dist/pdas.js.map +1 -0
  92. package/dist/position.d.ts +45 -0
  93. package/dist/position.d.ts.map +1 -0
  94. package/dist/position.js +68 -0
  95. package/dist/position.js.map +1 -0
  96. package/dist/positionMetrics.d.ts +108 -0
  97. package/dist/positionMetrics.d.ts.map +1 -0
  98. package/dist/positionMetrics.js +238 -0
  99. package/dist/positionMetrics.js.map +1 -0
  100. package/dist/program.d.ts +12 -0
  101. package/dist/program.d.ts.map +1 -0
  102. package/dist/program.js +24 -0
  103. package/dist/program.js.map +1 -0
  104. package/dist/quote.d.ts +90 -0
  105. package/dist/quote.d.ts.map +1 -0
  106. package/dist/quote.js +198 -0
  107. package/dist/quote.js.map +1 -0
  108. package/dist/rates/borrowRates.d.ts +44 -0
  109. package/dist/rates/borrowRates.d.ts.map +1 -0
  110. package/dist/rates/borrowRates.js +229 -0
  111. package/dist/rates/borrowRates.js.map +1 -0
  112. package/dist/rates/collateralInfo.d.ts +21 -0
  113. package/dist/rates/collateralInfo.d.ts.map +1 -0
  114. package/dist/rates/collateralInfo.js +104 -0
  115. package/dist/rates/collateralInfo.js.map +1 -0
  116. package/dist/rates/constants.d.ts +22 -0
  117. package/dist/rates/constants.d.ts.map +1 -0
  118. package/dist/rates/constants.js +70 -0
  119. package/dist/rates/constants.js.map +1 -0
  120. package/dist/rates/index.d.ts +11 -0
  121. package/dist/rates/index.d.ts.map +1 -0
  122. package/dist/rates/index.js +11 -0
  123. package/dist/rates/index.js.map +1 -0
  124. package/dist/rates/kaminoApi.d.ts +24 -0
  125. package/dist/rates/kaminoApi.d.ts.map +1 -0
  126. package/dist/rates/kaminoApi.js +98 -0
  127. package/dist/rates/kaminoApi.js.map +1 -0
  128. package/dist/rates/supplyRates.d.ts +23 -0
  129. package/dist/rates/supplyRates.d.ts.map +1 -0
  130. package/dist/rates/supplyRates.js +67 -0
  131. package/dist/rates/supplyRates.js.map +1 -0
  132. package/dist/transactionBuilder.d.ts +5 -0
  133. package/dist/transactionBuilder.d.ts.map +1 -0
  134. package/dist/transactionBuilder.js +287 -0
  135. package/dist/transactionBuilder.js.map +1 -0
  136. package/dist/txHelpers.d.ts +16 -0
  137. package/dist/txHelpers.d.ts.map +1 -0
  138. package/dist/txHelpers.js +82 -0
  139. package/dist/txHelpers.js.map +1 -0
  140. package/dist/userState.d.ts +21 -0
  141. package/dist/userState.d.ts.map +1 -0
  142. package/dist/userState.js +60 -0
  143. package/dist/userState.js.map +1 -0
  144. package/dist/vault/vaultAccounts.d.ts +82 -0
  145. package/dist/vault/vaultAccounts.d.ts.map +1 -0
  146. package/dist/vault/vaultAccounts.js +275 -0
  147. package/dist/vault/vaultAccounts.js.map +1 -0
  148. package/dist/wallet.d.ts +12 -0
  149. package/dist/wallet.d.ts.map +1 -0
  150. package/dist/wallet.js +2 -0
  151. package/dist/wallet.js.map +1 -0
  152. package/package.json +63 -0
  153. package/src/amountUtils.ts +69 -0
  154. package/src/constants.ts +263 -0
  155. package/src/errors.ts +33 -0
  156. package/src/executeAction.ts +884 -0
  157. package/src/executeDepositBorrow.ts +677 -0
  158. package/src/idl/hobba.json +5072 -0
  159. package/src/idl.ts +7 -0
  160. package/src/index.ts +21 -0
  161. package/src/juplend/accountResolvers.ts +207 -0
  162. package/src/juplend/operateRemaining.ts +81 -0
  163. package/src/juplend/positionInit.ts +86 -0
  164. package/src/juplend/positionParser.ts +41 -0
  165. package/src/juplend/sdkApi.ts +112 -0
  166. package/src/juplend/tickMath.ts +82 -0
  167. package/src/juplend/tickUtils.ts +163 -0
  168. package/src/juplend/vaultConfigParser.ts +45 -0
  169. package/src/kamino/accountResolvers.ts +299 -0
  170. package/src/kamino/obligationParser.ts +123 -0
  171. package/src/kamino/refreshInstructions.ts +45 -0
  172. package/src/limits.ts +222 -0
  173. package/src/marketConfig.ts +139 -0
  174. package/src/oracle/scopePrice.ts +31 -0
  175. package/src/pdas.ts +226 -0
  176. package/src/position.ts +131 -0
  177. package/src/positionMetrics.ts +387 -0
  178. package/src/program.ts +30 -0
  179. package/src/quote.ts +310 -0
  180. package/src/rates/borrowRates.ts +270 -0
  181. package/src/rates/collateralInfo.ts +136 -0
  182. package/src/rates/constants.ts +87 -0
  183. package/src/rates/index.ts +38 -0
  184. package/src/rates/kaminoApi.ts +134 -0
  185. package/src/rates/supplyRates.ts +102 -0
  186. package/src/transactionBuilder.ts +324 -0
  187. package/src/txHelpers.ts +107 -0
  188. package/src/userState.ts +91 -0
  189. package/src/vault/vaultAccounts.ts +365 -0
  190. 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
+ }