From 6e5eca7543fd0095275581b7d3ff6c6996f88a3e Mon Sep 17 00:00:00 2001 From: surbhi Date: Tue, 11 Nov 2025 14:41:52 -0400 Subject: [PATCH] moving redeem all functionality to stake form. Getting rid of unstake basket tab --- src/app/unstake-basket/page.tsx | 11 - src/components/header.tsx | 1 - src/components/stake-form.tsx | 252 +++++++++++++--- src/components/unstake-basket-form.tsx | 379 ------------------------- 4 files changed, 213 insertions(+), 430 deletions(-) delete mode 100644 src/app/unstake-basket/page.tsx delete mode 100644 src/components/unstake-basket-form.tsx diff --git a/src/app/unstake-basket/page.tsx b/src/app/unstake-basket/page.tsx deleted file mode 100644 index d24b18c..0000000 --- a/src/app/unstake-basket/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'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 d8b5d35..8e42c0b 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -38,7 +38,6 @@ 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/stake-form.tsx b/src/components/stake-form.tsx index 339db16..4e2edf9 100644 --- a/src/components/stake-form.tsx +++ b/src/components/stake-form.tsx @@ -6,11 +6,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { ChevronDown, CheckCircle, XCircle, Loader2, ArrowDownUp } from 'lucide-react'; -import { useAccount } from 'wagmi'; +import { useAccount, usePublicClient } from 'wagmi'; import { useGetAllPools, useTokenDetails, useSwapMintAmounts, useBurnSwapAmounts, useLPTokenBalance, type PoolDetails, type TokenDetails, type BurnSwapAmounts } from '@/hooks/usePartyPlanner'; -import { useSwapMint, useBurnSwap, type ActualSwapMintAmounts, type ActualBurnSwapAmounts } from '@/hooks/usePartyPool'; +import { useSwapMint, useBurnSwap, useBurn, type ActualSwapMintAmounts, type ActualBurnSwapAmounts, 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'; type Mode = 'stake' | 'unstake'; @@ -19,9 +20,16 @@ interface StakeFormProps { defaultMode?: Mode; } +interface TokenInfo { + address: `0x${string}`; + symbol: string; + decimals: number; +} + export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { const { t } = useTranslation(); const { isConnected, address } = useAccount(); + const publicClient = usePublicClient(); const [mode, setMode] = useState(defaultMode); const [stakeAmount, setStakeAmount] = useState(''); const [selectedPool, setSelectedPool] = useState(null); @@ -32,6 +40,9 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { const [transactionError, setTransactionError] = useState(null); const [actualSwapMintAmounts, setActualSwapMintAmounts] = useState(null); const [actualBurnSwapAmounts, setActualBurnSwapAmounts] = useState(null); + const [actualBurnAmounts, setActualBurnAmounts] = useState(null); + const [redeemAll, setRedeemAll] = useState(false); + const [poolTokens, setPoolTokens] = useState([]); const poolDropdownRef = useRef(null); const tokenDropdownRef = useRef(null); @@ -44,6 +55,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { // Initialize swap mint and burn swap hooks const { executeSwapMint, isSwapMinting } = useSwapMint(); const { executeBurnSwap, isBurnSwapping } = useBurnSwap(); + const { executeBurn, isBurning } = useBurn(); // Fetch LP token balance (for unstake mode) - must be before isAmountExceedingBalance const { lpBalance } = useLPTokenBalance( @@ -103,12 +115,15 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { // Parse the stake amount to Wei const maxAmountIn = useMemo(() => { - if (!stakeAmount || !selectedToken) return undefined; + if (!stakeAmount) return undefined; try { // For unstake mode, LP tokens always have 18 decimals - const decimals = mode === 'unstake' ? 18 : selectedToken.decimals; - return parseUnits(stakeAmount, decimals); + if (mode === 'unstake') { + return parseUnits(stakeAmount, 18); + } + if (!selectedToken) return undefined; + return parseUnits(stakeAmount, selectedToken.decimals); } catch { return undefined; } @@ -121,13 +136,54 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { mode === 'stake' ? maxAmountIn : undefined ); - // Fetch burn swap amounts (for unstake mode) + // Fetch burn swap amounts (for unstake mode, only when not redeeming all) const { burnSwapAmounts, loading: burnSwapLoading } = useBurnSwapAmounts( - mode === 'unstake' ? selectedPool?.address : undefined, - mode === 'unstake' ? maxAmountIn : undefined, - mode === 'unstake' ? inputTokenIndex : undefined + mode === 'unstake' && !redeemAll ? selectedPool?.address : undefined, + mode === 'unstake' && !redeemAll ? maxAmountIn : undefined, + mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined ); + // Fetch token details for the selected pool when Redeem All is active + useEffect(() => { + if (!publicClient || !selectedPool || mode !== 'unstake' || !redeemAll) { + setPoolTokens([]); + return; + } + + const fetchTokenDetails = async () => { + const tokenInfos: TokenInfo[] = []; + + for (const tokenAddress of selectedPool.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); + } + } + + setPoolTokens(tokenInfos); + }; + + fetchTokenDetails(); + }, [publicClient, selectedPool, mode, redeemAll]); + // Close dropdowns when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -144,16 +200,23 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { }, []); const handleStake = async () => { - if (!selectedPool || !selectedToken || !stakeAmount || inputTokenIndex === undefined || !maxAmountIn) { - console.error('Missing required stake parameters'); + if (!selectedPool || !stakeAmount || !maxAmountIn) { + console.error('Missing required parameters'); return; } + if (mode === 'unstake' && !redeemAll) { + if (!selectedToken || inputTokenIndex === undefined) { + console.error('Missing required unstake parameters'); + return; + } + } setTransactionStatus('pending'); setTransactionError(null); try { if (mode === 'stake') { + if (!selectedToken || inputTokenIndex === undefined) throw new Error('Missing stake parameters'); // Execute the swap mint transaction and capture actual amounts const result = await executeSwapMint( selectedPool.address, @@ -166,8 +229,19 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { if (result?.actualSwapMintAmounts) { setActualSwapMintAmounts(result.actualSwapMintAmounts); } + } else if (redeemAll) { + // Execute full burn (redeem all tokens from the pool) + const result = await executeBurn( + selectedPool.address, + maxAmountIn, + false // unwrap = false by default + ); + if (result?.actualBurnAmounts) { + setActualBurnAmounts(result.actualBurnAmounts); + } } else { // Execute the burn swap transaction and capture actual amounts + if (inputTokenIndex === undefined) throw new Error('Missing input token index'); const result = await executeBurnSwap( selectedPool.address, maxAmountIn, @@ -183,7 +257,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { setTransactionStatus('success'); } catch (err) { - console.error(`${mode === 'stake' ? 'Stake' : 'Unstake'} failed:`, err); + console.error(`${mode === 'stake' ? 'Stake' : redeemAll ? 'Redeem' : 'Unstake'} failed:`, err); setTransactionError(err instanceof Error ? err.message : 'Transaction failed'); setTransactionStatus('error'); } @@ -195,6 +269,10 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { setStakeAmount(''); setSelectedPool(null); setSelectedToken(null); + setRedeemAll(false); + setActualBurnAmounts(null); + setActualBurnSwapAmounts(null); + setActualSwapMintAmounts(null); } setTransactionStatus('idle'); setTransactionError(null); @@ -206,6 +284,10 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { setStakeAmount(''); setSelectedPool(null); setSelectedToken(null); + setRedeemAll(false); + setActualBurnAmounts(null); + setActualBurnSwapAmounts(null); + setActualSwapMintAmounts(null); }; return ( @@ -395,9 +477,11 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { variant="secondary" className="w-full h-16 justify-between" onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)} - disabled={!selectedPool} + disabled={!selectedPool || redeemAll} > - {selectedToken ? ( + {redeemAll ? ( + All Tokens (Redeem All) + ) : selectedToken ? ( {selectedToken.symbol} ) : ( t('stake.selectToken') @@ -407,7 +491,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
{t('swap.balance')}: {lpBalance !== null && selectedPool ? formatUnits(lpBalance, 18) : '0.00'} {selectedPool?.symbol || 'LP'}
- {isTokenDropdownOpen && ( + {!redeemAll && isTokenDropdownOpen && (
{availableTokensForPool.length > 0 ? ( availableTokensForPool.map((token) => ( @@ -441,21 +525,40 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
- +
+ {mode === 'unstake' && ( + + )} + +
setStakeAmount(e.target.value)} className="text-2xl h-16" - disabled={!selectedToken || !selectedPool} + disabled={!selectedPool || (mode === 'stake' && !selectedToken)} /> {isAmountExceedingBalance && (

@@ -497,7 +600,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { )} {/* Burn Swap Amounts Display (Unstake Mode) */} - {mode === 'unstake' && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && ( + {mode === 'unstake' && !redeemAll && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && (

{t('stake.amountOut')}: @@ -514,17 +617,48 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
)} + {/* Redeem All Tokens Display */} + {mode === 'unstake' && redeemAll && poolTokens.length > 0 && ( +
+
You will receive:
+
+ {poolTokens.map((token) => ( +
+ {token.symbol} + + (proportional to pool composition) + +
+ ))} +
+
+ )} + {/* Stake/Unstake Button */} @@ -536,12 +670,14 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {

- {mode === 'stake' ? 'Approving Stake' : 'Approving Unstake'} + {mode === 'stake' ? 'Approving Stake' : redeemAll ? 'Approving Redeem All' : 'Approving Unstake'}

{mode === 'stake' ? `Staking ${stakeAmount} ${selectedToken?.symbol} to ${selectedPool?.symbol}` - : `Unstaking ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}` + : redeemAll + ? `Redeeming ${stakeAmount} ${selectedPool?.symbol} LP for all pool tokens` + : `Unstaking ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}` }

@@ -554,7 +690,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {

- {mode === 'stake' ? 'Stake Confirmed!' : 'Unstake Confirmed!'} + {mode === 'stake' ? 'Stake Confirmed!' : redeemAll ? 'Redeem Confirmed!' : 'Unstake Confirmed!'}

{/* Display actual amounts or estimates */} @@ -594,6 +730,44 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { )}
+ ) : redeemAll ? ( + // Redeem All mode success message +
+ {actualBurnAmounts && selectedPool ? ( + // Show actual amounts from transaction + <> +
+ LP Burned: + {formatUnits(actualBurnAmounts.lpBurned, 18)} {selectedPool.symbol} +
+
Tokens Received:
+ {actualBurnAmounts.withdrawAmounts.map((amount, index) => { + const token = poolTokens[index]; + if (!token) return null; + + return ( +
+ {token.symbol}: + + {formatUnits(amount, token.decimals)} + +
+ ); + })} + + ) : ( + // Fallback to estimates + <> +

+ Successfully redeemed {stakeAmount} {selectedPool?.symbol} LP for all pool tokens +
+ + *Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage. + +

+ + )} +
) : ( // Unstake mode success message
diff --git a/src/components/unstake-basket-form.tsx b/src/components/unstake-basket-form.tsx deleted file mode 100644 index 9ad6531..0000000 --- a/src/components/unstake-basket-form.tsx +++ /dev/null @@ -1,379 +0,0 @@ -'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