@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/position.ts
ADDED
|
@@ -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
|
+
}
|