burn swap and page menu updates
This commit is contained in:
@@ -1,29 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import { SwapForm } from '@/components/swap-form';
|
import { SwapForm } from '@/components/swap-form';
|
||||||
import { StakeForm } from '@/components/stake-form';
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md mx-auto">
|
<div className="w-full max-w-md mx-auto">
|
||||||
<Tabs defaultValue="swap" className="w-full">
|
<SwapForm />
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-8">
|
|
||||||
<TabsTrigger value="swap">{t('nav.swap')}</TabsTrigger>
|
|
||||||
<TabsTrigger value="stake">{t('nav.stake')}</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="swap">
|
|
||||||
<SwapForm />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="stake">
|
|
||||||
<StakeForm />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/app/stake/page.tsx
Normal file
11
src/app/stake/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { StakeForm } from '@/components/stake-form';
|
||||||
|
|
||||||
|
export default function StakePage() {
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md mx-auto">
|
||||||
|
<StakeForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/app/unstake/page.tsx
Normal file
11
src/app/unstake/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { StakeForm } from '@/components/stake-form';
|
||||||
|
|
||||||
|
export default function UnstakePage() {
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md mx-auto">
|
||||||
|
<StakeForm defaultMode="unstake" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,19 +4,27 @@ import { useTheme } from 'next-themes';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { LanguageSelector } from '@/components/language-selector';
|
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 { useEffect, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Close mobile menu when route changes
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
console.log('Toggle clicked, current theme:', theme);
|
console.log('Toggle clicked, current theme:', theme);
|
||||||
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
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 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 (
|
return (
|
||||||
<header className="border-b">
|
<header className="border-b">
|
||||||
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||||
@@ -40,37 +55,95 @@ export function Header() {
|
|||||||
<span className="bg-yellow-500/20 text-yellow-500 text-xs font-bold px-2 py-1 rounded border border-yellow-500/50">
|
<span className="bg-yellow-500/20 text-yellow-500 text-xs font-bold px-2 py-1 rounded border border-yellow-500/50">
|
||||||
BETA
|
BETA
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{/* Mobile Menu Button - Moved to left side */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="md:hidden"
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? (
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">Toggle menu</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Navigation Links */}
|
{/* Desktop Navigation Links */}
|
||||||
<nav className="hidden md:flex items-center gap-1 mr-2">
|
<nav className="hidden md:flex items-center gap-1 mr-2">
|
||||||
<Link href="/">
|
{navLinks.map((link) => (
|
||||||
<Button variant="ghost" size="sm" className="text-sm font-medium">
|
<Link key={link.href} href={link.href}>
|
||||||
Swap
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</Link>
|
size="sm"
|
||||||
<Link href="/about">
|
className={`text-sm font-medium ${pathname === link.href ? 'bg-accent' : ''}`}
|
||||||
<Button variant="ghost" size="sm" className="text-sm font-medium">
|
>
|
||||||
About
|
{link.label}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Language Selector */}
|
{/* Language Selector - Hidden on small screens */}
|
||||||
<LanguageSelector />
|
<div className="hidden sm:block">
|
||||||
|
<LanguageSelector />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Theme Toggle */}
|
{/* Theme Toggle - Hidden on mobile */}
|
||||||
<Button variant="ghost" size="icon" onClick={toggleTheme} type="button">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
type="button"
|
||||||
|
className="hidden sm:flex"
|
||||||
|
>
|
||||||
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Wallet Connect */}
|
{/* Wallet Connect - Always visible */}
|
||||||
<ConnectButton />
|
<ConnectButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="md:hidden border-t">
|
||||||
|
<nav className="container mx-auto px-4 py-4 flex flex-col gap-2">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link key={link.href} href={link.href}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className={`w-full justify-start text-base font-medium ${pathname === link.href ? 'bg-accent' : ''}`}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<div className="flex items-center gap-2 pt-2 border-t mt-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<LanguageSelector />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
type="button"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,24 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { useAccount } from 'wagmi';
|
||||||
import { useGetAllPools, useTokenDetails, useSwapMintAmounts, type PoolDetails, type TokenDetails } from '@/hooks/usePartyPlanner';
|
import { useGetAllPools, useTokenDetails, useSwapMintAmounts, useBurnSwapAmounts, useLPTokenBalance, type PoolDetails, type TokenDetails } from '@/hooks/usePartyPlanner';
|
||||||
import { useSwapMint } from '@/hooks/usePartyPool';
|
import { useSwapMint, useBurnSwap } from '@/hooks/usePartyPool';
|
||||||
import { formatUnits, parseUnits } from 'viem';
|
import { formatUnits, parseUnits } from 'viem';
|
||||||
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
|
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
|
||||||
|
|
||||||
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
|
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 { t } = useTranslation();
|
||||||
const { isConnected, address } = useAccount();
|
const { isConnected, address } = useAccount();
|
||||||
|
const [mode, setMode] = useState<Mode>(defaultMode);
|
||||||
const [stakeAmount, setStakeAmount] = useState('');
|
const [stakeAmount, setStakeAmount] = useState('');
|
||||||
const [selectedPool, setSelectedPool] = useState<PoolDetails | null>(null);
|
const [selectedPool, setSelectedPool] = useState<PoolDetails | null>(null);
|
||||||
const [selectedToken, setSelectedToken] = useState<TokenDetails | null>(null);
|
const [selectedToken, setSelectedToken] = useState<TokenDetails | null>(null);
|
||||||
@@ -33,10 +39,17 @@ export function StakeForm() {
|
|||||||
// Get token details for the user
|
// Get token details for the user
|
||||||
const { tokenDetails, loading: tokensLoading } = useTokenDetails(address);
|
const { tokenDetails, loading: tokensLoading } = useTokenDetails(address);
|
||||||
|
|
||||||
// Initialize swap mint hook
|
// Initialize swap mint and burn swap hooks
|
||||||
const { executeSwapMint, isSwapMinting } = useSwapMint();
|
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
|
const availableTokensForPool = selectedPool && tokenDetails
|
||||||
? tokenDetails.filter(token =>
|
? tokenDetails.filter(token =>
|
||||||
selectedPool.tokens.some(poolToken =>
|
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
|
// Check if amount exceeds balance
|
||||||
const isAmountExceedingBalance = useMemo(() => {
|
const isAmountExceedingBalance = useMemo(() => {
|
||||||
if (!stakeAmount || !selectedToken) return false;
|
if (!stakeAmount) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const amountInWei = parseUnits(stakeAmount, selectedToken.decimals);
|
if (mode === 'stake') {
|
||||||
return amountInWei > selectedToken.balance;
|
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 {
|
} catch {
|
||||||
// If parseUnits fails (invalid input), don't show error
|
// If parseUnits fails (invalid input), don't show error
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [stakeAmount, selectedToken]);
|
}, [stakeAmount, selectedToken, mode, lpBalance]);
|
||||||
|
|
||||||
// Get the input token index in the selected pool
|
// Get the input token index in the selected pool
|
||||||
const inputTokenIndex = useMemo(() => {
|
const inputTokenIndex = useMemo(() => {
|
||||||
@@ -74,17 +104,26 @@ export function StakeForm() {
|
|||||||
if (!stakeAmount || !selectedToken) return undefined;
|
if (!stakeAmount || !selectedToken) return undefined;
|
||||||
|
|
||||||
try {
|
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 {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}, [stakeAmount, selectedToken]);
|
}, [stakeAmount, selectedToken, mode]);
|
||||||
|
|
||||||
// Fetch swap mint amounts
|
// Fetch swap mint amounts (for stake mode)
|
||||||
const { swapMintAmounts, loading: swapMintLoading } = useSwapMintAmounts(
|
const { swapMintAmounts, loading: swapMintLoading } = useSwapMintAmounts(
|
||||||
selectedPool?.address,
|
mode === 'stake' ? selectedPool?.address : undefined,
|
||||||
inputTokenIndex,
|
mode === 'stake' ? inputTokenIndex : undefined,
|
||||||
maxAmountIn
|
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
|
// Close dropdowns when clicking outside
|
||||||
@@ -112,17 +151,27 @@ export function StakeForm() {
|
|||||||
setTransactionError(null);
|
setTransactionError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute the swap mint transaction
|
if (mode === 'stake') {
|
||||||
await executeSwapMint(
|
// Execute the swap mint transaction
|
||||||
selectedPool.address,
|
await executeSwapMint(
|
||||||
selectedToken.address,
|
selectedPool.address,
|
||||||
inputTokenIndex,
|
selectedToken.address,
|
||||||
maxAmountIn
|
inputTokenIndex,
|
||||||
);
|
maxAmountIn
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Execute the burn swap transaction
|
||||||
|
await executeBurnSwap(
|
||||||
|
selectedPool.address,
|
||||||
|
maxAmountIn,
|
||||||
|
inputTokenIndex,
|
||||||
|
false // unwrap = false by default
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setTransactionStatus('success');
|
setTransactionStatus('success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Stake failed:', err);
|
console.error(`${mode === 'stake' ? 'Stake' : 'Unstake'} failed:`, err);
|
||||||
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
|
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
|
||||||
setTransactionStatus('error');
|
setTransactionStatus('error');
|
||||||
}
|
}
|
||||||
@@ -139,120 +188,262 @@ export function StakeForm() {
|
|||||||
setTransactionError(null);
|
setTransactionError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleMode = () => {
|
||||||
|
setMode(mode === 'stake' ? 'unstake' : 'stake');
|
||||||
|
// Clear selections when switching modes
|
||||||
|
setStakeAmount('');
|
||||||
|
setSelectedPool(null);
|
||||||
|
setSelectedToken(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md mx-auto relative">
|
<Card className="w-full max-w-md mx-auto relative">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<CardTitle>Stake</CardTitle>
|
<CardTitle>{mode === 'stake' ? 'Stake' : 'Unstake'}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Pool Selection */}
|
{/* First Selection - depends on mode */}
|
||||||
<div className="space-y-2">
|
{mode === 'stake' ? (
|
||||||
<div className="text-sm">
|
/* Pool Selection (Stake Mode) */
|
||||||
<label className="text-muted-foreground">{t('stake.selectPool')}</label>
|
<div className="space-y-2">
|
||||||
</div>
|
<div className="text-sm">
|
||||||
<div className="relative" ref={poolDropdownRef}>
|
<label className="text-muted-foreground">{t('stake.selectPool')}</label>
|
||||||
<Button
|
</div>
|
||||||
variant="secondary"
|
<div className="relative" ref={poolDropdownRef}>
|
||||||
className="w-full h-16 justify-between"
|
<Button
|
||||||
onClick={() => setIsPoolDropdownOpen(!isPoolDropdownOpen)}
|
variant="secondary"
|
||||||
>
|
className="w-full h-16 justify-between"
|
||||||
{selectedPool ? (
|
onClick={() => setIsPoolDropdownOpen(!isPoolDropdownOpen)}
|
||||||
<div className="flex flex-col items-start">
|
>
|
||||||
<span className="font-medium">{selectedPool.symbol}</span>
|
{selectedPool ? (
|
||||||
<span className="text-xs text-muted-foreground">{selectedPool.name}</span>
|
<div className="flex flex-col items-start">
|
||||||
</div>
|
<span className="font-medium">{selectedPool.symbol}</span>
|
||||||
) : (
|
<span className="text-xs text-muted-foreground">{selectedPool.name}</span>
|
||||||
t('stake.selectPool')
|
|
||||||
)}
|
|
||||||
<ChevronDown className="h-4 w-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
{isPoolDropdownOpen && (
|
|
||||||
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
|
||||||
{poolDetails && poolDetails.length > 0 ? (
|
|
||||||
poolDetails.map((pool) => (
|
|
||||||
<button
|
|
||||||
key={pool.address}
|
|
||||||
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedPool(pool);
|
|
||||||
setIsPoolDropdownOpen(false);
|
|
||||||
// Reset token selection when pool changes
|
|
||||||
setSelectedToken(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">{pool.symbol}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">{pool.name}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
||||||
{poolsLoading ? 'Loading pools...' : 'No pools available'}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
t('stake.selectPool')
|
||||||
)}
|
)}
|
||||||
</div>
|
<ChevronDown className="h-4 w-4 ml-2" />
|
||||||
)}
|
</Button>
|
||||||
|
{isPoolDropdownOpen && (
|
||||||
|
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
||||||
|
{poolDetails && poolDetails.length > 0 ? (
|
||||||
|
poolDetails.map((pool) => (
|
||||||
|
<button
|
||||||
|
key={pool.address}
|
||||||
|
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPool(pool);
|
||||||
|
setIsPoolDropdownOpen(false);
|
||||||
|
setSelectedToken(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{pool.symbol}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{pool.name}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||||||
|
{poolsLoading ? 'Loading pools...' : 'No pools available'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Pool Selection (Unstake Mode) */
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm">
|
||||||
|
<label className="text-muted-foreground">{t('stake.selectPool')}</label>
|
||||||
|
</div>
|
||||||
|
<div className="relative" ref={poolDropdownRef}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full h-16 justify-between"
|
||||||
|
onClick={() => setIsPoolDropdownOpen(!isPoolDropdownOpen)}
|
||||||
|
>
|
||||||
|
{selectedPool ? (
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<span className="font-medium">{selectedPool.symbol}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{selectedPool.name}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t('stake.selectPool')
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
{isPoolDropdownOpen && (
|
||||||
|
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
||||||
|
{poolDetails && poolDetails.length > 0 ? (
|
||||||
|
poolDetails.map((pool) => (
|
||||||
|
<button
|
||||||
|
key={pool.address}
|
||||||
|
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPool(pool);
|
||||||
|
setIsPoolDropdownOpen(false);
|
||||||
|
setSelectedToken(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{pool.symbol}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{pool.name}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||||||
|
{poolsLoading ? 'Loading pools...' : 'No pools available'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Toggle Button */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleMode}
|
||||||
|
className="rounded-full"
|
||||||
|
title={mode === 'stake' ? 'Switch to Unstake' : 'Switch to Stake'}
|
||||||
|
>
|
||||||
|
<ArrowDownUp className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Token Selection */}
|
{/* Second Selection - depends on mode */}
|
||||||
<div className="space-y-2">
|
{mode === 'stake' ? (
|
||||||
<div className="text-sm">
|
/* Token Selection (Stake Mode) */
|
||||||
<label className="text-muted-foreground">{t('stake.selectToken')}</label>
|
<div className="space-y-2">
|
||||||
</div>
|
<div className="text-sm">
|
||||||
<div className="relative" ref={tokenDropdownRef}>
|
<label className="text-muted-foreground">{t('stake.selectToken')}</label>
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
className="w-full h-16 justify-between"
|
|
||||||
onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
|
|
||||||
disabled={!selectedPool}
|
|
||||||
>
|
|
||||||
{selectedToken ? (
|
|
||||||
<span className="font-medium">{selectedToken.symbol}</span>
|
|
||||||
) : (
|
|
||||||
t('stake.selectToken')
|
|
||||||
)}
|
|
||||||
<ChevronDown className="h-4 w-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
<div className="text-xs text-muted-foreground text-right mt-1">
|
|
||||||
{t('swap.balance')}: {selectedToken ? formatUnits(selectedToken.balance, selectedToken.decimals) : '0.00'}
|
|
||||||
</div>
|
</div>
|
||||||
{isTokenDropdownOpen && (
|
<div className="relative" ref={tokenDropdownRef}>
|
||||||
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
<Button
|
||||||
{availableTokensForPool.length > 0 ? (
|
variant="secondary"
|
||||||
availableTokensForPool.map((token) => (
|
className="w-full h-16 justify-between"
|
||||||
<button
|
onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
|
||||||
key={token.address}
|
disabled={!selectedPool}
|
||||||
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
>
|
||||||
onClick={() => {
|
{selectedToken ? (
|
||||||
setSelectedToken(token);
|
<span className="font-medium">{selectedToken.symbol}</span>
|
||||||
setIsTokenDropdownOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="font-medium">{token.symbol}</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
) : selectedPool ? (
|
|
||||||
<div className="px-4 py-3 text-sm text-muted-foreground">
|
|
||||||
{tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="px-4 py-3 text-sm text-muted-foreground">
|
t('stake.selectToken')
|
||||||
Select a pool first
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<ChevronDown className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
<div className="text-xs text-muted-foreground text-right mt-1">
|
||||||
|
{t('swap.balance')}: {selectedToken ? formatUnits(selectedToken.balance, selectedToken.decimals) : '0.00'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{isTokenDropdownOpen && (
|
||||||
|
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
||||||
|
{availableTokensForPool.length > 0 ? (
|
||||||
|
availableTokensForPool.map((token) => (
|
||||||
|
<button
|
||||||
|
key={token.address}
|
||||||
|
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedToken(token);
|
||||||
|
setIsTokenDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="font-medium">{token.symbol}</span>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : selectedPool ? (
|
||||||
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||||||
|
{tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||||||
|
Select a pool first
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
/* Token Selection (Unstake Mode) */
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm">
|
||||||
|
<label className="text-muted-foreground">{t('stake.selectToken')}</label>
|
||||||
|
</div>
|
||||||
|
<div className="relative" ref={tokenDropdownRef}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full h-16 justify-between"
|
||||||
|
onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
|
||||||
|
disabled={!selectedPool}
|
||||||
|
>
|
||||||
|
{selectedToken ? (
|
||||||
|
<span className="font-medium">{selectedToken.symbol}</span>
|
||||||
|
) : (
|
||||||
|
t('stake.selectToken')
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
<div className="text-xs text-muted-foreground text-right mt-1">
|
||||||
|
{t('swap.balance')}: {lpBalance !== null && selectedPool ? formatUnits(lpBalance, 18) : '0.00'} {selectedPool?.symbol || 'LP'}
|
||||||
|
</div>
|
||||||
|
{isTokenDropdownOpen && (
|
||||||
|
<div className="absolute z-50 w-full mt-2 bg-popover border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
||||||
|
{availableTokensForPool.length > 0 ? (
|
||||||
|
availableTokensForPool.map((token) => (
|
||||||
|
<button
|
||||||
|
key={token.address}
|
||||||
|
className="w-full px-4 py-3 text-left hover:bg-accent focus:bg-accent focus:outline-none"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedToken(token);
|
||||||
|
setIsTokenDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="font-medium">{token.symbol}</span>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : selectedPool ? (
|
||||||
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||||||
|
{tokensLoading ? 'Loading tokens...' : 'No tokens available for this pool'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="px-4 py-3 text-sm text-muted-foreground">
|
||||||
|
Select a pool first
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Amount Input */}
|
{/* Amount Input */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-sm">
|
<div className="flex justify-between items-center text-sm">
|
||||||
<label className="text-muted-foreground">{t('stake.amount')}</label>
|
<label className="text-muted-foreground">{t('stake.amount')}</label>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-2 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
if (mode === 'stake' && selectedToken) {
|
||||||
|
setStakeAmount(formatUnits(selectedToken.balance, selectedToken.decimals));
|
||||||
|
} else if (mode === 'unstake' && lpBalance !== null) {
|
||||||
|
setStakeAmount(formatUnits(lpBalance, 18));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedToken || !selectedPool || (mode === 'stake' ? !selectedToken : lpBalance === null)}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -269,8 +460,8 @@ export function StakeForm() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Swap Mint Amounts Display */}
|
{/* Swap Mint Amounts Display (Stake Mode) */}
|
||||||
{swapMintAmounts && selectedToken && !isAmountExceedingBalance && (
|
{mode === 'stake' && swapMintAmounts && selectedToken && !isAmountExceedingBalance && (
|
||||||
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
|
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">{t('stake.amountUsed')}:</span>
|
<span className="text-muted-foreground">{t('stake.amountUsed')}:</span>
|
||||||
@@ -293,17 +484,29 @@ export function StakeForm() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Stake Button */}
|
{/* Burn Swap Amounts Display (Unstake Mode) */}
|
||||||
|
{mode === 'unstake' && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && (
|
||||||
|
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">{t('stake.amountOut')}:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{burnSwapLoading ? 'Calculating...' : `${formatUnits(burnSwapAmounts, selectedToken.decimals)} ${selectedToken.symbol}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stake/Unstake Button */}
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-14 text-lg"
|
className="w-full h-14 text-lg"
|
||||||
onClick={handleStake}
|
onClick={handleStake}
|
||||||
disabled={!isConnected || !stakeAmount || !selectedPool || !selectedToken || isAmountExceedingBalance || isSwapMinting}
|
disabled={!isConnected || !stakeAmount || !selectedPool || !selectedToken || isAmountExceedingBalance || isSwapMinting || isBurnSwapping}
|
||||||
>
|
>
|
||||||
{!isConnected
|
{!isConnected
|
||||||
? t('swap.connectWalletToSwap')
|
? t('swap.connectWalletToSwap')
|
||||||
: isSwapMinting
|
: (isSwapMinting || isBurnSwapping)
|
||||||
? 'Staking...'
|
? mode === 'stake' ? 'Staking...' : 'Unstaking...'
|
||||||
: t('stake.stakeButton')}
|
: mode === 'stake' ? t('stake.stakeButton') : 'Unstake'}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
@@ -314,9 +517,14 @@ export function StakeForm() {
|
|||||||
{transactionStatus === 'pending' && (
|
{transactionStatus === 'pending' && (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<Loader2 className="h-16 w-16 animate-spin text-primary" />
|
<Loader2 className="h-16 w-16 animate-spin text-primary" />
|
||||||
<h3 className="text-xl font-semibold text-center">Approving Stake</h3>
|
<h3 className="text-xl font-semibold text-center">
|
||||||
|
{mode === 'stake' ? 'Approving Stake' : 'Approving Unstake'}
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground text-center">
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
Staking {stakeAmount} {selectedToken?.symbol} to {selectedPool?.symbol}
|
{mode === 'stake'
|
||||||
|
? `Staking ${stakeAmount} ${selectedToken?.symbol} to ${selectedPool?.symbol}`
|
||||||
|
: `Unstaking ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}`
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground text-center">
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
Please confirm the transactions in your wallet
|
Please confirm the transactions in your wallet
|
||||||
@@ -327,9 +535,14 @@ export function StakeForm() {
|
|||||||
{transactionStatus === 'success' && (
|
{transactionStatus === 'success' && (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<CheckCircle className="h-16 w-16 text-green-500" />
|
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||||
<h3 className="text-xl font-semibold text-center">Stake Confirmed!</h3>
|
<h3 className="text-xl font-semibold text-center">
|
||||||
|
{mode === 'stake' ? 'Stake Confirmed!' : 'Unstake Confirmed!'}
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground text-center">
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
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}`
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCloseModal}
|
onClick={handleCloseModal}
|
||||||
@@ -343,7 +556,9 @@ export function StakeForm() {
|
|||||||
{transactionStatus === 'error' && (
|
{transactionStatus === 'error' && (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<XCircle className="h-16 w-16 text-destructive" />
|
<XCircle className="h-16 w-16 text-destructive" />
|
||||||
<h3 className="text-xl font-semibold text-center">Stake Failed</h3>
|
<h3 className="text-xl font-semibold text-center">
|
||||||
|
{mode === 'stake' ? 'Stake Failed' : 'Unstake Failed'}
|
||||||
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground text-center break-words">
|
<p className="text-sm text-muted-foreground text-center break-words">
|
||||||
{transactionError || 'Transaction failed'}
|
{transactionError || 'Transaction failed'}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -187,8 +187,21 @@ export function SwapForm() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* From Token */}
|
{/* From Token */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-sm">
|
<div className="flex justify-between items-center text-sm">
|
||||||
<label className="text-muted-foreground">{t('swap.youPay')}</label>
|
<label className="text-muted-foreground">{t('swap.youPay')}</label>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-2 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedFromToken) {
|
||||||
|
setFromAmount(formatUnits(selectedFromToken.balance, selectedFromToken.decimals));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedFromToken}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -522,4 +522,137 @@ export function useSwapMintAmounts(
|
|||||||
error,
|
error,
|
||||||
isReady: mounted,
|
isReady: mounted,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBurnSwapAmounts(
|
||||||
|
poolAddress: `0x${string}` | undefined,
|
||||||
|
lpAmount: bigint | undefined,
|
||||||
|
inputTokenIndex: number | undefined
|
||||||
|
) {
|
||||||
|
const publicClient = usePublicClient();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [burnSwapAmounts, setBurnSwapAmounts] = useState<bigint | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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<string, { v1: { PartyPlanner: string; PartyPoolViewer: string } }>)[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<bigint | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -462,4 +462,111 @@ export function useSwapMint() {
|
|||||||
swapMintHash,
|
swapMintHash,
|
||||||
swapMintError,
|
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<string | null>(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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"insufficientBalance": "Insufficient balance",
|
"insufficientBalance": "Insufficient balance",
|
||||||
"amountUsed": "Amount Used",
|
"amountUsed": "Amount Used",
|
||||||
"fee": "Fee",
|
"fee": "Fee",
|
||||||
"lpMinted": "LP Minted"
|
"lpMinted": "LP Minted",
|
||||||
|
"amountOut": "Amount Out"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user