adding unstake basket page

This commit is contained in:
2025-11-06 12:39:16 -04:00
parent 0a518d31f9
commit f690799b50
4 changed files with 549 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
'use client';
import { UnstakeBasketForm } from '@/components/unstake-basket-form';
export default function UnstakeBasketPage() {
return (
<div className="w-full max-w-2xl mx-auto">
<UnstakeBasketForm />
</div>
);
}

View File

@@ -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' },
];

View 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>
);
}

View File

@@ -630,7 +630,6 @@ export function useBurnSwap() {
deadline: deadline.toString(),
unwrap,
});
// STEP 3: Execute the burnSwap transaction
const hash = await walletClient.writeContract({
address: poolAddress,
@@ -710,3 +709,161 @@ export function useBurnSwap() {
burnSwapError,
};
}
export interface ActualBurnAmounts {
lpBurned: bigint;
withdrawAmounts: bigint[];
}
export function useBurn() {
const publicClient = usePublicClient();
const { data: walletClient } = useWalletClient();
const [isBurning, setIsBurning] = useState(false);
const [burnHash, setBurnHash] = useState<`0x${string}` | null>(null);
const [burnError, setBurnError] = useState<string | null>(null);
const executeBurn = async (
poolAddress: `0x${string}`,
lpAmount: bigint,
unwrap: boolean = false
) => {
if (!walletClient || !publicClient) {
setBurnError('Wallet not connected');
return;
}
try {
setIsBurning(true);
setBurnError(null);
setBurnHash(null);
const userAddress = walletClient.account.address;
// STEP 1: Approve the pool to spend the LP tokens
console.log('🔐 Approving LP token spend for burn...');
console.log('LP token (pool) to approve:', poolAddress);
console.log('Spender (pool):', poolAddress);
console.log('Amount:', lpAmount.toString());
const approvalHash = await walletClient.writeContract({
address: poolAddress,
abi: [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }]
}
],
functionName: 'approve',
args: [poolAddress, lpAmount],
account: userAddress,
});
console.log('✅ Approval transaction submitted:', approvalHash);
await publicClient.waitForTransactionReceipt({ hash: approvalHash });
console.log('✅ Approval confirmed');
// STEP 2: Calculate deadline (20 minutes from now)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200); // 20 minutes = 1200 seconds
console.log('🚀 Executing burn with params:', {
pool: poolAddress,
payer: userAddress,
receiver: userAddress,
lpAmount: lpAmount.toString(),
deadline: deadline.toString(),
unwrap,
});
// Log details for cast call
console.log('\n=== CAST CALL DETAILS FOR BURN ===');
console.log('Contract Address:', poolAddress);
console.log('Function: burn(address,address,uint256,uint256,bool)');
console.log('Parameters:');
console.log(' - payer (address):', userAddress);
console.log(' - receiver (address):', userAddress);
console.log(' - lpAmount (uint256):', lpAmount.toString());
console.log(' - deadline (uint256):', deadline.toString());
console.log(' - unwrap (bool):', unwrap);
console.log('\nCast command:');
console.log(`cast call ${poolAddress} "burn(address,address,uint256,uint256,bool)" ${userAddress} ${userAddress} ${lpAmount.toString()} ${deadline.toString()} ${unwrap} --from ${userAddress}`);
console.log('=====================================\n');
// STEP 3: Execute the burn transaction
const hash = await walletClient.writeContract({
address: poolAddress,
abi: IPartyPoolABI,
functionName: 'burn',
args: [
userAddress, // payer
userAddress, // receiver
lpAmount,
deadline,
unwrap,
],
account: userAddress,
});
setBurnHash(hash);
console.log('✅ Burn transaction submitted:', hash);
// Wait for transaction confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('✅ Burn transaction confirmed:', receipt);
// Parse the Burn event from the receipt logs
let actualBurnAmounts: ActualBurnAmounts | null = null;
for (const log of receipt.logs) {
try {
const decodedLog = decodeEventLog({
abi: IPartyPoolABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'Burn') {
const { amounts, lpBurned } = decodedLog.args as {
amounts: bigint[];
lpBurned: bigint;
};
actualBurnAmounts = {
lpBurned,
withdrawAmounts: amounts,
};
console.log('📊 Actual burn amounts from event:', {
lpBurned: lpBurned.toString(),
withdrawAmounts: amounts.map(a => a.toString()),
});
break;
}
} catch (err) {
// Skip logs that don't match our ABI
continue;
}
}
return { receipt, actualBurnAmounts };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Burn failed';
setBurnError(errorMessage);
console.error('❌ Burn error:', err);
throw err;
} finally {
setIsBurning(false);
}
};
return {
executeBurn,
isBurning,
burnHash,
burnError,
};
}