adding unstake basket page
This commit is contained in:
@@ -38,6 +38,7 @@ export function Header() {
|
||||
{ href: '/', label: 'Swap' },
|
||||
{ href: '/stake', label: 'Stake' },
|
||||
{ href: '/unstake', label: 'Unstake' },
|
||||
{ href: '/unstake-basket', label: 'Unstake Basket' },
|
||||
{ href: '/about', label: 'About' },
|
||||
];
|
||||
|
||||
|
||||
379
src/components/unstake-basket-form.tsx
Normal file
379
src/components/unstake-basket-form.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle, XCircle, Loader2, Trash2 } from 'lucide-react';
|
||||
import { useAccount, usePublicClient } from 'wagmi';
|
||||
import { useGetAllPools, useLPTokenBalance, type PoolDetails } from '@/hooks/usePartyPlanner';
|
||||
import { useBurn, type ActualBurnAmounts } from '@/hooks/usePartyPool';
|
||||
import { formatUnits, parseUnits } from 'viem';
|
||||
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
|
||||
import { ERC20ABI } from '@/contracts/ERC20ABI';
|
||||
|
||||
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
|
||||
|
||||
interface PoolWithBalance extends PoolDetails {
|
||||
lpBalance: bigint;
|
||||
}
|
||||
|
||||
interface TokenInfo {
|
||||
address: `0x${string}`;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export function UnstakeBasketForm() {
|
||||
const { t } = useTranslation();
|
||||
const { isConnected, address } = useAccount();
|
||||
const publicClient = usePublicClient();
|
||||
const [selectedPools, setSelectedPools] = useState<Set<string>>(new Set());
|
||||
const [poolsWithBalances, setPoolsWithBalances] = useState<PoolWithBalance[]>([]);
|
||||
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
|
||||
const [transactionError, setTransactionError] = useState<string | null>(null);
|
||||
const [actualBurnResults, setActualBurnResults] = useState<{[key: string]: ActualBurnAmounts}>({});
|
||||
const [poolTokens, setPoolTokens] = useState<{[key: string]: TokenInfo[]}>({});
|
||||
|
||||
// Fetch all pools using the hook
|
||||
const { poolDetails, loading: poolsLoading } = useGetAllPools();
|
||||
|
||||
// Initialize burn hook
|
||||
const { executeBurn, isBurning } = useBurn();
|
||||
|
||||
// Fetch LP balances for all pools
|
||||
useEffect(() => {
|
||||
if (!poolDetails || !address || !publicClient) return;
|
||||
|
||||
const fetchBalances = async () => {
|
||||
const poolsWithBal: PoolWithBalance[] = [];
|
||||
|
||||
for (const pool of poolDetails) {
|
||||
try {
|
||||
const balance = await publicClient.readContract({
|
||||
address: pool.address,
|
||||
abi: IPartyPoolABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [address],
|
||||
}) as bigint;
|
||||
|
||||
if (balance > 0n) {
|
||||
poolsWithBal.push({
|
||||
...pool,
|
||||
lpBalance: balance,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error fetching balance for pool ${pool.address}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
setPoolsWithBalances(poolsWithBal);
|
||||
};
|
||||
|
||||
fetchBalances();
|
||||
}, [poolDetails, address, publicClient]);
|
||||
|
||||
// Fetch token details for selected pools
|
||||
useEffect(() => {
|
||||
if (!publicClient || selectedPools.size === 0) return;
|
||||
|
||||
const fetchTokenDetails = async () => {
|
||||
const tokenInfoMap: {[key: string]: TokenInfo[]} = {};
|
||||
|
||||
for (const poolAddress of Array.from(selectedPools)) {
|
||||
const pool = poolsWithBalances.find(p => p.address === poolAddress);
|
||||
if (!pool) continue;
|
||||
|
||||
const tokenInfos: TokenInfo[] = [];
|
||||
|
||||
for (const tokenAddress of pool.tokens) {
|
||||
try {
|
||||
const [symbol, decimals] = await Promise.all([
|
||||
publicClient.readContract({
|
||||
address: tokenAddress as `0x${string}`,
|
||||
abi: ERC20ABI,
|
||||
functionName: 'symbol',
|
||||
}) as Promise<string>,
|
||||
publicClient.readContract({
|
||||
address: tokenAddress as `0x${string}`,
|
||||
abi: ERC20ABI,
|
||||
functionName: 'decimals',
|
||||
}) as Promise<number>,
|
||||
]);
|
||||
|
||||
tokenInfos.push({
|
||||
address: tokenAddress as `0x${string}`,
|
||||
symbol,
|
||||
decimals,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error fetching token details for ${tokenAddress}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
tokenInfoMap[poolAddress] = tokenInfos;
|
||||
}
|
||||
|
||||
setPoolTokens(tokenInfoMap);
|
||||
};
|
||||
|
||||
fetchTokenDetails();
|
||||
}, [publicClient, selectedPools, poolsWithBalances]);
|
||||
|
||||
const togglePoolSelection = (poolAddress: string) => {
|
||||
const newSelected = new Set(selectedPools);
|
||||
if (newSelected.has(poolAddress)) {
|
||||
newSelected.delete(poolAddress);
|
||||
} else {
|
||||
newSelected.add(poolAddress);
|
||||
}
|
||||
setSelectedPools(newSelected);
|
||||
};
|
||||
|
||||
const handleBurnAll = async () => {
|
||||
if (selectedPools.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTransactionStatus('pending');
|
||||
setTransactionError(null);
|
||||
setActualBurnResults({});
|
||||
|
||||
try {
|
||||
const results: {[key: string]: ActualBurnAmounts} = {};
|
||||
|
||||
// Execute burn for each selected pool
|
||||
for (const poolAddress of Array.from(selectedPools)) {
|
||||
const pool = poolsWithBalances.find(p => p.address === poolAddress);
|
||||
if (!pool) continue;
|
||||
|
||||
console.log(`Burning LP tokens for pool ${pool.symbol}...`);
|
||||
|
||||
// Execute the burn transaction
|
||||
const result = await executeBurn(
|
||||
pool.address,
|
||||
pool.lpBalance,
|
||||
false // unwrap = false by default
|
||||
);
|
||||
|
||||
// Store actual burn amounts if available
|
||||
if (result?.actualBurnAmounts) {
|
||||
results[poolAddress] = result.actualBurnAmounts;
|
||||
}
|
||||
}
|
||||
|
||||
setActualBurnResults(results);
|
||||
setTransactionStatus('success');
|
||||
} catch (err) {
|
||||
console.error('Burn failed:', err);
|
||||
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
|
||||
setTransactionStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (transactionStatus === 'success') {
|
||||
// Clear selections and refresh
|
||||
setSelectedPools(new Set());
|
||||
// Refresh pool balances
|
||||
window.location.reload();
|
||||
}
|
||||
setTransactionStatus('idle');
|
||||
setTransactionError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto relative">
|
||||
<CardHeader>
|
||||
<CardTitle>Unstake Basket</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Select pools to burn all LP tokens and receive all underlying tokens proportionally
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Pool Selection List */}
|
||||
{poolsLoading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : poolsWithBalances.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>No pools with LP token balance found.</p>
|
||||
<p className="text-sm">Stake some tokens first to use this feature.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{poolsWithBalances.map((pool) => (
|
||||
<div
|
||||
key={pool.address}
|
||||
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
|
||||
selectedPools.has(pool.address)
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
onClick={() => togglePoolSelection(pool.address)}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPools.has(pool.address)}
|
||||
onChange={() => {}}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium">{pool.symbol}</div>
|
||||
<div className="text-xs text-muted-foreground">{pool.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-medium">
|
||||
{formatUnits(pool.lpBalance, 18)} LP
|
||||
</div>
|
||||
{poolTokens[pool.address] && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{poolTokens[pool.address].length} tokens
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedPools.has(pool.address) && poolTokens[pool.address] && (
|
||||
<div className="mt-3 pt-3 border-t border-border">
|
||||
<div className="text-xs text-muted-foreground mb-2">You will receive:</div>
|
||||
<div className="space-y-1">
|
||||
{poolTokens[pool.address].map((token) => (
|
||||
<div key={token.address} className="text-xs flex justify-between">
|
||||
<span>{token.symbol}</span>
|
||||
<span className="text-muted-foreground">
|
||||
(proportional to pool composition)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Burn Button */}
|
||||
<Button
|
||||
className="w-full h-14 text-lg"
|
||||
onClick={handleBurnAll}
|
||||
disabled={!isConnected || selectedPools.size === 0 || isBurning}
|
||||
>
|
||||
{!isConnected
|
||||
? 'Connect Wallet'
|
||||
: isBurning
|
||||
? 'Burning...'
|
||||
: `Burn ${selectedPools.size} Pool${selectedPools.size !== 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
|
||||
{selectedPools.size > 0 && (
|
||||
<div className="text-xs text-center text-muted-foreground">
|
||||
This will burn all LP tokens from {selectedPools.size} selected pool{selectedPools.size !== 1 ? 's' : ''}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{/* Transaction Modal Overlay */}
|
||||
{transactionStatus !== 'idle' && (
|
||||
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
|
||||
<div className="bg-card border rounded-lg p-8 max-w-md w-full mx-4 shadow-lg max-h-[80vh] overflow-y-auto">
|
||||
{transactionStatus === 'pending' && (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<Loader2 className="h-16 w-16 animate-spin text-primary" />
|
||||
<h3 className="text-xl font-semibold text-center">
|
||||
Burning LP Tokens
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Burning LP tokens from {selectedPools.size} pool{selectedPools.size !== 1 ? 's' : ''}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
Please confirm the transactions in your wallet
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transactionStatus === 'success' && (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||
<h3 className="text-xl font-semibold text-center">
|
||||
Burn Confirmed!
|
||||
</h3>
|
||||
|
||||
<div className="w-full space-y-4">
|
||||
{Object.entries(actualBurnResults).map(([poolAddress, burnAmounts]) => {
|
||||
const pool = poolsWithBalances.find(p => p.address === poolAddress);
|
||||
const tokens = poolTokens[poolAddress];
|
||||
|
||||
if (!pool || !tokens) return null;
|
||||
|
||||
return (
|
||||
<div key={poolAddress} className="border rounded-lg p-4">
|
||||
<div className="font-medium mb-2">{pool.symbol}</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">LP Burned:</span>
|
||||
<span className="font-medium">
|
||||
{formatUnits(burnAmounts.lpBurned, 18)} {pool.symbol}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Tokens Received:</div>
|
||||
{burnAmounts.withdrawAmounts.map((amount, index) => {
|
||||
const token = tokens[index];
|
||||
if (!token) return null;
|
||||
|
||||
return (
|
||||
<div key={index} className="flex justify-between text-sm pl-2">
|
||||
<span className="text-muted-foreground">{token.symbol}:</span>
|
||||
<span className="font-medium">
|
||||
{formatUnits(amount, token.decimals)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleCloseModal}
|
||||
className="w-full mt-4"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transactionStatus === 'error' && (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<XCircle className="h-16 w-16 text-destructive" />
|
||||
<h3 className="text-xl font-semibold text-center">
|
||||
Burn Failed
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground text-center break-words">
|
||||
{transactionError || 'Transaction failed'}
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleCloseModal}
|
||||
variant="outline"
|
||||
className="w-full mt-4"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user