adding a stake form and approvals in both stake and swap form
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
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();
|
const { t } = useTranslation();
|
||||||
@@ -20,10 +21,7 @@ export default function HomePage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="stake">
|
<TabsContent value="stake">
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<StakeForm />
|
||||||
<h2 className="text-2xl font-semibold mb-2">{t('stake.title')}</h2>
|
|
||||||
<p>{t('stake.comingSoon')}</p>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
364
src/components/stake-form.tsx
Normal file
364
src/components/stake-form.tsx
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef, 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 { ChevronDown, CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||||
|
import { useAccount } from 'wagmi';
|
||||||
|
import { useGetAllPools, useTokenDetails, useSwapMintAmounts, type PoolDetails, type TokenDetails } from '@/hooks/usePartyPlanner';
|
||||||
|
import { useSwapMint } from '@/hooks/usePartyPool';
|
||||||
|
import { formatUnits, parseUnits } from 'viem';
|
||||||
|
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
|
||||||
|
|
||||||
|
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
|
||||||
|
|
||||||
|
export function StakeForm() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isConnected, address } = useAccount();
|
||||||
|
const [stakeAmount, setStakeAmount] = useState('');
|
||||||
|
const [selectedPool, setSelectedPool] = useState<PoolDetails | null>(null);
|
||||||
|
const [selectedToken, setSelectedToken] = useState<TokenDetails | null>(null);
|
||||||
|
const [isPoolDropdownOpen, setIsPoolDropdownOpen] = useState(false);
|
||||||
|
const [isTokenDropdownOpen, setIsTokenDropdownOpen] = useState(false);
|
||||||
|
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
|
||||||
|
const [transactionError, setTransactionError] = useState<string | null>(null);
|
||||||
|
const poolDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tokenDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Fetch all pools using the new hook
|
||||||
|
const { poolDetails, loading: poolsLoading } = useGetAllPools();
|
||||||
|
|
||||||
|
// Get token details for the user
|
||||||
|
const { tokenDetails, loading: tokensLoading } = useTokenDetails(address);
|
||||||
|
|
||||||
|
// Initialize swap mint hook
|
||||||
|
const { executeSwapMint, isSwapMinting } = useSwapMint();
|
||||||
|
|
||||||
|
// Get available tokens for staking based on selected pool
|
||||||
|
const availableTokensForPool = selectedPool && tokenDetails
|
||||||
|
? tokenDetails.filter(token =>
|
||||||
|
selectedPool.tokens.some(poolToken =>
|
||||||
|
poolToken.toLowerCase() === token.address.toLowerCase()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Check if amount exceeds balance
|
||||||
|
const isAmountExceedingBalance = useMemo(() => {
|
||||||
|
if (!stakeAmount || !selectedToken) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const amountInWei = parseUnits(stakeAmount, selectedToken.decimals);
|
||||||
|
return amountInWei > selectedToken.balance;
|
||||||
|
} catch {
|
||||||
|
// If parseUnits fails (invalid input), don't show error
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [stakeAmount, selectedToken]);
|
||||||
|
|
||||||
|
// Get the input token index in the selected pool
|
||||||
|
const inputTokenIndex = useMemo(() => {
|
||||||
|
if (!selectedPool || !selectedToken) return undefined;
|
||||||
|
|
||||||
|
const index = selectedPool.tokens.findIndex(
|
||||||
|
token => token.toLowerCase() === selectedToken.address.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
return index !== -1 ? index : undefined;
|
||||||
|
}, [selectedPool, selectedToken]);
|
||||||
|
|
||||||
|
// Parse the stake amount to Wei
|
||||||
|
const maxAmountIn = useMemo(() => {
|
||||||
|
if (!stakeAmount || !selectedToken) return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return parseUnits(stakeAmount, selectedToken.decimals);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [stakeAmount, selectedToken]);
|
||||||
|
|
||||||
|
// Fetch swap mint amounts
|
||||||
|
const { swapMintAmounts, loading: swapMintLoading } = useSwapMintAmounts(
|
||||||
|
selectedPool?.address,
|
||||||
|
inputTokenIndex,
|
||||||
|
maxAmountIn
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close dropdowns when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (poolDropdownRef.current && !poolDropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsPoolDropdownOpen(false);
|
||||||
|
}
|
||||||
|
if (tokenDropdownRef.current && !tokenDropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsTokenDropdownOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleStake = async () => {
|
||||||
|
if (!selectedPool || !selectedToken || !stakeAmount || inputTokenIndex === undefined || !maxAmountIn) {
|
||||||
|
console.error('Missing required stake parameters');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransactionStatus('pending');
|
||||||
|
setTransactionError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute the swap mint transaction
|
||||||
|
await executeSwapMint(
|
||||||
|
selectedPool.address,
|
||||||
|
selectedToken.address,
|
||||||
|
inputTokenIndex,
|
||||||
|
maxAmountIn
|
||||||
|
);
|
||||||
|
|
||||||
|
setTransactionStatus('success');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Stake failed:', err);
|
||||||
|
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
|
||||||
|
setTransactionStatus('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
if (transactionStatus === 'success') {
|
||||||
|
// Clear the form after successful stake
|
||||||
|
setStakeAmount('');
|
||||||
|
setSelectedPool(null);
|
||||||
|
setSelectedToken(null);
|
||||||
|
}
|
||||||
|
setTransactionStatus('idle');
|
||||||
|
setTransactionError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-md mx-auto relative">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<CardTitle>Stake</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Pool Selection */}
|
||||||
|
<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);
|
||||||
|
// 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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Token Selection */}
|
||||||
|
<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')}: {selectedToken ? formatUnits(selectedToken.balance, selectedToken.decimals) : '0.00'}
|
||||||
|
</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 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm">
|
||||||
|
<label className="text-muted-foreground">{t('stake.amount')}</label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="0.0"
|
||||||
|
value={stakeAmount}
|
||||||
|
onChange={(e) => setStakeAmount(e.target.value)}
|
||||||
|
className="text-2xl h-16"
|
||||||
|
disabled={!selectedToken || !selectedPool}
|
||||||
|
/>
|
||||||
|
{isAmountExceedingBalance && (
|
||||||
|
<p className="text-sm text-destructive">
|
||||||
|
{t('stake.insufficientBalance')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swap Mint Amounts Display */}
|
||||||
|
{swapMintAmounts && 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.amountUsed')}:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.amountInUsed, selectedToken.decimals)} ${selectedToken.symbol}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">{t('stake.fee')}:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.fee, selectedToken.decimals)} ${selectedToken.symbol}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">{t('stake.lpMinted')}:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{swapMintLoading ? 'Calculating...' : `${formatUnits(swapMintAmounts.lpMinted, 18)} ${selectedPool?.symbol || 'LP'}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stake Button */}
|
||||||
|
<Button
|
||||||
|
className="w-full h-14 text-lg"
|
||||||
|
onClick={handleStake}
|
||||||
|
disabled={!isConnected || !stakeAmount || !selectedPool || !selectedToken || isAmountExceedingBalance || isSwapMinting}
|
||||||
|
>
|
||||||
|
{!isConnected
|
||||||
|
? t('swap.connectWalletToSwap')
|
||||||
|
: isSwapMinting
|
||||||
|
? 'Staking...'
|
||||||
|
: t('stake.stakeButton')}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Transaction Modal Overlay */}
|
||||||
|
{transactionStatus !== 'idle' && (
|
||||||
|
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
|
||||||
|
<div className="bg-card border rounded-lg p-8 max-w-sm w-full mx-4 shadow-lg">
|
||||||
|
{transactionStatus === 'pending' && (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<Loader2 className="h-16 w-16 animate-spin text-primary" />
|
||||||
|
<h3 className="text-xl font-semibold text-center">Approving Stake</h3>
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
Staking {stakeAmount} {selectedToken?.symbol} to {selectedPool?.symbol}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
|
Please confirm the transactions in your wallet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transactionStatus === 'success' && (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||||
|
<h3 className="text-xl font-semibold text-center">Stake Confirmed!</h3>
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
Successfully staked {stakeAmount} {selectedToken?.symbol} to {selectedPool?.symbol}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
className="w-full mt-4"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transactionStatus === 'error' && (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<XCircle className="h-16 w-16 text-destructive" />
|
||||||
|
<h3 className="text-xl font-semibold text-center">Stake Failed</h3>
|
||||||
|
<p className="text-sm text-muted-foreground text-center break-words">
|
||||||
|
{transactionError || 'Transaction failed'}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-4"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,18 +5,18 @@ 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 { ArrowDownUp, ChevronDown, Settings } from 'lucide-react';
|
import { ArrowDownUp, ChevronDown, Settings, CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||||
import { useAccount } from 'wagmi';
|
import { useAccount } from 'wagmi';
|
||||||
import { useTokenDetails, useGetPoolsByToken, type TokenDetails } from '@/hooks/usePartyPlanner';
|
import { useTokenDetails, useGetPoolsByToken, type TokenDetails } from '@/hooks/usePartyPlanner';
|
||||||
import { useSwapAmounts, useSwap, selectBestSwapRoute } from '@/hooks/usePartyPool';
|
import { useSwapAmounts, useSwap, selectBestSwapRoute } from '@/hooks/usePartyPool';
|
||||||
import { formatUnits, parseUnits } from 'viem';
|
import { formatUnits, parseUnits } from 'viem';
|
||||||
import { SwapReviewModal } from './swap-review-modal';
|
import { SwapReviewModal } from './swap-review-modal';
|
||||||
import { useToast } from '@/components/ui/toast';
|
|
||||||
|
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
|
||||||
|
|
||||||
export function SwapForm() {
|
export function SwapForm() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isConnected, address } = useAccount();
|
const { isConnected, address } = useAccount();
|
||||||
const { addToast, updateToast } = useToast();
|
|
||||||
const [fromAmount, setFromAmount] = useState('');
|
const [fromAmount, setFromAmount] = useState('');
|
||||||
const [toAmount, setToAmount] = useState('');
|
const [toAmount, setToAmount] = useState('');
|
||||||
const [selectedFromToken, setSelectedFromToken] = useState<TokenDetails | null>(null);
|
const [selectedFromToken, setSelectedFromToken] = useState<TokenDetails | null>(null);
|
||||||
@@ -26,6 +26,8 @@ export function SwapForm() {
|
|||||||
const [maxSlippage, setMaxSlippage] = useState<string>('5.5'); // Default 5.5%
|
const [maxSlippage, setMaxSlippage] = useState<string>('5.5'); // Default 5.5%
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false);
|
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false);
|
||||||
|
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
|
||||||
|
const [transactionError, setTransactionError] = useState<string | null>(null);
|
||||||
const fromDropdownRef = useRef<HTMLDivElement>(null);
|
const fromDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const toDropdownRef = useRef<HTMLDivElement>(null);
|
const toDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -95,12 +97,8 @@ export function SwapForm() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show swapping toast
|
setTransactionStatus('pending');
|
||||||
const toastId = addToast({
|
setTransactionError(null);
|
||||||
title: 'Swapping',
|
|
||||||
description: `${fromAmount} ${selectedFromToken.symbol} → ${toAmount} ${selectedToToken.symbol}`,
|
|
||||||
type: 'loading',
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the shared helper to select the best swap route
|
// Use the shared helper to select the best swap route
|
||||||
@@ -108,11 +106,8 @@ export function SwapForm() {
|
|||||||
|
|
||||||
if (!bestRoute) {
|
if (!bestRoute) {
|
||||||
console.error('No valid swap route found');
|
console.error('No valid swap route found');
|
||||||
updateToast(toastId, {
|
setTransactionError('No valid swap route found');
|
||||||
title: 'Swap Failed',
|
setTransactionStatus('error');
|
||||||
description: 'No valid swap route found',
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,26 +124,24 @@ export function SwapForm() {
|
|||||||
currentSlippage
|
currentSlippage
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update toast to success
|
setTransactionStatus('success');
|
||||||
updateToast(toastId, {
|
} catch (err) {
|
||||||
title: 'Swap Confirmed',
|
console.error('Swap failed:', err);
|
||||||
description: `Successfully swapped ${fromAmount} ${selectedFromToken.symbol} to ${toAmount} ${selectedToToken.symbol}`,
|
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
|
||||||
type: 'success',
|
setTransactionStatus('error');
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
if (transactionStatus === 'success') {
|
||||||
// Clear the form after successful swap
|
// Clear the form after successful swap
|
||||||
setFromAmount('');
|
setFromAmount('');
|
||||||
setToAmount('');
|
setToAmount('');
|
||||||
setSelectedFromToken(null);
|
setSelectedFromToken(null);
|
||||||
setSelectedToToken(null);
|
setSelectedToToken(null);
|
||||||
} catch (err) {
|
|
||||||
console.error('Swap failed:', err);
|
|
||||||
updateToast(toastId, {
|
|
||||||
title: 'Swap Failed',
|
|
||||||
description: err instanceof Error ? err.message : 'Transaction failed',
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
setTransactionStatus('idle');
|
||||||
|
setTransactionError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchTokens = () => {
|
const switchTokens = () => {
|
||||||
@@ -177,7 +170,7 @@ export function SwapForm() {
|
|||||||
}, [swapAmounts, selectedFromToken, selectedToToken, fromAmount, currentSlippage, isConnected]);
|
}, [swapAmounts, selectedFromToken, selectedToToken, fromAmount, currentSlippage, isConnected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md mx-auto">
|
<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>Swap</CardTitle>
|
<CardTitle>Swap</CardTitle>
|
||||||
@@ -428,6 +421,59 @@ export function SwapForm() {
|
|||||||
}}
|
}}
|
||||||
isSwapping={isSwapping}
|
isSwapping={isSwapping}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Transaction Modal Overlay */}
|
||||||
|
{transactionStatus !== 'idle' && (
|
||||||
|
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
|
||||||
|
<div className="bg-card border rounded-lg p-8 max-w-sm w-full mx-4 shadow-lg">
|
||||||
|
{transactionStatus === 'pending' && (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<Loader2 className="h-16 w-16 animate-spin text-primary" />
|
||||||
|
<h3 className="text-xl font-semibold text-center">Approving Swap</h3>
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
Swapping {fromAmount} {selectedFromToken?.symbol} → {toAmount} {selectedToToken?.symbol}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
|
Please confirm the transactions in your wallet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transactionStatus === 'success' && (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||||
|
<h3 className="text-xl font-semibold text-center">Swap Confirmed!</h3>
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
Successfully swapped {fromAmount} {selectedFromToken?.symbol} to {toAmount} {selectedToToken?.symbol}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
className="w-full mt-4"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transactionStatus === 'error' && (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<XCircle className="h-16 w-16 text-destructive" />
|
||||||
|
<h3 className="text-xl font-semibold text-center">Swap Failed</h3>
|
||||||
|
<p className="text-sm text-muted-foreground text-center break-words">
|
||||||
|
{transactionError || 'Transaction failed'}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-4"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { usePublicClient } from 'wagmi';
|
|||||||
import chainInfo from '@/contracts/liqp-deployments.json';
|
import chainInfo from '@/contracts/liqp-deployments.json';
|
||||||
import IPartyPlannerABI from '@/contracts/IPartyPlannerABI';
|
import IPartyPlannerABI from '@/contracts/IPartyPlannerABI';
|
||||||
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
|
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
|
||||||
|
import IPartyPoolViewerABI from '@/contracts/IPartyPoolViewerABI';
|
||||||
import { ERC20ABI } from '@/contracts/ERC20ABI';
|
import { ERC20ABI } from '@/contracts/ERC20ABI';
|
||||||
|
|
||||||
export function useGetAllTokens(offset: number = 0, limit: number = 100) {
|
export function useGetAllTokens(offset: number = 0, limit: number = 100) {
|
||||||
@@ -324,4 +325,201 @@ export function useTokenDetails(userAddress: `0x${string}` | undefined) {
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PoolDetails {
|
||||||
|
address: `0x${string}`;
|
||||||
|
name: string;
|
||||||
|
symbol: string;
|
||||||
|
tokens: readonly `0x${string}`[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetAllPools(offset: number = 0, limit: number = 100) {
|
||||||
|
const publicClient = usePublicClient();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [pools, setPools] = useState<readonly `0x${string}`[] | null>(null);
|
||||||
|
const [poolDetails, setPoolDetails] = useState<PoolDetails[] | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Handle hydration for Next.js static export
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
const fetchPools = async () => {
|
||||||
|
if (!publicClient) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Get chain ID and contract address
|
||||||
|
const chainId = await publicClient.getChainId();
|
||||||
|
const address = (chainInfo as Record<string, { v1: { PartyPlanner: string; PartyPoolViewer: string } }>)[chainId.toString()]?.v1?.PartyPlanner;
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
setError('IPartyPlanner contract not found for current chain');
|
||||||
|
setPools([]);
|
||||||
|
setPoolDetails([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call getAllPools function
|
||||||
|
const result = await publicClient.readContract({
|
||||||
|
address: address as `0x${string}`,
|
||||||
|
abi: IPartyPlannerABI,
|
||||||
|
functionName: 'getAllPools',
|
||||||
|
args: [BigInt(offset), BigInt(limit)],
|
||||||
|
});
|
||||||
|
|
||||||
|
setPools(result);
|
||||||
|
|
||||||
|
// Fetch details for each pool
|
||||||
|
const details: PoolDetails[] = [];
|
||||||
|
for (const poolAddress of result) {
|
||||||
|
try {
|
||||||
|
const [name, symbol, tokens] = await Promise.all([
|
||||||
|
publicClient.readContract({
|
||||||
|
address: poolAddress,
|
||||||
|
abi: ERC20ABI,
|
||||||
|
functionName: 'name',
|
||||||
|
}).catch(() => 'Unknown Pool'),
|
||||||
|
publicClient.readContract({
|
||||||
|
address: poolAddress,
|
||||||
|
abi: ERC20ABI,
|
||||||
|
functionName: 'symbol',
|
||||||
|
}).catch(() => 'POOL'),
|
||||||
|
publicClient.readContract({
|
||||||
|
address: poolAddress,
|
||||||
|
abi: IPartyPoolABI,
|
||||||
|
functionName: 'allTokens',
|
||||||
|
}).catch(() => [] as readonly `0x${string}`[]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
details.push({
|
||||||
|
address: poolAddress,
|
||||||
|
name: name as string,
|
||||||
|
symbol: symbol as string,
|
||||||
|
tokens: tokens as readonly `0x${string}`[],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching pool details for', poolAddress, err);
|
||||||
|
// Add pool with fallback values
|
||||||
|
details.push({
|
||||||
|
address: poolAddress,
|
||||||
|
name: 'Unknown Pool',
|
||||||
|
symbol: 'POOL',
|
||||||
|
tokens: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPoolDetails(details);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error calling getAllPools:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch pools');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPools();
|
||||||
|
}, [publicClient, mounted, offset, limit]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pools,
|
||||||
|
poolDetails,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
isReady: mounted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwapMintAmounts {
|
||||||
|
amountInUsed: bigint;
|
||||||
|
fee: bigint;
|
||||||
|
lpMinted: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSwapMintAmounts(
|
||||||
|
poolAddress: `0x${string}` | undefined,
|
||||||
|
inputTokenIndex: number | undefined,
|
||||||
|
maxAmountIn: bigint | undefined
|
||||||
|
) {
|
||||||
|
const publicClient = usePublicClient();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [swapMintAmounts, setSwapMintAmounts] = useState<SwapMintAmounts | 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 || inputTokenIndex === undefined || !maxAmountIn || maxAmountIn === BigInt(0)) {
|
||||||
|
setLoading(false);
|
||||||
|
setSwapMintAmounts(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSwapMintAmounts = 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');
|
||||||
|
setSwapMintAmounts(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call swapMintAmounts function
|
||||||
|
const result = await publicClient.readContract({
|
||||||
|
address: viewerAddress as `0x${string}`,
|
||||||
|
abi: IPartyPoolViewerABI,
|
||||||
|
functionName: 'swapMintAmounts',
|
||||||
|
args: [poolAddress, BigInt(inputTokenIndex), maxAmountIn],
|
||||||
|
}) as readonly [bigint, bigint, bigint];
|
||||||
|
|
||||||
|
setSwapMintAmounts({
|
||||||
|
amountInUsed: result[0],
|
||||||
|
fee: result[1],
|
||||||
|
lpMinted: result[2],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error calling swapMintAmounts:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch swap mint amounts');
|
||||||
|
setSwapMintAmounts(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSwapMintAmounts();
|
||||||
|
}, [publicClient, mounted, poolAddress, inputTokenIndex, maxAmountIn]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
swapMintAmounts,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
isReady: mounted,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -357,4 +357,109 @@ export function useSwap() {
|
|||||||
gasEstimate,
|
gasEstimate,
|
||||||
isEstimatingGas,
|
isEstimatingGas,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSwapMint() {
|
||||||
|
const publicClient = usePublicClient();
|
||||||
|
const { data: walletClient } = useWalletClient();
|
||||||
|
const [isSwapMinting, setIsSwapMinting] = useState(false);
|
||||||
|
const [swapMintHash, setSwapMintHash] = useState<`0x${string}` | null>(null);
|
||||||
|
const [swapMintError, setSwapMintError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const executeSwapMint = async (
|
||||||
|
poolAddress: `0x${string}`,
|
||||||
|
inputTokenAddress: `0x${string}`,
|
||||||
|
inputTokenIndex: number,
|
||||||
|
maxAmountIn: bigint
|
||||||
|
) => {
|
||||||
|
if (!walletClient || !publicClient) {
|
||||||
|
setSwapMintError('Wallet not connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSwapMinting(true);
|
||||||
|
setSwapMintError(null);
|
||||||
|
setSwapMintHash(null);
|
||||||
|
|
||||||
|
const userAddress = walletClient.account.address;
|
||||||
|
|
||||||
|
// STEP 1: Approve the pool to spend the input token
|
||||||
|
console.log('🔐 Approving token spend for swap mint...');
|
||||||
|
console.log('Token to approve:', inputTokenAddress);
|
||||||
|
console.log('Spender (pool):', poolAddress);
|
||||||
|
console.log('Amount:', maxAmountIn.toString());
|
||||||
|
|
||||||
|
const approvalHash = await walletClient.writeContract({
|
||||||
|
address: inputTokenAddress,
|
||||||
|
abi: [
|
||||||
|
{
|
||||||
|
name: 'approve',
|
||||||
|
type: 'function',
|
||||||
|
stateMutability: 'nonpayable',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'spender', type: 'address' },
|
||||||
|
{ name: 'amount', type: 'uint256' }
|
||||||
|
],
|
||||||
|
outputs: [{ name: '', type: 'bool' }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
functionName: 'approve',
|
||||||
|
args: [poolAddress, maxAmountIn],
|
||||||
|
});
|
||||||
|
|
||||||
|
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 swapMint with params:', {
|
||||||
|
pool: poolAddress,
|
||||||
|
payer: userAddress,
|
||||||
|
receiver: userAddress,
|
||||||
|
inputTokenIndex,
|
||||||
|
maxAmountIn: maxAmountIn.toString(),
|
||||||
|
deadline: deadline.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// STEP 3: Execute the swapMint transaction
|
||||||
|
const hash = await walletClient.writeContract({
|
||||||
|
address: poolAddress,
|
||||||
|
abi: IPartyPoolABI,
|
||||||
|
functionName: 'swapMint',
|
||||||
|
args: [
|
||||||
|
userAddress, // payer
|
||||||
|
userAddress, // receiver
|
||||||
|
BigInt(inputTokenIndex),
|
||||||
|
maxAmountIn,
|
||||||
|
deadline,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
setSwapMintHash(hash);
|
||||||
|
console.log('✅ SwapMint transaction submitted:', hash);
|
||||||
|
|
||||||
|
// Wait for transaction confirmation
|
||||||
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
||||||
|
console.log('✅ SwapMint transaction confirmed:', receipt);
|
||||||
|
|
||||||
|
return receipt;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'SwapMint failed';
|
||||||
|
setSwapMintError(errorMessage);
|
||||||
|
console.error('❌ SwapMint error:', err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setIsSwapMinting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
executeSwapMint,
|
||||||
|
isSwapMinting,
|
||||||
|
swapMintHash,
|
||||||
|
swapMintError,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,14 @@
|
|||||||
},
|
},
|
||||||
"stake": {
|
"stake": {
|
||||||
"title": "Stake",
|
"title": "Stake",
|
||||||
"comingSoon": "Coming soon..."
|
"comingSoon": "Coming soon...",
|
||||||
|
"selectPool": "Select Pool",
|
||||||
|
"selectToken": "Select Token",
|
||||||
|
"amount": "Amount",
|
||||||
|
"stakeButton": "Stake",
|
||||||
|
"insufficientBalance": "Insufficient balance",
|
||||||
|
"amountUsed": "Amount Used",
|
||||||
|
"fee": "Fee",
|
||||||
|
"lpMinted": "LP Minted"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user