init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
450
webapp/hooks/useHealthFactor.ts
Normal file
450
webapp/hooks/useHealthFactor.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
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 // 借款价值(USD,1e18精度)
|
||||
collateralValue: bigint // 抵押品价值(USD,1e18精度)
|
||||
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() 返回年化 APR(1e18 精度,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() 返回年化 APR(1e18 精度,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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user