@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
@@ -0,0 +1,131 @@
1
+ import { Connection, PublicKey } from "@solana/web3.js";
2
+ import {
3
+ DEFAULT_LENDING_PROVIDER,
4
+ LENDING_PROVIDER_JUPLEND,
5
+ XSTOCKS_MARKET,
6
+ } from "./constants";
7
+ import {
8
+ parseObligationDebt,
9
+ parseObligationCTokens,
10
+ cTokensToUnderlying,
11
+ } from "./kamino/obligationParser";
12
+ import {
13
+ XSTOCKS_MARKET_CONFIG,
14
+ resolveMainMarketConfigByMint,
15
+ type KaminoMarketConfig,
16
+ } from "./marketConfig";
17
+ import { fetchUserState, type CurrentVaultType } from "./userState";
18
+
19
+ export type Lender = "juplend" | "kamino";
20
+
21
+ /**
22
+ * A user's Hobba position for one collateral asset, in base units
23
+ * (collateral in lamports/satoshis, debt in micro-USDC). Amounts are decimal
24
+ * strings so they survive JSON and avoid float drift at the integration
25
+ * boundary.
26
+ */
27
+ export interface HobbaPosition {
28
+ /** false → no UserState exists for this (owner, collateral). */
29
+ exists: boolean;
30
+ /** Active lender; for new users this is the default lender init() would pick. */
31
+ lender: Lender;
32
+ /** Kamino lending market pubkey (distinguishes Main vs xStocks). */
33
+ lendingMarket?: string;
34
+ /** Deposited collateral, base units. */
35
+ collateral: string;
36
+ /** Total obligation debt, micro-USDC (user + operator-deployed). */
37
+ debtUsdc: string;
38
+ /** Operator-deployed capital, micro-USDC. */
39
+ depositedToVault: string;
40
+ /** max(0, debtUsdc - depositedToVault) — what UIs show as the user's loan. */
41
+ userDebtUsdc: string;
42
+ currentVault: CurrentVaultType;
43
+ /** Obligation (Kamino) / position (JupLend) address. */
44
+ obligation?: string;
45
+ }
46
+
47
+ export function resolveKaminoMarketConfig(
48
+ lendingMarket: PublicKey,
49
+ collateralMint: PublicKey,
50
+ ): KaminoMarketConfig {
51
+ return lendingMarket.equals(XSTOCKS_MARKET)
52
+ ? XSTOCKS_MARKET_CONFIG
53
+ : resolveMainMarketConfigByMint(collateralMint);
54
+ }
55
+
56
+ const EMPTY_POSITION = (lender: Lender): HobbaPosition => ({
57
+ exists: false,
58
+ lender,
59
+ collateral: "0",
60
+ debtUsdc: "0",
61
+ depositedToVault: "0",
62
+ userDebtUsdc: "0",
63
+ currentVault: "none",
64
+ });
65
+
66
+ export interface GetPositionParams {
67
+ connection: Connection;
68
+ owner: PublicKey;
69
+ collateralMint: PublicKey;
70
+ /** Hobba backend base URL serving /api/juplend-position. "" = same-origin. */
71
+ apiBaseUrl?: string;
72
+ }
73
+
74
+ /**
75
+ * Headless position read: UserState → lender routing → obligation (Kamino,
76
+ * parsed client-side) or JupLend position (via the backend, which handles the
77
+ * tick-based debt computation). Mirrors the app's useUserState +
78
+ * useObligationData hooks.
79
+ */
80
+ export async function getPosition(params: GetPositionParams): Promise<HobbaPosition> {
81
+ const { connection, owner, collateralMint } = params;
82
+ const base = (params.apiBaseUrl ?? "").replace(/\/+$/, "");
83
+
84
+ const userState = await fetchUserState(connection, owner, collateralMint);
85
+ if (!userState) {
86
+ return EMPTY_POSITION(
87
+ DEFAULT_LENDING_PROVIDER === LENDING_PROVIDER_JUPLEND ? "juplend" : "kamino",
88
+ );
89
+ }
90
+
91
+ const isJuplend = userState.lendingProvider === LENDING_PROVIDER_JUPLEND;
92
+ let collateral = "0";
93
+ let debtUsdc = "0";
94
+
95
+ if (isJuplend) {
96
+ const res = await fetch(
97
+ `${base}/api/juplend-position?address=${userState.obligation.toBase58()}`,
98
+ );
99
+ if (!res.ok) throw new Error(`juplend-position fetch failed: ${res.status}`);
100
+ const data = await res.json();
101
+ collateral = String(data.collateral ?? "0");
102
+ debtUsdc = String(data.debt ?? "0");
103
+ } else {
104
+ const mc = resolveKaminoMarketConfig(userState.lendingMarket, collateralMint);
105
+ const [obligationInfo, reserveInfo] = await Promise.all([
106
+ connection.getAccountInfo(userState.obligation),
107
+ connection.getAccountInfo(mc.collateralReserve),
108
+ ]);
109
+ if (obligationInfo) {
110
+ debtUsdc = String(parseObligationDebt(obligationInfo.data as Buffer));
111
+ const cTokens = parseObligationCTokens(obligationInfo.data as Buffer);
112
+ collateral = String(
113
+ reserveInfo ? cTokensToUnderlying(cTokens, reserveInfo.data as Buffer) : cTokens,
114
+ );
115
+ }
116
+ }
117
+
118
+ const userDebt = Math.max(0, Number(debtUsdc) - userState.depositedToVault);
119
+
120
+ return {
121
+ exists: true,
122
+ lender: isJuplend ? "juplend" : "kamino",
123
+ lendingMarket: isJuplend ? undefined : userState.lendingMarket.toBase58(),
124
+ collateral,
125
+ debtUsdc,
126
+ depositedToVault: String(userState.depositedToVault),
127
+ userDebtUsdc: String(userDebt),
128
+ currentVault: userState.currentVaultType,
129
+ obligation: userState.obligation.toBase58(),
130
+ };
131
+ }
@@ -0,0 +1,387 @@
1
+ import { Connection, PublicKey } from "@solana/web3.js";
2
+ import { collateralDecimals, collateralLabel } from "./amountUtils";
3
+ import { XSTOCKS_MARKET } from "./constants";
4
+ import { getPosition, type HobbaPosition, type Lender } from "./position";
5
+
6
+ /**
7
+ * Display-ready dashboard metrics for a Hobba position — the exact values
8
+ * app.hobba.io renders, computed with the same logic (ported verbatim from the
9
+ * app's Dashboard.tsx). Partners render these without knowing Hobba's rate /
10
+ * operator / dust-threshold rules.
11
+ */
12
+ export interface HobbaPositionMetrics {
13
+ position: HobbaPosition;
14
+ lender: Lender;
15
+
16
+ /** Collateral price used for every USD figure below. */
17
+ collateralPriceUsd: number;
18
+ collateralUi: number;
19
+ collateralUsd: number;
20
+ /** User-visible loan (total debt minus operator-deployed capital), UI units. */
21
+ loanUsdc: number;
22
+
23
+ currentLtvPct: number;
24
+ ltvStatus: "HEALTHY" | "MODERATE" | "NEEDS_ATTENTION";
25
+ liquidationLtvPct: number;
26
+ /** null when the position is effectively just earning (no meaningful debt). */
27
+ liquidationPriceUsd: number | null;
28
+ dropToLiquidationPct: number | null;
29
+
30
+ /** Big Supply APY figure (earn APY once the loan is effectively cleared). */
31
+ supplyApyPct: number;
32
+ /** Big Loan APY figure; null renders as "—" (no effective debt). */
33
+ loanApyPct: number | null;
34
+ /** True → "Smart Earning" badge; false → "Reducing Interest". */
35
+ isEarning: boolean;
36
+ /**
37
+ * True when the position renders as effectively debt-free (earn view or
38
+ * dust-offset loan) — the state in which loanApyPct is null and LTV/loan
39
+ * figures display as zero.
40
+ */
41
+ showZeroDebt: boolean;
42
+ /** Operator debt used in the APY math (optimistic target-LTV projection), UI USDC. */
43
+ operatorBorrowedUsdc: number;
44
+ /** Estimated self-repay date (dd/mm/yyyy) when loan APY is negative. */
45
+ selfRepayDate: string | null;
46
+
47
+ estimatedEarningsPerYearUsd: number;
48
+ borrowLimitUsd: number;
49
+ borrowLimitUsedUsd: number;
50
+ borrowLimitUsedPct: number;
51
+
52
+ // "More info" block
53
+ lendingSource: string; // "JupLend" | "Kamino Main" | "Kamino XStocks" | "—"
54
+ borrowApyPct: number;
55
+ yieldVault: string; // "Perena USD*" | "Allez USDC" | … | "—"
56
+ vaultSupplyApyPct: number;
57
+ capitalEarningYieldUsdc: number;
58
+
59
+ // Tooltip-level breakdown (per day, USD) so partner UIs can mirror Hobba's
60
+ // Supply/Loan APY tooltips without re-deriving anything.
61
+ dailyBorrowCostUsd: number;
62
+ dailyEngineEarningsUsd: number;
63
+ collateralSupplyApyPct: number;
64
+ collateralRewardsApyPct: number;
65
+ }
66
+
67
+ const VAULT_NAME_MAP: Record<string, string> = {
68
+ allez: "Allez USDC",
69
+ prime: "Prime USDC",
70
+ rockawayRWA: "RockawayRWA USDC",
71
+ perena: "Perena USDC",
72
+ };
73
+
74
+ const VAULT_DISPLAY_MAP: Record<string, string> = {
75
+ allez: "Allez USDC",
76
+ prime: "Prime USDC",
77
+ rockawayRWA: "RockawayRWA USDC",
78
+ perena: "Perena USD*",
79
+ };
80
+
81
+ export interface RatesPayload {
82
+ markets: { name: string; borrowAPY: number; error?: string }[];
83
+ collateralSupplyAPYs: Record<string, number>;
84
+ juplendCollateralSupplyAPYs: Record<string, number>;
85
+ juplendCollateralRewardsAPYs: Record<string, number>;
86
+ collateralLtvs: Record<string, { loanToValuePct: number; liquidationThresholdPct: number }>;
87
+ juplendCollateralLtvs: Record<string, { loanToValuePct: number; liquidationThresholdPct: number }>;
88
+ }
89
+
90
+ export interface SupplyRatesPayload {
91
+ vaults: { name: string; supplyAPY: number }[];
92
+ }
93
+
94
+ export async function fetchRatesPayload(base: string): Promise<RatesPayload> {
95
+ const res = await fetch(`${base}/api/rates`);
96
+ if (!res.ok) throw new Error(`rates fetch failed: ${res.status}`);
97
+ const data = await res.json();
98
+ return {
99
+ markets: (data.markets ?? []).filter((m: any) => !m.error),
100
+ collateralSupplyAPYs: data.collateralSupplyAPYs ?? {},
101
+ juplendCollateralSupplyAPYs: data.juplendCollateralSupplyAPYs ?? {},
102
+ juplendCollateralRewardsAPYs: data.juplendCollateralRewardsAPYs ?? {},
103
+ collateralLtvs: data.collateralLtvs ?? {},
104
+ juplendCollateralLtvs: data.juplendCollateralLtvs ?? {},
105
+ };
106
+ }
107
+
108
+ export async function fetchSupplyRatesPayload(base: string): Promise<SupplyRatesPayload> {
109
+ const res = await fetch(`${base}/api/supply-rates`);
110
+ if (!res.ok) throw new Error(`supply-rates fetch failed: ${res.status}`);
111
+ const data = await res.json();
112
+ return { vaults: (data.vaults ?? []).filter((v: any) => !v.error && v.supplyAPY > 0) };
113
+ }
114
+
115
+ export async function fetchCollateralPrice(base: string, label: string): Promise<number> {
116
+ const route = label === "SOL" ? "sol-price" : "btc-price";
117
+ const res = await fetch(`${base}/api/${route}`);
118
+ if (!res.ok) throw new Error(`${route} fetch failed: ${res.status}`);
119
+ const data = await res.json();
120
+ const price = Number(data.price);
121
+ if (!Number.isFinite(price) || price <= 0) throw new Error(`${route} returned no price`);
122
+ return price;
123
+ }
124
+
125
+ export function bestMarketName(rates: RatesPayload): string | null {
126
+ let best: string | null = null;
127
+ let bestApy = Infinity;
128
+ for (const m of rates.markets) {
129
+ if (m.borrowAPY < bestApy) {
130
+ bestApy = m.borrowAPY;
131
+ best = m.name;
132
+ }
133
+ }
134
+ return best;
135
+ }
136
+
137
+ export interface GetPositionMetricsParams {
138
+ connection: Connection;
139
+ owner: PublicKey;
140
+ collateralMint: PublicKey;
141
+ apiBaseUrl?: string;
142
+ }
143
+
144
+ export async function getPositionMetrics(
145
+ params: GetPositionMetricsParams,
146
+ ): Promise<HobbaPositionMetrics> {
147
+ const { connection, owner, collateralMint } = params;
148
+ const base = (params.apiBaseUrl ?? "").replace(/\/+$/, "");
149
+ const label = collateralLabel(collateralMint);
150
+
151
+ const [position, rates, supplyRates, collateralPrice] = await Promise.all([
152
+ getPosition({ connection, owner, collateralMint, apiBaseUrl: base }),
153
+ fetchRatesPayload(base),
154
+ fetchSupplyRatesPayload(base),
155
+ fetchCollateralPrice(base, label),
156
+ ]);
157
+
158
+ return computePositionMetrics({
159
+ position,
160
+ rates,
161
+ supplyRates,
162
+ collateralPriceUsd: collateralPrice,
163
+ collateralMint,
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Inputs for the pure metric computation. The app feeds these from its own
169
+ * cached hooks (per-keystroke/optimistic refresh paths); `getPositionMetrics`
170
+ * feeds them from the backend fetches above. Same math either way.
171
+ */
172
+ export interface PositionMetricsInputs {
173
+ position: HobbaPosition;
174
+ rates: RatesPayload;
175
+ supplyRates: SupplyRatesPayload;
176
+ collateralPriceUsd: number;
177
+ collateralMint: PublicKey;
178
+ /**
179
+ * Pending-harvest yield not yet applied on-chain (micro-USDC), netted off
180
+ * the user-visible debt. The headless read passes 0 (projection requires an
181
+ * extra chain read); the app passes its useJuplendHarvestProjection value.
182
+ */
183
+ earnedNotAppliedMicro?: number;
184
+ }
185
+
186
+ export function computePositionMetrics(
187
+ inputs: PositionMetricsInputs,
188
+ ): HobbaPositionMetrics {
189
+ const {
190
+ position,
191
+ rates,
192
+ supplyRates,
193
+ collateralPriceUsd: collateralPrice,
194
+ collateralMint,
195
+ earnedNotAppliedMicro = 0,
196
+ } = inputs;
197
+ const label = collateralLabel(collateralMint);
198
+ const decimals = collateralDecimals(collateralMint);
199
+
200
+ // --- Ported verbatim from app Dashboard.tsx ---
201
+ const collateral = Number(position.collateral);
202
+ const totalDebt = Number(position.debtUsdc);
203
+ const depositedToVault = Number(position.depositedToVault);
204
+ // NOTE: earnedNotAppliedMicro is NOT netted here — the app keeps userDebt
205
+ // (and so LTV / loan APY) on the on-chain figure and only uses the pending
206
+ // harvest to decide the earn-view threshold below.
207
+ const userDebt = Math.max(0, totalDebt - depositedToVault);
208
+
209
+ const divisor = Math.pow(10, decimals);
210
+ const collateralUsd = (collateral / divisor) * collateralPrice;
211
+ const ltv = collateralUsd > 0 ? (userDebt / 1e6 / collateralUsd) * 100 : 0;
212
+
213
+ const isJuplendLender = position.exists
214
+ ? position.lender === "juplend"
215
+ : bestMarketName(rates) === "JupLend";
216
+
217
+ const borrowSource = !position.exists
218
+ ? "—"
219
+ : position.lender === "juplend"
220
+ ? "JupLend"
221
+ : position.lendingMarket === XSTOCKS_MARKET.toBase58()
222
+ ? "Kamino XStocks"
223
+ : "Kamino Main";
224
+
225
+ const borrowMarketNameForRates = !position.exists
226
+ ? "Kamino Main Market"
227
+ : position.lender === "juplend"
228
+ ? "JupLend"
229
+ : position.lendingMarket === XSTOCKS_MARKET.toBase58()
230
+ ? "Kamino xStocks"
231
+ : "Kamino Main Market";
232
+ const borrowMarket = rates.markets.find((m) => m.name === borrowMarketNameForRates);
233
+ const borrowAPY = borrowMarket?.borrowAPY ?? 0;
234
+
235
+ const vaultName = VAULT_NAME_MAP[position.currentVault] ?? "";
236
+ const supplyVault = supplyRates.vaults.find((v) => v.name === vaultName);
237
+ const currentVaultAPY = supplyVault?.supplyAPY ?? 0;
238
+ // Assumed default vault when no position exists yet: Perena USD*.
239
+ const perenaVaultAPY = supplyRates.vaults.find((v) => v.name === "Perena USDC")?.supplyAPY ?? 0;
240
+ const vaultAPY = currentVaultAPY > 0 ? currentVaultAPY : perenaVaultAPY;
241
+
242
+ const collateralBaseSupplyAPY =
243
+ (isJuplendLender
244
+ ? rates.juplendCollateralSupplyAPYs[label]
245
+ : rates.collateralSupplyAPYs[label]) ?? 0;
246
+ const collateralRewardsAPY = isJuplendLender
247
+ ? (rates.juplendCollateralRewardsAPYs[label] ?? 0)
248
+ : 0;
249
+ const collateralSupplyAPY = collateralBaseSupplyAPY + collateralRewardsAPY;
250
+
251
+ const collateralLtv = isJuplendLender
252
+ ? rates.juplendCollateralLtvs[label]
253
+ : rates.collateralLtvs[label];
254
+ const liquidationLtvPct = collateralLtv?.liquidationThresholdPct ?? 85;
255
+
256
+ const userBorrowed = userDebt / 1e6;
257
+ const actualOperatorBorrowed = depositedToVault / 1e6;
258
+
259
+ // Optimistic operator debt: what the operator WOULD borrow to reach the
260
+ // per-asset target LTV (65% cbBTC, 60% SOL).
261
+ const TARGET_LTV = label === "SOL" ? 0.6 : 0.65;
262
+ const optimisticOperatorBorrowed = (() => {
263
+ if (!collateralUsd || collateralUsd <= 0) return actualOperatorBorrowed;
264
+ if (vaultAPY <= borrowAPY) return 0;
265
+ const targetTotalDebt = collateralUsd * TARGET_LTV;
266
+ const projected = Math.max(0, targetTotalDebt - userBorrowed);
267
+ return Math.max(actualOperatorBorrowed, projected);
268
+ })();
269
+ const operatorBorrowed = optimisticOperatorBorrowed;
270
+
271
+ const operatorHasNotBorrowed = depositedToVault === 0;
272
+ const hasDeposit = collateralUsd > 0;
273
+ const willDeployToVault = hasDeposit && operatorHasNotBorrowed && borrowAPY < vaultAPY;
274
+ const capitalEarningYield = willDeployToVault ? operatorBorrowed : depositedToVault / 1e6;
275
+ const displayVaultType =
276
+ position.currentVault !== "none"
277
+ ? position.currentVault
278
+ : willDeployToVault
279
+ ? "perena"
280
+ : position.currentVault;
281
+
282
+ const loanAPY =
283
+ userBorrowed > 0
284
+ ? ((userBorrowed + operatorBorrowed) * borrowAPY - operatorBorrowed * vaultAPY) /
285
+ userBorrowed
286
+ : null;
287
+
288
+ const currentPrice = collateralPrice;
289
+ const liquidationPrice =
290
+ currentPrice > 0 && ltv > 0 && liquidationLtvPct > 0
291
+ ? currentPrice * (ltv / liquidationLtvPct)
292
+ : 0;
293
+ const buffer =
294
+ currentPrice > 0 && liquidationPrice > 0
295
+ ? Math.max(0, ((currentPrice - liquidationPrice) / currentPrice) * 100)
296
+ : 100;
297
+
298
+ const ltvStatus: HobbaPositionMetrics["ltvStatus"] =
299
+ ltv > 75 ? "NEEDS_ATTENTION" : ltv > 50 ? "MODERATE" : "HEALTHY";
300
+
301
+ // Dust-offset loan: tiny outstanding debt more than covered by net yield.
302
+ const hobbaDailyEarning = (operatorBorrowed * vaultAPY) / 100 / 365;
303
+ const dailyBorrowCost = ((userBorrowed + operatorBorrowed) * borrowAPY) / 100 / 365;
304
+ const netDailyEarning = hobbaDailyEarning - dailyBorrowCost;
305
+
306
+ const DUST_DEBT_THRESHOLD_USDC = 0.01;
307
+ const isDustOffsetLoan =
308
+ userBorrowed < DUST_DEBT_THRESHOLD_USDC && netDailyEarning > userBorrowed;
309
+
310
+ // Earn view: once the yield already earned-but-not-yet-applied (pending
311
+ // harvest) brings residual debt under 0.1 USDC, the position is effectively
312
+ // just earning. Matches app Dashboard.tsx: only this threshold test nets the
313
+ // pending harvest; every other figure stays on the on-chain debt.
314
+ const EARN_VIEW_THRESHOLD_USDC = 0.1;
315
+ const residualDebtMicro = Math.max(0, userDebt - earnedNotAppliedMicro);
316
+ const isEarnView = residualDebtMicro < EARN_VIEW_THRESHOLD_USDC * 1e6;
317
+ const showZeroDebt = isEarnView || isDustOffsetLoan;
318
+
319
+ const hobbaSupplyContribution =
320
+ isDustOffsetLoan && collateralUsd > 0 ? ((netDailyEarning * 365) / collateralUsd) * 100 : 0;
321
+
322
+ const displaySupplyAPY = collateralSupplyAPY + hobbaSupplyContribution;
323
+ const displayLoanAPY: number | null = showZeroDebt ? null : loanAPY;
324
+ const displayUserBorrowed = showZeroDebt ? 0 : userBorrowed;
325
+
326
+ const estimatedAnnualEarnings =
327
+ operatorBorrowed > 0 && vaultAPY > borrowAPY
328
+ ? (operatorBorrowed * (vaultAPY - borrowAPY)) / 100
329
+ : 0;
330
+ const earnApy =
331
+ collateralSupplyAPY + (collateralUsd > 0 ? (estimatedAnnualEarnings / collateralUsd) * 100 : 0);
332
+
333
+ // Self-repay estimate when the net loan APY is negative.
334
+ let selfRepayDate: string | null = null;
335
+ if (displayLoanAPY !== null && displayLoanAPY < 0) {
336
+ const yearsToRepay = 1 / Math.abs(displayLoanAPY / 100);
337
+ if (yearsToRepay <= 10) {
338
+ const daysToRepay = yearsToRepay * 365;
339
+ const repayDate = new Date();
340
+ repayDate.setDate(repayDate.getDate() + Math.ceil(daysToRepay));
341
+ selfRepayDate = `${repayDate.getDate().toString().padStart(2, "0")}/${(repayDate.getMonth() + 1)
342
+ .toString()
343
+ .padStart(2, "0")}/${repayDate.getFullYear()}`;
344
+ }
345
+ }
346
+
347
+ const borrowLimit = collateralUsd * 0.5;
348
+ const borrowLimitUsed = borrowLimit > 0 ? (displayUserBorrowed / borrowLimit) * 100 : 0;
349
+
350
+ return {
351
+ position,
352
+ lender: position.lender,
353
+ collateralPriceUsd: collateralPrice,
354
+ collateralUi: collateral / divisor,
355
+ collateralUsd,
356
+ loanUsdc: displayUserBorrowed,
357
+ currentLtvPct: showZeroDebt ? 0 : ltv,
358
+ ltvStatus,
359
+ liquidationLtvPct,
360
+ liquidationPriceUsd: isEarnView || liquidationPrice <= 0 ? null : liquidationPrice,
361
+ dropToLiquidationPct: isEarnView || liquidationPrice <= 0 ? null : buffer,
362
+ supplyApyPct: showZeroDebt ? earnApy : displaySupplyAPY,
363
+ loanApyPct: displayLoanAPY,
364
+ isEarning: showZeroDebt || (displayLoanAPY !== null && displayLoanAPY <= 0),
365
+ showZeroDebt,
366
+ operatorBorrowedUsdc: operatorBorrowed,
367
+ selfRepayDate,
368
+ estimatedEarningsPerYearUsd: estimatedAnnualEarnings,
369
+ borrowLimitUsd: borrowLimit,
370
+ borrowLimitUsedUsd: displayUserBorrowed,
371
+ borrowLimitUsedPct: borrowLimitUsed,
372
+ lendingSource: borrowSource,
373
+ borrowApyPct: borrowAPY,
374
+ yieldVault:
375
+ borrowSource === "—"
376
+ ? "—"
377
+ : !displayVaultType || displayVaultType === "none"
378
+ ? "Perena USD*"
379
+ : (VAULT_DISPLAY_MAP[displayVaultType] ?? displayVaultType),
380
+ vaultSupplyApyPct: vaultAPY,
381
+ capitalEarningYieldUsdc: capitalEarningYield,
382
+ dailyBorrowCostUsd: dailyBorrowCost,
383
+ dailyEngineEarningsUsd: hobbaDailyEarning,
384
+ collateralSupplyApyPct: collateralSupplyAPY,
385
+ collateralRewardsApyPct: collateralRewardsAPY,
386
+ };
387
+ }
package/src/program.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { AnchorProvider, Program } from "@coral-xyz/anchor";
2
+ import {
3
+ Connection,
4
+ PublicKey,
5
+ Transaction,
6
+ VersionedTransaction,
7
+ } from "@solana/web3.js";
8
+ import idl from "./idl/hobba.json";
9
+ import { PROGRAM_ID } from "./constants";
10
+
11
+ /**
12
+ * Build the Hobba wrapper Anchor `Program` for instruction-building only.
13
+ *
14
+ * `.methods.x().instruction()` never signs, so a no-op signing wallet is enough;
15
+ * real signing happens later in `transactionBuilder` via the caller's HobbaSigner.
16
+ * `PROGRAM_ID` is selected from `NEXT_PUBLIC_MAINNET`/`NEXT_PUBLIC_STAGING` at
17
+ * build/runtime (see constants.ts), matching the app's AnchorProvider.
18
+ */
19
+ export function buildHobbaProgram(connection: Connection, owner: PublicKey): Program {
20
+ const noopWallet = {
21
+ publicKey: owner,
22
+ signTransaction: async <T extends Transaction | VersionedTransaction>(tx: T) => tx,
23
+ signAllTransactions: async <T extends Transaction | VersionedTransaction>(txs: T[]) => txs,
24
+ };
25
+ const provider = new AnchorProvider(connection, noopWallet as any, {
26
+ commitment: "confirmed",
27
+ });
28
+ const idlWithAddress = { ...(idl as any), address: PROGRAM_ID.toBase58() };
29
+ return new Program(idlWithAddress as any, provider);
30
+ }