init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
This commit is contained in:
267
webapp/hooks/useCollateral.ts
Normal file
267
webapp/hooks/useCollateral.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { useAccount, useReadContract, useReadContracts } from 'wagmi'
|
||||
import { formatUnits } from 'viem'
|
||||
import { abis, getContractAddress } from '@/lib/contracts'
|
||||
import { Token } from '@/lib/api/tokens'
|
||||
import { useTokenList } from './useTokenList'
|
||||
|
||||
/**
|
||||
* Hook to query user's collateral balance for a specific YT token
|
||||
*/
|
||||
export function useCollateralBalance(token: Token | undefined) {
|
||||
const { address, chainId } = useAccount()
|
||||
|
||||
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
|
||||
|
||||
const { data: collateralBalance, refetch, isLoading } = useReadContract({
|
||||
address: lendingProxyAddress,
|
||||
abi: abis.lendingProxy,
|
||||
functionName: 'getCollateral',
|
||||
args: address && token?.contractAddress ? [address, token.contractAddress as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: !!address && !!token?.contractAddress && !!lendingProxyAddress,
|
||||
},
|
||||
})
|
||||
|
||||
const decimals = token?.onChainDecimals ?? token?.decimals ?? 18
|
||||
const formattedBalance = collateralBalance
|
||||
? formatUnits(collateralBalance as bigint, decimals)
|
||||
: '0'
|
||||
|
||||
return {
|
||||
balance: collateralBalance as bigint | undefined,
|
||||
formattedBalance,
|
||||
isLoading,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to query user's borrow balance (in USDC)
|
||||
*/
|
||||
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, isLoading } = useReadContract({
|
||||
address: lendingProxyAddress,
|
||||
abi: abis.lendingProxy,
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: address ? [address] : undefined,
|
||||
query: {
|
||||
enabled: !!address && !!chainId && !!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,
|
||||
isLoading,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to query user's balance (positive = supply, negative = borrow)
|
||||
*/
|
||||
export function useAccountBalance() {
|
||||
const { address, chainId } = useAccount()
|
||||
|
||||
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
|
||||
|
||||
const { data: supplyBalance, refetch: refetchSupply, isLoading: isLoadingSupply } = useReadContract({
|
||||
address: lendingProxyAddress,
|
||||
abi: abis.lendingProxy,
|
||||
functionName: 'supplyBalanceOf',
|
||||
args: address ? [address] : undefined,
|
||||
query: {
|
||||
enabled: !!address && !!chainId && !!lendingProxyAddress,
|
||||
},
|
||||
})
|
||||
|
||||
const { data: borrowBalance, refetch: refetchBorrow, isLoading: isLoadingBorrow } = useReadContract({
|
||||
address: lendingProxyAddress,
|
||||
abi: abis.lendingProxy,
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: address ? [address] : undefined,
|
||||
query: {
|
||||
enabled: !!address && !!chainId && !!lendingProxyAddress,
|
||||
},
|
||||
})
|
||||
|
||||
const supplyValue = (supplyBalance as bigint) || 0n
|
||||
const borrowValue = (borrowBalance as bigint) || 0n
|
||||
const netBalance = supplyValue - borrowValue
|
||||
|
||||
return {
|
||||
balance: netBalance,
|
||||
isSupply: supplyValue > 0n,
|
||||
isBorrow: borrowValue > 0n,
|
||||
isLoading: isLoadingSupply || isLoadingBorrow,
|
||||
refetch: () => {
|
||||
refetchSupply()
|
||||
refetchBorrow()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to calculate LTV (Loan-to-Value) ratio
|
||||
* LTV = (Borrow Balance / Collateral Value) * 100
|
||||
*/
|
||||
export function useLTV() {
|
||||
const { bySymbol } = useTokenList()
|
||||
const ytA = bySymbol['YT-A']
|
||||
const ytB = bySymbol['YT-B']
|
||||
const ytC = bySymbol['YT-C']
|
||||
|
||||
const { formattedBalance: borrowBalance } = useBorrowBalance()
|
||||
const { value: valueA } = useCollateralValue(ytA)
|
||||
const { value: valueB } = useCollateralValue(ytB)
|
||||
const { value: valueC } = useCollateralValue(ytC)
|
||||
|
||||
const totalCollateralValue = parseFloat(valueA) + parseFloat(valueB) + parseFloat(valueC)
|
||||
const borrowValue = parseFloat(borrowBalance)
|
||||
|
||||
const ltv = totalCollateralValue > 0
|
||||
? (borrowValue / totalCollateralValue) * 100
|
||||
: 0
|
||||
|
||||
return {
|
||||
ltv: ltv.toFixed(2),
|
||||
ltvRaw: ltv,
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get YT token price from its contract
|
||||
*/
|
||||
export function useYTPrice(token: Token | undefined) {
|
||||
const { data: ytPrice, isLoading } = useReadContract({
|
||||
address: token?.contractAddress as `0x${string}` | undefined,
|
||||
abi: abis.YTToken,
|
||||
functionName: 'ytPrice',
|
||||
query: {
|
||||
enabled: !!token?.contractAddress,
|
||||
},
|
||||
})
|
||||
|
||||
// ytPrice 返回 30 位精度的价格(PRICE_PRECISION = 1e30)
|
||||
const formattedPrice = ytPrice
|
||||
? formatUnits(ytPrice as bigint, 30)
|
||||
: '0'
|
||||
|
||||
return {
|
||||
price: ytPrice as bigint | undefined,
|
||||
formattedPrice,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to calculate collateral value in USD
|
||||
*/
|
||||
export function useCollateralValue(token: Token | undefined) {
|
||||
const { formattedBalance } = useCollateralBalance(token)
|
||||
const { formattedPrice } = useYTPrice(token)
|
||||
|
||||
const value = parseFloat(formattedBalance) * parseFloat(formattedPrice)
|
||||
|
||||
return {
|
||||
value: value.toFixed(2),
|
||||
valueRaw: value,
|
||||
}
|
||||
}
|
||||
|
||||
type AssetConfig = {
|
||||
asset: `0x${string}`
|
||||
decimals: number
|
||||
borrowCollateralFactor: bigint
|
||||
liquidateCollateralFactor: bigint
|
||||
liquidationFactor: bigint
|
||||
supplyCap: bigint
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to calculate total borrow capacity and available borrowable amount
|
||||
*/
|
||||
export function useMaxBorrowable() {
|
||||
const { chainId } = useAccount()
|
||||
const { bySymbol } = useTokenList()
|
||||
const ytA = bySymbol['YT-A']
|
||||
const ytB = bySymbol['YT-B']
|
||||
const ytC = bySymbol['YT-C']
|
||||
|
||||
const lendingProxyAddress = chainId ? getContractAddress('lendingProxy', chainId) : undefined
|
||||
|
||||
// Collateral balances
|
||||
const { formattedBalance: balA, refetch: refetchA } = useCollateralBalance(ytA)
|
||||
const { formattedBalance: balB, refetch: refetchB } = useCollateralBalance(ytB)
|
||||
const { formattedBalance: balC, refetch: refetchC } = useCollateralBalance(ytC)
|
||||
|
||||
// YT token prices (ytPrice(), 30-decimal precision)
|
||||
const { formattedPrice: priceA } = useYTPrice(ytA)
|
||||
const { formattedPrice: priceB } = useYTPrice(ytB)
|
||||
const { formattedPrice: priceC } = useYTPrice(ytC)
|
||||
|
||||
// Borrow collateral factors from lendingProxy.assetConfigs
|
||||
const ytAddresses = [ytA, ytB, ytC]
|
||||
.filter(t => !!t?.contractAddress)
|
||||
.map(t => t!.contractAddress as `0x${string}`)
|
||||
|
||||
const configContracts = ytAddresses.map((addr) => ({
|
||||
address: lendingProxyAddress,
|
||||
abi: abis.lendingProxy,
|
||||
functionName: 'assetConfigs' as const,
|
||||
args: [addr],
|
||||
}))
|
||||
|
||||
const { data: configData } = useReadContracts({
|
||||
contracts: configContracts as any,
|
||||
query: { enabled: !!lendingProxyAddress && configContracts.length > 0 },
|
||||
})
|
||||
|
||||
type ConfigTuple = readonly [string, number, bigint, bigint, bigint, bigint]
|
||||
|
||||
const getConfig = (i: number): ConfigTuple | undefined =>
|
||||
configData?.[i]?.result as ConfigTuple | undefined
|
||||
|
||||
const calcCapacity = (bal: string, price: string, config: ConfigTuple | undefined) => {
|
||||
if (!config || !config[2]) return 0
|
||||
const factor = Number(config[2]) / 1e18
|
||||
return parseFloat(bal) * parseFloat(price) * factor
|
||||
}
|
||||
|
||||
const capacityA = calcCapacity(balA, priceA, getConfig(0))
|
||||
const capacityB = calcCapacity(balB, priceB, getConfig(1))
|
||||
const capacityC = calcCapacity(balC, priceC, getConfig(2))
|
||||
|
||||
const totalCapacity = capacityA + capacityB + capacityC
|
||||
|
||||
const { formattedBalance: borrowedBalance, refetch: refetchBorrow } = useBorrowBalance()
|
||||
const currentBorrow = parseFloat(borrowedBalance) || 0
|
||||
const available = Math.max(0, totalCapacity - currentBorrow)
|
||||
|
||||
const refetch = () => {
|
||||
refetchA()
|
||||
refetchB()
|
||||
refetchC()
|
||||
refetchBorrow()
|
||||
}
|
||||
|
||||
return {
|
||||
totalCapacity,
|
||||
available,
|
||||
formattedCapacity: totalCapacity.toFixed(2),
|
||||
formattedAvailable: available.toFixed(2),
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user