From f690799b509089305ed4b530324ba9b578cf899b Mon Sep 17 00:00:00 2001 From: surbhi Date: Thu, 6 Nov 2025 12:39:16 -0400 Subject: [PATCH] adding unstake basket page --- src/app/unstake-basket/page.tsx | 11 + src/components/header.tsx | 1 + src/components/unstake-basket-form.tsx | 379 +++++++++++++++++++++++++ src/hooks/usePartyPool.ts | 159 ++++++++++- 4 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 src/app/unstake-basket/page.tsx create mode 100644 src/components/unstake-basket-form.tsx diff --git a/src/app/unstake-basket/page.tsx b/src/app/unstake-basket/page.tsx new file mode 100644 index 0000000..d24b18c --- /dev/null +++ b/src/app/unstake-basket/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { UnstakeBasketForm } from '@/components/unstake-basket-form'; + +export default function UnstakeBasketPage() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/header.tsx b/src/components/header.tsx index 8e42c0b..d8b5d35 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -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' }, ]; diff --git a/src/components/unstake-basket-form.tsx b/src/components/unstake-basket-form.tsx new file mode 100644 index 0000000..9ad6531 --- /dev/null +++ b/src/components/unstake-basket-form.tsx @@ -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>(new Set()); + const [poolsWithBalances, setPoolsWithBalances] = useState([]); + const [transactionStatus, setTransactionStatus] = useState('idle'); + const [transactionError, setTransactionError] = useState(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, + publicClient.readContract({ + address: tokenAddress as `0x${string}`, + abi: ERC20ABI, + functionName: 'decimals', + }) as Promise, + ]); + + 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 ( + + + Unstake Basket +

+ Select pools to burn all LP tokens and receive all underlying tokens proportionally +

+
+ + {/* Pool Selection List */} + {poolsLoading ? ( +
+ +
+ ) : poolsWithBalances.length === 0 ? ( +
+

No pools with LP token balance found.

+

Stake some tokens first to use this feature.

+
+ ) : ( +
+ {poolsWithBalances.map((pool) => ( +
togglePoolSelection(pool.address)} + > +
+
+
+ {}} + className="w-4 h-4" + /> +
+
{pool.symbol}
+
{pool.name}
+
+
+
+
+
+ {formatUnits(pool.lpBalance, 18)} LP +
+ {poolTokens[pool.address] && ( +
+ {poolTokens[pool.address].length} tokens +
+ )} +
+
+ {selectedPools.has(pool.address) && poolTokens[pool.address] && ( +
+
You will receive:
+
+ {poolTokens[pool.address].map((token) => ( +
+ {token.symbol} + + (proportional to pool composition) + +
+ ))} +
+
+ )} +
+ ))} +
+ )} + + {/* Burn Button */} + + + {selectedPools.size > 0 && ( +
+ This will burn all LP tokens from {selectedPools.size} selected pool{selectedPools.size !== 1 ? 's' : ''} +
+ )} +
+ + {/* Transaction Modal Overlay */} + {transactionStatus !== 'idle' && ( +
+
+ {transactionStatus === 'pending' && ( +
+ +

+ Burning LP Tokens +

+

+ Burning LP tokens from {selectedPools.size} pool{selectedPools.size !== 1 ? 's' : ''} +

+

+ Please confirm the transactions in your wallet +

+
+ )} + + {transactionStatus === 'success' && ( +
+ +

+ Burn Confirmed! +

+ +
+ {Object.entries(actualBurnResults).map(([poolAddress, burnAmounts]) => { + const pool = poolsWithBalances.find(p => p.address === poolAddress); + const tokens = poolTokens[poolAddress]; + + if (!pool || !tokens) return null; + + return ( +
+
{pool.symbol}
+
+
+ LP Burned: + + {formatUnits(burnAmounts.lpBurned, 18)} {pool.symbol} + +
+
Tokens Received:
+ {burnAmounts.withdrawAmounts.map((amount, index) => { + const token = tokens[index]; + if (!token) return null; + + return ( +
+ {token.symbol}: + + {formatUnits(amount, token.decimals)} + +
+ ); + })} +
+
+ ); + })} +
+ + +
+ )} + + {transactionStatus === 'error' && ( +
+ +

+ Burn Failed +

+

+ {transactionError || 'Transaction failed'} +

+ +
+ )} +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/hooks/usePartyPool.ts b/src/hooks/usePartyPool.ts index 09e538c..f923269 100644 --- a/src/hooks/usePartyPool.ts +++ b/src/hooks/usePartyPool.ts @@ -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(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, + }; } \ No newline at end of file