330 lines
9.7 KiB
TypeScript
330 lines
9.7 KiB
TypeScript
|
|
import { useState, useCallback, useEffect } from 'react'
|
||
|
|
import { useAccount, useWriteContract, useWaitForTransactionReceipt, useReadContract } from 'wagmi'
|
||
|
|
import { parseUnits, formatUnits } from 'viem'
|
||
|
|
import { abis, getContractAddress } from '@/lib/contracts'
|
||
|
|
import { Token } from '@/lib/api/tokens'
|
||
|
|
import { handleContractCatchError, isValidAmount, parseContractError } from '@/lib/errors'
|
||
|
|
|
||
|
|
type WithdrawCollateralStatus = 'idle' | 'withdrawing' | 'success' | 'error'
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Hook for withdrawing collateral (YT tokens) from lending protocol
|
||
|
|
*/
|
||
|
|
export function useWithdrawCollateral() {
|
||
|
|
const { address, chainId } = useAccount()
|
||
|
|
const [status, setStatus] = useState<WithdrawCollateralStatus>('idle')
|
||
|
|
const [error, setError] = useState<string | null>(null)
|
||
|
|
|
||
|
|
const {
|
||
|
|
writeContractAsync: withdrawWrite,
|
||
|
|
data: withdrawHash,
|
||
|
|
isPending: isWithdrawPending,
|
||
|
|
reset: resetWithdraw,
|
||
|
|
} = useWriteContract()
|
||
|
|
|
||
|
|
const {
|
||
|
|
isLoading: isWithdrawConfirming,
|
||
|
|
isSuccess: isWithdrawSuccess,
|
||
|
|
isError: isWithdrawError,
|
||
|
|
error: withdrawReceiptError,
|
||
|
|
} = useWaitForTransactionReceipt({ hash: withdrawHash })
|
||
|
|
|
||
|
|
const executeWithdrawCollateral = useCallback(async (token: Token, amount: string) => {
|
||
|
|
if (status !== 'idle') return false
|
||
|
|
if (!address || !chainId || !token?.contractAddress || !amount) {
|
||
|
|
setError('Missing required parameters')
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if (!isValidAmount(amount)) {
|
||
|
|
setError('Invalid amount')
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
setError(null)
|
||
|
|
setStatus('withdrawing')
|
||
|
|
const lendingProxyAddress = getContractAddress('lendingProxy', chainId)
|
||
|
|
if (!lendingProxyAddress) {
|
||
|
|
throw new Error('Contract not deployed on this chain')
|
||
|
|
}
|
||
|
|
const decimals = token.onChainDecimals ?? token.decimals
|
||
|
|
const amountInWei = parseUnits(amount, decimals)
|
||
|
|
await withdrawWrite({
|
||
|
|
address: lendingProxyAddress,
|
||
|
|
abi: abis.lendingProxy,
|
||
|
|
functionName: 'withdrawCollateral',
|
||
|
|
args: [token.contractAddress as `0x${string}`, amountInWei],
|
||
|
|
})
|
||
|
|
return true
|
||
|
|
} catch (err: unknown) {
|
||
|
|
handleContractCatchError(err, 'Withdraw collateral failed', setError, setStatus as (s: string) => void)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [address, chainId, withdrawWrite, status])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isWithdrawSuccess && status === 'withdrawing') {
|
||
|
|
setStatus('success')
|
||
|
|
}
|
||
|
|
}, [isWithdrawSuccess, status])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isWithdrawError && status === 'withdrawing') {
|
||
|
|
setError(parseContractError(withdrawReceiptError))
|
||
|
|
setStatus('error')
|
||
|
|
}
|
||
|
|
}, [isWithdrawError, status, withdrawReceiptError])
|
||
|
|
|
||
|
|
const reset = useCallback(() => {
|
||
|
|
setStatus('idle')
|
||
|
|
setError(null)
|
||
|
|
resetWithdraw()
|
||
|
|
}, [resetWithdraw])
|
||
|
|
|
||
|
|
return {
|
||
|
|
status,
|
||
|
|
error,
|
||
|
|
isLoading: isWithdrawPending || isWithdrawConfirming,
|
||
|
|
withdrawHash,
|
||
|
|
executeWithdrawCollateral,
|
||
|
|
reset,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type CollateralStatus = 'idle' | 'approving' | 'approved' | 'supplying' | 'success' | 'error'
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Hook for supplying collateral (YT tokens) to lending protocol
|
||
|
|
*/
|
||
|
|
export function useLendingCollateral() {
|
||
|
|
const { address, chainId } = useAccount()
|
||
|
|
const [status, setStatus] = useState<CollateralStatus>('idle')
|
||
|
|
const [error, setError] = useState<string | null>(null)
|
||
|
|
const [pendingToken, setPendingToken] = useState<Token | null>(null)
|
||
|
|
const [pendingAmount, setPendingAmount] = useState<string>('')
|
||
|
|
|
||
|
|
const {
|
||
|
|
writeContractAsync: approveWrite,
|
||
|
|
data: approveHash,
|
||
|
|
isPending: isApprovePending,
|
||
|
|
reset: resetApprove,
|
||
|
|
} = useWriteContract()
|
||
|
|
|
||
|
|
const {
|
||
|
|
isLoading: isApproveConfirming,
|
||
|
|
isSuccess: isApproveSuccess,
|
||
|
|
isError: isApproveError,
|
||
|
|
error: approveReceiptError,
|
||
|
|
} = useWaitForTransactionReceipt({ hash: approveHash })
|
||
|
|
|
||
|
|
const {
|
||
|
|
writeContractAsync: supplyWrite,
|
||
|
|
data: supplyHash,
|
||
|
|
isPending: isSupplyPending,
|
||
|
|
reset: resetSupply,
|
||
|
|
} = useWriteContract()
|
||
|
|
|
||
|
|
const {
|
||
|
|
isLoading: isSupplyConfirming,
|
||
|
|
isSuccess: isSupplySuccess,
|
||
|
|
isError: isSupplyError,
|
||
|
|
error: supplyReceiptError,
|
||
|
|
} = useWaitForTransactionReceipt({ hash: supplyHash })
|
||
|
|
|
||
|
|
const executeApprove = useCallback(async (token: Token, amount: string) => {
|
||
|
|
if (status !== 'idle') return false
|
||
|
|
if (!address || !chainId || !token?.contractAddress || !amount) {
|
||
|
|
setError('Missing required parameters')
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if (!isValidAmount(amount)) {
|
||
|
|
setError('Invalid amount')
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
setError(null)
|
||
|
|
setStatus('approving')
|
||
|
|
|
||
|
|
const lendingProxyAddress = getContractAddress('lendingProxy', chainId)
|
||
|
|
if (!lendingProxyAddress) {
|
||
|
|
throw new Error('Contract not deployed on this chain')
|
||
|
|
}
|
||
|
|
|
||
|
|
const decimals = token.onChainDecimals ?? token.decimals
|
||
|
|
const amountInWei = parseUnits(amount, decimals)
|
||
|
|
|
||
|
|
await approveWrite({
|
||
|
|
address: token.contractAddress as `0x${string}`,
|
||
|
|
abi: abis.YTToken,
|
||
|
|
functionName: 'approve',
|
||
|
|
args: [lendingProxyAddress, amountInWei],
|
||
|
|
})
|
||
|
|
|
||
|
|
return true
|
||
|
|
} catch (err: unknown) {
|
||
|
|
handleContractCatchError(err, 'Approve failed', setError, setStatus as (s: string) => void)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [address, chainId, approveWrite, status])
|
||
|
|
|
||
|
|
const executeSupplyCollateral = useCallback(async (token: Token, amount: string) => {
|
||
|
|
if (status === 'supplying' || status === 'success' || status === 'error') return false
|
||
|
|
if (!address || !chainId || !token?.contractAddress || !amount) {
|
||
|
|
setError('Missing required parameters')
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if (!isValidAmount(amount)) {
|
||
|
|
setError('Invalid amount')
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
setError(null)
|
||
|
|
setStatus('supplying')
|
||
|
|
|
||
|
|
const lendingProxyAddress = getContractAddress('lendingProxy', chainId)
|
||
|
|
if (!lendingProxyAddress) {
|
||
|
|
throw new Error('Contract not deployed on this chain')
|
||
|
|
}
|
||
|
|
|
||
|
|
const decimals = token.onChainDecimals ?? token.decimals
|
||
|
|
const amountInWei = parseUnits(amount, decimals)
|
||
|
|
|
||
|
|
await supplyWrite({
|
||
|
|
address: lendingProxyAddress,
|
||
|
|
abi: abis.lendingProxy,
|
||
|
|
functionName: 'supplyCollateral',
|
||
|
|
args: [token.contractAddress as `0x${string}`, amountInWei],
|
||
|
|
})
|
||
|
|
|
||
|
|
return true
|
||
|
|
} catch (err: unknown) {
|
||
|
|
handleContractCatchError(err, 'Supply collateral failed', setError, setStatus as (s: string) => void)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [address, chainId, supplyWrite, status])
|
||
|
|
|
||
|
|
const executeApproveAndSupply = useCallback(async (token: Token, amount: string) => {
|
||
|
|
if (status !== 'idle') return
|
||
|
|
setPendingToken(token)
|
||
|
|
setPendingAmount(amount)
|
||
|
|
const approveSuccess = await executeApprove(token, amount)
|
||
|
|
if (!approveSuccess) return
|
||
|
|
}, [executeApprove, status])
|
||
|
|
|
||
|
|
// Auto-execute supply when approve is successful
|
||
|
|
useEffect(() => {
|
||
|
|
if (isApproveSuccess && status === 'approving' && pendingToken && pendingAmount) {
|
||
|
|
setStatus('approved')
|
||
|
|
executeSupplyCollateral(pendingToken, pendingAmount)
|
||
|
|
}
|
||
|
|
}, [isApproveSuccess, status, pendingToken, pendingAmount, executeSupplyCollateral])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isApproveError && status === 'approving') {
|
||
|
|
setError(parseContractError(approveReceiptError))
|
||
|
|
setStatus('error')
|
||
|
|
}
|
||
|
|
}, [isApproveError, status, approveReceiptError])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isSupplySuccess && status === 'supplying') {
|
||
|
|
setStatus('success')
|
||
|
|
}
|
||
|
|
}, [isSupplySuccess, status])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isSupplyError && status === 'supplying') {
|
||
|
|
setError(parseContractError(supplyReceiptError))
|
||
|
|
setStatus('error')
|
||
|
|
}
|
||
|
|
}, [isSupplyError, status, supplyReceiptError])
|
||
|
|
|
||
|
|
const reset = useCallback(() => {
|
||
|
|
setStatus('idle')
|
||
|
|
setError(null)
|
||
|
|
setPendingToken(null)
|
||
|
|
setPendingAmount('')
|
||
|
|
resetApprove()
|
||
|
|
resetSupply()
|
||
|
|
}, [resetApprove, resetSupply])
|
||
|
|
|
||
|
|
return {
|
||
|
|
status,
|
||
|
|
error,
|
||
|
|
isLoading: isApprovePending || isApproveConfirming || isSupplyPending || isSupplyConfirming,
|
||
|
|
approveHash,
|
||
|
|
supplyHash,
|
||
|
|
executeApprove,
|
||
|
|
executeSupplyCollateral,
|
||
|
|
executeApproveAndSupply,
|
||
|
|
reset,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Query user's wallet balance of a specific YT token (ERC20 balanceOf)
|
||
|
|
*/
|
||
|
|
export function useYTWalletBalance(token: Token | undefined) {
|
||
|
|
const { address } = useAccount()
|
||
|
|
|
||
|
|
const { data: balance, isLoading, refetch } = useReadContract({
|
||
|
|
address: token?.contractAddress as `0x${string}` | undefined,
|
||
|
|
abi: abis.YTToken,
|
||
|
|
functionName: 'balanceOf',
|
||
|
|
args: address ? [address] : undefined,
|
||
|
|
query: {
|
||
|
|
enabled: !!address && !!token?.contractAddress,
|
||
|
|
retry: false,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
const decimals = token?.onChainDecimals ?? token?.decimals ?? 18
|
||
|
|
const formattedBalance = balance
|
||
|
|
? formatUnits(balance as bigint, decimals)
|
||
|
|
: '0.00'
|
||
|
|
|
||
|
|
return {
|
||
|
|
balance: balance as bigint || 0n,
|
||
|
|
formattedBalance,
|
||
|
|
isLoading,
|
||
|
|
refetch,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Query 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: balance,
|
||
|
|
isLoading,
|
||
|
|
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,
|
||
|
|
retry: false,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
const decimals = token?.onChainDecimals ?? token?.decimals ?? 18
|
||
|
|
const formattedBalance = balance
|
||
|
|
? formatUnits(balance as bigint, decimals)
|
||
|
|
: '0.00'
|
||
|
|
|
||
|
|
return {
|
||
|
|
balance: balance as bigint || 0n,
|
||
|
|
formattedBalance,
|
||
|
|
isLoading,
|
||
|
|
refetch,
|
||
|
|
}
|
||
|
|
}
|