Files
assetx/webapp/hooks/useHealthFactor.ts
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

451 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useAccount, useReadContract, useReadContracts } from 'wagmi'
import { formatUnits } from 'viem'
import { abis, getContractAddress } from '@/lib/contracts'
import { useEffect, useState } from 'react'
import { Token } from '@/lib/api/tokens'
import { useTokenList } from './useTokenList'
/**
* 健康因子状态
*/
export type HealthFactorStatus = 'safe' | 'warning' | 'danger' | 'critical'
/**
* 健康因子结果
*/
export interface HealthFactorResult {
healthFactor: bigint
formattedHealthFactor: string
status: HealthFactorStatus
utilization: number // 利用率百分比
borrowValue: bigint // 借款价值USD1e18精度
collateralValue: bigint // 抵押品价值USD1e18精度
isLoading: boolean
refetch: () => void
}
/**
* 获取用户健康因子
*
* 健康因子定义:
* - > 1.5: 安全(绿色)
* - 1.2 ~ 1.5: 警告(黄色)
* - 1.0 ~ 1.2: 危险(橙色)
* - < 1.0: 临界/可被清算(红色)
* - MaxUint256: 无债务
*/
export function useHealthFactor(): HealthFactorResult {
const { address, chainId } = useAccount()
const { bySymbol } = useTokenList()
const usdcToken = bySymbol['USDC']
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
// 1. 获取用户基本信息principal
const { data: userBasic, refetch: refetchUserBasic } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'userBasic',
args: address ? [address] : undefined,
query: {
enabled: !!address && !!lendingProxyAddress,
},
})
const principal = userBasic !== undefined ? (userBasic as bigint) : 0n
// 如果没有借款principal >= 0提前返回
const hasDebt = principal < 0n
const shouldCalculate = hasDebt && !!address && !!lendingProxyAddress
// 2. 获取借款索引
const { data: borrowIndex, refetch: refetchBorrowIndex } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'borrowIndex',
query: {
enabled: shouldCalculate,
},
})
// 3. 获取价格预言机地址
const { data: priceFeedAddress } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'lendingPriceSource',
query: {
enabled: shouldCalculate,
},
})
// 4. 获取 baseToken 地址
const { data: baseToken } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'baseToken',
query: {
enabled: shouldCalculate,
},
})
// 5. 直接获取资产列表YT-A, YT-B, YT-C
const assetIndices = [0, 1, 2] as const
const assetListContracts = assetIndices.map((i) => ({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'assetList' as const,
args: [BigInt(i)],
}))
const { data: assetListData } = useReadContracts({
contracts: assetListContracts as any,
query: {
enabled: shouldCalculate && assetListContracts.length > 0,
},
})
const assetList = assetListData?.map((item) => item.result as `0x${string}`).filter(Boolean) || []
// 7. 使用 useState 来管理计算结果
const [result, setResult] = useState<HealthFactorResult>({
healthFactor: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'), // MaxUint256
formattedHealthFactor: '∞',
status: 'safe',
utilization: 0,
borrowValue: 0n,
collateralValue: 0n,
isLoading: true,
refetch: () => {
refetchUserBasic()
refetchBorrowIndex()
refetchCollateral()
refetchPrices()
},
})
// 8. 获取所有抵押品余额和配置
const collateralContracts = assetList.flatMap((asset) => [
// 获取用户抵押品余额
{
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'userCollateral' as const,
args: [address, asset],
},
// 获取资产配置
{
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'assetConfigs' as const,
args: [asset],
},
])
const { data: collateralData, refetch: refetchCollateral } = useReadContracts({
contracts: collateralContracts as any,
query: {
enabled: shouldCalculate && assetList.length > 0,
},
})
// 9. 获取所有资产价格(包括 baseToken
const priceContracts = [baseToken, ...assetList].filter(Boolean).map((token) => ({
address: priceFeedAddress,
abi: [
{
type: 'function',
name: 'getPrice',
inputs: [{ name: 'asset', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
],
functionName: 'getPrice' as const,
args: [token],
}))
const { data: priceData, refetch: refetchPrices } = useReadContracts({
contracts: priceContracts as any,
query: {
enabled: shouldCalculate && !!priceFeedAddress && priceContracts.length > 0,
},
})
// 10. 计算健康因子
useEffect(() => {
if (!shouldCalculate) {
// 没有债务,返回最大健康因子
setResult({
healthFactor: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
formattedHealthFactor: '∞',
status: 'safe',
utilization: 0,
borrowValue: 0n,
collateralValue: 0n,
isLoading: false,
refetch: () => {
refetchUserBasic()
},
})
return
}
if (!borrowIndex || !priceData || !collateralData || !baseToken || !chainId) {
return
}
try {
// 计算实际债务principal * borrowIndex / 1e18
const balance = (principal * (borrowIndex as bigint)) / BigInt(1e18)
const debt = -balance // 转为正数
// 获取 baseToken 价格(第一个价格)
const basePrice = priceData[0]?.result as bigint
if (!basePrice) return
// 计算债务价值USD
const baseDecimals = usdcToken?.onChainDecimals ?? usdcToken?.decimals ?? 18
const debtValue = (debt * basePrice) / BigInt(10 ** baseDecimals)
if (debtValue === 0n) {
setResult({
healthFactor: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
formattedHealthFactor: '∞',
status: 'safe',
utilization: 0,
borrowValue: 0n,
collateralValue: 0n,
isLoading: false,
refetch: () => {
refetchUserBasic()
},
})
return
}
// 计算抵押品总价值(使用 liquidateCollateralFactor
let totalCollateralValue = 0n
// assetConfigs 返回元组: [asset, decimals, borrowCollateralFactor, liquidateCollateralFactor, liquidationFactor, supplyCap]
type AssetConfigTuple = readonly [string, number, bigint, bigint, bigint, bigint]
for (let i = 0; i < assetList.length; i++) {
const collateralAmount = collateralData[i * 2]?.result as bigint
const assetConfig = collateralData[i * 2 + 1]?.result as AssetConfigTuple | undefined
const assetPrice = priceData[i + 1]?.result as bigint
if (collateralAmount > 0n && assetConfig && assetPrice) {
// assetConfig[1] = decimals, assetConfig[3] = liquidateCollateralFactor
const assetScale = 10n ** BigInt(Number(assetConfig[1]))
const collateralValue = (collateralAmount * assetPrice) / assetScale
// 应用清算折扣系数liquidateCollateralFactor
const discountedValue = (collateralValue * assetConfig[3]) / BigInt(1e18)
totalCollateralValue += discountedValue
}
}
// 计算健康因子 = 抵押品价值 / 债务价值1e18 精度)
const healthFactor = (totalCollateralValue * BigInt(1e18)) / debtValue
// 计算利用率(债务/抵押品)
const utilization = totalCollateralValue > 0n
? Number((debtValue * BigInt(10000)) / totalCollateralValue) / 100
: 0
// 确定健康因子状态
let status: HealthFactorStatus
const hfNumber = Number(formatUnits(healthFactor, 18))
if (hfNumber >= 1.5) {
status = 'safe'
} else if (hfNumber >= 1.2) {
status = 'warning'
} else if (hfNumber >= 1.0) {
status = 'danger'
} else {
status = 'critical'
}
// 格式化健康因子显示
const formattedHealthFactor = hfNumber >= 10
? hfNumber.toFixed(1)
: hfNumber >= 1
? hfNumber.toFixed(2)
: hfNumber.toFixed(3)
setResult({
healthFactor,
formattedHealthFactor,
status,
utilization,
borrowValue: debtValue,
collateralValue: totalCollateralValue,
isLoading: false,
refetch: () => {
refetchUserBasic()
refetchBorrowIndex()
refetchCollateral()
refetchPrices()
},
})
} catch (error) {
console.error('Health factor calculation error:', error)
setResult((prev) => ({ ...prev, isLoading: false }))
}
}, [
shouldCalculate,
principal,
borrowIndex,
priceData,
collateralData,
baseToken,
chainId,
assetList.length,
refetchUserBasic,
])
return result
}
/**
* 获取用户借款余额
*/
export function useBorrowBalance() {
const { address, chainId } = useAccount()
const { bySymbol } = useTokenList()
const usdcToken = bySymbol['USDC']
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
const { data: borrowBalance, refetch } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'borrowBalanceOf',
args: address ? [address] : undefined,
query: {
enabled: !!address && !!lendingProxyAddress,
},
})
const usdcDecimals = usdcToken?.onChainDecimals ?? usdcToken?.decimals ?? 18
const formattedBalance = borrowBalance ? formatUnits(borrowBalance as bigint, usdcDecimals) : '0.00'
return {
balance: borrowBalance as bigint | undefined,
formattedBalance,
refetch,
}
}
/**
* 获取用户在特定资产的抵押品余额
*/
export function useCollateralBalance(token: Token | undefined) {
const { address, chainId } = useAccount()
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
const { data: collateralBalance, refetch } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'userCollateral',
args: address && token?.contractAddress ? [address, token.contractAddress as `0x${string}`] : undefined,
query: {
enabled: !!address && !!lendingProxyAddress && !!token?.contractAddress,
},
})
const decimals = token?.onChainDecimals ?? token?.decimals ?? 18
const formattedBalance = collateralBalance ? formatUnits(collateralBalance as bigint, decimals) : '0.00'
return {
balance: collateralBalance as bigint | undefined,
formattedBalance,
refetch,
}
}
/**
* 获取协议利用率
*/
export function useProtocolUtilization() {
const { chainId } = useAccount()
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
const { data: utilization } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'getUtilization',
query: {
enabled: !!lendingProxyAddress,
},
})
// utilization 是 1e18 精度的百分比
const utilizationPercent = utilization ? Number(formatUnits(utilization as bigint, 18)) * 100 : 0
return {
utilization: utilization as bigint | undefined,
utilizationPercent,
}
}
const SECONDS_PER_YEAR = 365 * 24 * 60 * 60
function calcAPY(annualAprBigint: bigint | undefined): { apr: number; apy: number } {
if (!annualAprBigint) return { apr: 0, apy: 0 }
// getSupplyRate / getBorrowRate return annual APR with 1e18 precision
const annualApr = Number(formatUnits(annualAprBigint, 18)) // e.g. 0.05 = 5%
const perSecondRate = annualApr / SECONDS_PER_YEAR
const apy = perSecondRate > 0
? (Math.pow(1 + perSecondRate, SECONDS_PER_YEAR) - 1) * 100
: 0
return { apr: annualApr * 100, apy }
}
/**
* 获取当前供应 APY
* getSupplyRate() 返回年化 APR1e18 精度uint64
*/
export function useSupplyAPY() {
const { chainId } = useAccount()
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
const { data: supplyRate } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'getSupplyRate',
query: { enabled: !!lendingProxyAddress },
})
const { apr, apy } = calcAPY(supplyRate as bigint | undefined)
return {
supplyRate: supplyRate as bigint | undefined,
apr,
apy,
}
}
/**
* 获取当前借款 APR/APY
* getBorrowRate() 返回年化 APR1e18 精度uint64
*/
export function useBorrowAPR() {
const { chainId } = useAccount()
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
const { data: borrowRate } = useReadContract({
address: lendingProxyAddress,
abi: abis.lendingProxy,
functionName: 'getBorrowRate',
query: { enabled: !!lendingProxyAddress },
})
const { apr, apy } = calcAPY(borrowRate as bigint | undefined)
return {
borrowRate: borrowRate as bigint | undefined,
apr,
apy,
}
}