包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
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,
|
||
}
|
||
}
|