451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
|
|
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,
|
|||
|
|
}
|
|||
|
|
}
|