adding unstake basket page
This commit is contained in:
11
src/app/unstake-basket/page.tsx
Normal file
11
src/app/unstake-basket/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -630,7 +630,6 @@ export function useBurnSwap() {
|
||||
deadline: deadline.toString(),
|
||||
unwrap,
|
||||
});
|
||||
|
||||
// STEP 3: Execute the burnSwap transaction
|
||||
const hash = await walletClient.writeContract({
|
||||
address: poolAddress,
|
||||
@@ -709,4 +708,162 @@ export function useBurnSwap() {
|
||||
burnSwapHash,
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user