From e08506e72ca086bf2281edc612ab6b04ecf165ac Mon Sep 17 00:00:00 2001 From: surbhi Date: Wed, 29 Oct 2025 12:36:07 -0400 Subject: [PATCH] burn swap and page menu updates --- src/app/page.tsx | 20 +- src/app/stake/page.tsx | 11 + src/app/unstake/page.tsx | 11 + src/components/header.tsx | 107 ++++++-- src/components/stake-form.tsx | 473 ++++++++++++++++++++++++---------- src/components/swap-form.tsx | 15 +- src/hooks/usePartyPlanner.ts | 133 ++++++++++ src/hooks/usePartyPool.ts | 107 ++++++++ src/locales/en.json | 3 +- 9 files changed, 713 insertions(+), 167 deletions(-) create mode 100644 src/app/stake/page.tsx create mode 100644 src/app/unstake/page.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index be37c93..bbc182c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,29 +1,11 @@ 'use client'; -import { useTranslation } from 'react-i18next'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { SwapForm } from '@/components/swap-form'; -import { StakeForm } from '@/components/stake-form'; export default function HomePage() { - const { t } = useTranslation(); - return (
- - - {t('nav.swap')} - {t('nav.stake')} - - - - - - - - - - +
); } diff --git a/src/app/stake/page.tsx b/src/app/stake/page.tsx new file mode 100644 index 0000000..0b89078 --- /dev/null +++ b/src/app/stake/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { StakeForm } from '@/components/stake-form'; + +export default function StakePage() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/app/unstake/page.tsx b/src/app/unstake/page.tsx new file mode 100644 index 0000000..e95582e --- /dev/null +++ b/src/app/unstake/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { StakeForm } from '@/components/stake-form'; + +export default function UnstakePage() { + return ( +
+ +
+ ); +} diff --git a/src/components/header.tsx b/src/components/header.tsx index 3183d16..8e42c0b 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -4,19 +4,27 @@ import { useTheme } from 'next-themes'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { LanguageSelector } from '@/components/language-selector'; -import { Moon, Sun } from 'lucide-react'; +import { Moon, Sun, Menu, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import Link from 'next/link'; +import { usePathname } from 'next/navigation'; export function Header() { const { theme, setTheme } = useTheme(); const { t } = useTranslation(); const [mounted, setMounted] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const pathname = usePathname(); useEffect(() => { setMounted(true); }, []); + useEffect(() => { + // Close mobile menu when route changes + setMobileMenuOpen(false); + }, [pathname]); + const toggleTheme = () => { console.log('Toggle clicked, current theme:', theme); const newTheme = theme === 'dark' ? 'light' : 'dark'; @@ -26,6 +34,13 @@ export function Header() { const logoSrc = !mounted ? '/logo-dark.svg' : theme === 'dark' ? '/logo-dark.svg' : '/logo-light.svg'; + const navLinks = [ + { href: '/', label: 'Swap' }, + { href: '/stake', label: 'Stake' }, + { href: '/unstake', label: 'Unstake' }, + { href: '/about', label: 'About' }, + ]; + return (
@@ -40,37 +55,95 @@ export function Header() { BETA + + {/* Mobile Menu Button - Moved to left side */} +
- {/* Navigation Links */} + {/* Desktop Navigation Links */} - {/* Language Selector */} - + {/* Language Selector - Hidden on small screens */} +
+ +
- {/* Theme Toggle */} - - {/* Wallet Connect */} + {/* Wallet Connect - Always visible */}
+ + {/* Mobile Menu */} + {mobileMenuOpen && ( +
+ +
+ )}
); } diff --git a/src/components/stake-form.tsx b/src/components/stake-form.tsx index ef05265..2275bbe 100644 --- a/src/components/stake-form.tsx +++ b/src/components/stake-form.tsx @@ -5,18 +5,24 @@ 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 { ChevronDown, CheckCircle, XCircle, Loader2 } from 'lucide-react'; +import { ChevronDown, CheckCircle, XCircle, Loader2, ArrowDownUp } from 'lucide-react'; import { useAccount } from 'wagmi'; -import { useGetAllPools, useTokenDetails, useSwapMintAmounts, type PoolDetails, type TokenDetails } from '@/hooks/usePartyPlanner'; -import { useSwapMint } from '@/hooks/usePartyPool'; +import { useGetAllPools, useTokenDetails, useSwapMintAmounts, useBurnSwapAmounts, useLPTokenBalance, type PoolDetails, type TokenDetails } from '@/hooks/usePartyPlanner'; +import { useSwapMint, useBurnSwap } from '@/hooks/usePartyPool'; import { formatUnits, parseUnits } from 'viem'; import IPartyPoolABI from '@/contracts/IPartyPoolABI'; type TransactionStatus = 'idle' | 'pending' | 'success' | 'error'; +type Mode = 'stake' | 'unstake'; -export function StakeForm() { +interface StakeFormProps { + defaultMode?: Mode; +} + +export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) { const { t } = useTranslation(); const { isConnected, address } = useAccount(); + const [mode, setMode] = useState(defaultMode); const [stakeAmount, setStakeAmount] = useState(''); const [selectedPool, setSelectedPool] = useState(null); const [selectedToken, setSelectedToken] = useState(null); @@ -33,10 +39,17 @@ export function StakeForm() { // Get token details for the user const { tokenDetails, loading: tokensLoading } = useTokenDetails(address); - // Initialize swap mint hook + // Initialize swap mint and burn swap hooks const { executeSwapMint, isSwapMinting } = useSwapMint(); + const { executeBurnSwap, isBurnSwapping } = useBurnSwap(); - // Get available tokens for staking based on selected pool + // Fetch LP token balance (for unstake mode) - must be before isAmountExceedingBalance + const { lpBalance } = useLPTokenBalance( + mode === 'unstake' ? selectedPool?.address : undefined, + address + ); + + // Get available tokens for staking based on selected pool (for unstake mode) const availableTokensForPool = selectedPool && tokenDetails ? tokenDetails.filter(token => selectedPool.tokens.some(poolToken => @@ -45,18 +58,35 @@ export function StakeForm() { ) : []; + // Get available pools for staking based on selected token (for stake mode) + const availablePoolsForToken = selectedToken && poolDetails + ? poolDetails.filter(pool => + pool.tokens.some(poolToken => + poolToken.toLowerCase() === selectedToken.address.toLowerCase() + ) + ) + : []; + // Check if amount exceeds balance const isAmountExceedingBalance = useMemo(() => { - if (!stakeAmount || !selectedToken) return false; + if (!stakeAmount) return false; try { - const amountInWei = parseUnits(stakeAmount, selectedToken.decimals); - return amountInWei > selectedToken.balance; + if (mode === 'stake') { + if (!selectedToken) return false; + const amountInWei = parseUnits(stakeAmount, selectedToken.decimals); + return amountInWei > selectedToken.balance; + } else { + // Unstake mode - check against LP balance + if (!lpBalance) return false; + const amountInWei = parseUnits(stakeAmount, 18); // LP tokens have 18 decimals + return amountInWei > lpBalance; + } } catch { // If parseUnits fails (invalid input), don't show error return false; } - }, [stakeAmount, selectedToken]); + }, [stakeAmount, selectedToken, mode, lpBalance]); // Get the input token index in the selected pool const inputTokenIndex = useMemo(() => { @@ -74,17 +104,26 @@ export function StakeForm() { if (!stakeAmount || !selectedToken) return undefined; try { - return parseUnits(stakeAmount, selectedToken.decimals); + // For unstake mode, LP tokens always have 18 decimals + const decimals = mode === 'unstake' ? 18 : selectedToken.decimals; + return parseUnits(stakeAmount, decimals); } catch { return undefined; } - }, [stakeAmount, selectedToken]); + }, [stakeAmount, selectedToken, mode]); - // Fetch swap mint amounts + // Fetch swap mint amounts (for stake mode) const { swapMintAmounts, loading: swapMintLoading } = useSwapMintAmounts( - selectedPool?.address, - inputTokenIndex, - maxAmountIn + mode === 'stake' ? selectedPool?.address : undefined, + mode === 'stake' ? inputTokenIndex : undefined, + mode === 'stake' ? maxAmountIn : undefined + ); + + // Fetch burn swap amounts (for unstake mode) + const { burnSwapAmounts, loading: burnSwapLoading } = useBurnSwapAmounts( + mode === 'unstake' ? selectedPool?.address : undefined, + mode === 'unstake' ? maxAmountIn : undefined, + mode === 'unstake' ? inputTokenIndex : undefined ); // Close dropdowns when clicking outside @@ -112,17 +151,27 @@ export function StakeForm() { setTransactionError(null); try { - // Execute the swap mint transaction - await executeSwapMint( - selectedPool.address, - selectedToken.address, - inputTokenIndex, - maxAmountIn - ); + if (mode === 'stake') { + // Execute the swap mint transaction + await executeSwapMint( + selectedPool.address, + selectedToken.address, + inputTokenIndex, + maxAmountIn + ); + } else { + // Execute the burn swap transaction + await executeBurnSwap( + selectedPool.address, + maxAmountIn, + inputTokenIndex, + false // unwrap = false by default + ); + } setTransactionStatus('success'); } catch (err) { - console.error('Stake failed:', err); + console.error(`${mode === 'stake' ? 'Stake' : 'Unstake'} failed:`, err); setTransactionError(err instanceof Error ? err.message : 'Transaction failed'); setTransactionStatus('error'); } @@ -139,120 +188,262 @@ export function StakeForm() { setTransactionError(null); }; + const toggleMode = () => { + setMode(mode === 'stake' ? 'unstake' : 'stake'); + // Clear selections when switching modes + setStakeAmount(''); + setSelectedPool(null); + setSelectedToken(null); + }; + return (
- Stake + {mode === 'stake' ? 'Stake' : 'Unstake'}
- {/* Pool Selection */} -
-
- -
-
- - {isPoolDropdownOpen && ( -
- {poolDetails && poolDetails.length > 0 ? ( - poolDetails.map((pool) => ( - - )) - ) : ( -
- {poolsLoading ? 'Loading pools...' : 'No pools available'} + {/* First Selection - depends on mode */} + {mode === 'stake' ? ( + /* Pool Selection (Stake Mode) */ +
+
+ +
+
+
- )} + + + {isPoolDropdownOpen && ( +
+ {poolDetails && poolDetails.length > 0 ? ( + poolDetails.map((pool) => ( + + )) + ) : ( +
+ {poolsLoading ? 'Loading pools...' : 'No pools available'} +
+ )} +
+ )} +
+ ) : ( + /* Pool Selection (Unstake Mode) */ +
+
+ +
+
+ + {isPoolDropdownOpen && ( +
+ {poolDetails && poolDetails.length > 0 ? ( + poolDetails.map((pool) => ( + + )) + ) : ( +
+ {poolsLoading ? 'Loading pools...' : 'No pools available'} +
+ )} +
+ )} +
+
+ )} + + {/* Toggle Button */} +
+
- {/* Token Selection */} -
-
- -
-
- -
- {t('swap.balance')}: {selectedToken ? formatUnits(selectedToken.balance, selectedToken.decimals) : '0.00'} + {/* Second Selection - depends on mode */} + {mode === 'stake' ? ( + /* Token Selection (Stake Mode) */ +
+
+
- {isTokenDropdownOpen && ( -
- {availableTokensForPool.length > 0 ? ( - availableTokensForPool.map((token) => ( - - )) - ) : selectedPool ? ( -
- {tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'} -
+
+ +
+ {t('swap.balance')}: {selectedToken ? formatUnits(selectedToken.balance, selectedToken.decimals) : '0.00'}
- )} + {isTokenDropdownOpen && ( +
+ {availableTokensForPool.length > 0 ? ( + availableTokensForPool.map((token) => ( + + )) + ) : selectedPool ? ( +
+ {tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'} +
+ ) : ( +
+ Select a pool first +
+ )} +
+ )} +
-
+ ) : ( + /* Token Selection (Unstake Mode) */ +
+
+ +
+
+ +
+ {t('swap.balance')}: {lpBalance !== null && selectedPool ? formatUnits(lpBalance, 18) : '0.00'} {selectedPool?.symbol || 'LP'} +
+ {isTokenDropdownOpen && ( +
+ {availableTokensForPool.length > 0 ? ( + availableTokensForPool.map((token) => ( + + )) + ) : selectedPool ? ( +
+ {tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'} +
+ ) : ( +
+ Select a pool first +
+ )} +
+ )} +
+
+ )} {/* Amount Input */}
-
+
+
- {/* Swap Mint Amounts Display */} - {swapMintAmounts && selectedToken && !isAmountExceedingBalance && ( + {/* Swap Mint Amounts Display (Stake Mode) */} + {mode === 'stake' && swapMintAmounts && selectedToken && !isAmountExceedingBalance && (
{t('stake.amountUsed')}: @@ -293,17 +484,29 @@ export function StakeForm() {
)} - {/* Stake Button */} + {/* Burn Swap Amounts Display (Unstake Mode) */} + {mode === 'unstake' && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && ( +
+
+ {t('stake.amountOut')}: + + {burnSwapLoading ? 'Calculating...' : `${formatUnits(burnSwapAmounts, selectedToken.decimals)} ${selectedToken.symbol}`} + +
+
+ )} + + {/* Stake/Unstake Button */} @@ -314,9 +517,14 @@ export function StakeForm() { {transactionStatus === 'pending' && (
-

Approving Stake

+

+ {mode === 'stake' ? 'Approving Stake' : 'Approving Unstake'} +

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

Please confirm the transactions in your wallet @@ -327,9 +535,14 @@ export function StakeForm() { {transactionStatus === 'success' && (

-

Stake Confirmed!

+

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

- Successfully staked {stakeAmount} {selectedToken?.symbol} to {selectedPool?.symbol} + {mode === 'stake' + ? `Successfully staked ${stakeAmount} ${selectedToken?.symbol} to ${selectedPool?.symbol}` + : `Successfully unstaked ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}` + }

(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Handle hydration for Next.js static export + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted || !poolAddress || !lpAmount || lpAmount === BigInt(0) || inputTokenIndex === undefined) { + setLoading(false); + setBurnSwapAmounts(null); + return; + } + + const fetchBurnSwapAmounts = async () => { + if (!publicClient) { + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + // Get chain ID and contract address + const chainId = await publicClient.getChainId(); + const viewerAddress = (chainInfo as Record)[chainId.toString()]?.v1?.PartyPoolViewer; + + if (!viewerAddress) { + setError('IPartyPoolViewer contract not found for current chain'); + setBurnSwapAmounts(null); + return; + } + + // Call burnSwapAmounts function + const result = await publicClient.readContract({ + address: viewerAddress as `0x${string}`, + abi: IPartyPoolViewerABI, + functionName: 'burnSwapAmounts', + args: [poolAddress, lpAmount, BigInt(inputTokenIndex)], + }) as bigint; + + setBurnSwapAmounts(result); + } catch (err) { + console.error('Error calling burnSwapAmounts:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch burn swap amounts'); + setBurnSwapAmounts(null); + } finally { + setLoading(false); + } + }; + + fetchBurnSwapAmounts(); + }, [publicClient, mounted, poolAddress, lpAmount, inputTokenIndex]); + + return { + burnSwapAmounts, + loading, + error, + isReady: mounted, + }; +} + +export function useLPTokenBalance( + poolAddress: `0x${string}` | undefined, + userAddress: `0x${string}` | undefined +) { + const publicClient = usePublicClient(); + const [mounted, setMounted] = useState(false); + const [lpBalance, setLpBalance] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Handle hydration for Next.js static export + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted || !poolAddress || !userAddress) { + setLoading(false); + setLpBalance(null); + return; + } + + const fetchLPBalance = async () => { + if (!publicClient) { + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + // Call balanceOf on the pool (which is an ERC20 LP token) + const balance = await publicClient.readContract({ + address: poolAddress, + abi: ERC20ABI, + functionName: 'balanceOf', + args: [userAddress], + }) as bigint; + + setLpBalance(balance); + } catch (err) { + console.error('Error fetching LP token balance:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch LP balance'); + setLpBalance(null); + } finally { + setLoading(false); + } + }; + + fetchLPBalance(); + }, [publicClient, mounted, poolAddress, userAddress]); + + return { + lpBalance, + loading, + error, + isReady: mounted, + }; } \ No newline at end of file diff --git a/src/hooks/usePartyPool.ts b/src/hooks/usePartyPool.ts index d706099..dacfd6d 100644 --- a/src/hooks/usePartyPool.ts +++ b/src/hooks/usePartyPool.ts @@ -462,4 +462,111 @@ export function useSwapMint() { swapMintHash, swapMintError, }; +} + +export function useBurnSwap() { + const publicClient = usePublicClient(); + const { data: walletClient } = useWalletClient(); + const [isBurnSwapping, setIsBurnSwapping] = useState(false); + const [burnSwapHash, setBurnSwapHash] = useState<`0x${string}` | null>(null); + const [burnSwapError, setBurnSwapError] = useState(null); + + const executeBurnSwap = async ( + poolAddress: `0x${string}`, + lpAmount: bigint, + inputTokenIndex: number, + unwrap: boolean = false + ) => { + if (!walletClient || !publicClient) { + setBurnSwapError('Wallet not connected'); + return; + } + + try { + setIsBurnSwapping(true); + setBurnSwapError(null); + setBurnSwapHash(null); + + const userAddress = walletClient.account.address; + + // STEP 1: Approve the pool to spend the LP tokens + console.log('🔐 Approving LP token spend for burn swap...'); + 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], + }); + + console.log('✅ Approval transaction submitted:', approvalHash); + await publicClient.waitForTransactionReceipt({ hash: approvalHash }); + console.log('✅ Approval confirmed'); + + // STEP 2: Calculate deadline (5 minutes from now) + const deadline = BigInt(Math.floor(Date.now() / 1000) + 300); // 5 minutes = 300 seconds + + console.log('🚀 Executing burnSwap with params:', { + pool: poolAddress, + payer: userAddress, + receiver: userAddress, + lpAmount: lpAmount.toString(), + inputTokenIndex, + deadline: deadline.toString(), + unwrap, + }); + + // STEP 3: Execute the burnSwap transaction + const hash = await walletClient.writeContract({ + address: poolAddress, + abi: IPartyPoolABI, + functionName: 'burnSwap', + args: [ + userAddress, // payer + userAddress, // receiver + lpAmount, + BigInt(inputTokenIndex), + deadline, + unwrap, + ], + }); + + setBurnSwapHash(hash); + console.log('✅ BurnSwap transaction submitted:', hash); + + // Wait for transaction confirmation + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + console.log('✅ BurnSwap transaction confirmed:', receipt); + + return receipt; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'BurnSwap failed'; + setBurnSwapError(errorMessage); + console.error('❌ BurnSwap error:', err); + throw err; + } finally { + setIsBurnSwapping(false); + } + }; + + return { + executeBurnSwap, + isBurnSwapping, + burnSwapHash, + burnSwapError, + }; } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 97323ab..e1d7aae 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -28,6 +28,7 @@ "insufficientBalance": "Insufficient balance", "amountUsed": "Amount Used", "fee": "Fee", - "lpMinted": "LP Minted" + "lpMinted": "LP Minted", + "amountOut": "Amount Out" } }