Files
assetx/webapp/hooks/useHealthFactor.ts

451 lines
14 KiB
TypeScript
Raw Normal View History

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,
}
}