moving redeem all functionality to stake form. Getting rid of unstake basket tab

This commit is contained in:
2025-11-11 14:41:52 -04:00
parent 9795d03493
commit 6e5eca7543
4 changed files with 213 additions and 430 deletions

View File

@@ -6,11 +6,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { ChevronDown, CheckCircle, XCircle, Loader2, ArrowDownUp } from 'lucide-react';
import { useAccount } from 'wagmi';
import { useAccount, usePublicClient } from 'wagmi';
import { useGetAllPools, useTokenDetails, useSwapMintAmounts, useBurnSwapAmounts, useLPTokenBalance, type PoolDetails, type TokenDetails, type BurnSwapAmounts } from '@/hooks/usePartyPlanner';
import { useSwapMint, useBurnSwap, type ActualSwapMintAmounts, type ActualBurnSwapAmounts } from '@/hooks/usePartyPool';
import { useSwapMint, useBurnSwap, useBurn, type ActualSwapMintAmounts, type ActualBurnSwapAmounts, type ActualBurnAmounts } from '@/hooks/usePartyPool';
import { formatUnits, parseUnits } from 'viem';
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
import { ERC20ABI } from '@/contracts/ERC20ABI';
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
type Mode = 'stake' | 'unstake';
@@ -19,9 +20,16 @@ interface StakeFormProps {
defaultMode?: Mode;
}
interface TokenInfo {
address: `0x${string}`;
symbol: string;
decimals: number;
}
export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
const { t } = useTranslation();
const { isConnected, address } = useAccount();
const publicClient = usePublicClient();
const [mode, setMode] = useState<Mode>(defaultMode);
const [stakeAmount, setStakeAmount] = useState('');
const [selectedPool, setSelectedPool] = useState<PoolDetails | null>(null);
@@ -32,6 +40,9 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
const [transactionError, setTransactionError] = useState<string | null>(null);
const [actualSwapMintAmounts, setActualSwapMintAmounts] = useState<ActualSwapMintAmounts | null>(null);
const [actualBurnSwapAmounts, setActualBurnSwapAmounts] = useState<ActualBurnSwapAmounts | null>(null);
const [actualBurnAmounts, setActualBurnAmounts] = useState<ActualBurnAmounts | null>(null);
const [redeemAll, setRedeemAll] = useState(false);
const [poolTokens, setPoolTokens] = useState<TokenInfo[]>([]);
const poolDropdownRef = useRef<HTMLDivElement>(null);
const tokenDropdownRef = useRef<HTMLDivElement>(null);
@@ -44,6 +55,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
// Initialize swap mint and burn swap hooks
const { executeSwapMint, isSwapMinting } = useSwapMint();
const { executeBurnSwap, isBurnSwapping } = useBurnSwap();
const { executeBurn, isBurning } = useBurn();
// Fetch LP token balance (for unstake mode) - must be before isAmountExceedingBalance
const { lpBalance } = useLPTokenBalance(
@@ -103,12 +115,15 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
// Parse the stake amount to Wei
const maxAmountIn = useMemo(() => {
if (!stakeAmount || !selectedToken) return undefined;
if (!stakeAmount) return undefined;
try {
// For unstake mode, LP tokens always have 18 decimals
const decimals = mode === 'unstake' ? 18 : selectedToken.decimals;
return parseUnits(stakeAmount, decimals);
if (mode === 'unstake') {
return parseUnits(stakeAmount, 18);
}
if (!selectedToken) return undefined;
return parseUnits(stakeAmount, selectedToken.decimals);
} catch {
return undefined;
}
@@ -121,13 +136,54 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
mode === 'stake' ? maxAmountIn : undefined
);
// Fetch burn swap amounts (for unstake mode)
// Fetch burn swap amounts (for unstake mode, only when not redeeming all)
const { burnSwapAmounts, loading: burnSwapLoading } = useBurnSwapAmounts(
mode === 'unstake' ? selectedPool?.address : undefined,
mode === 'unstake' ? maxAmountIn : undefined,
mode === 'unstake' ? inputTokenIndex : undefined
mode === 'unstake' && !redeemAll ? selectedPool?.address : undefined,
mode === 'unstake' && !redeemAll ? maxAmountIn : undefined,
mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined
);
// Fetch token details for the selected pool when Redeem All is active
useEffect(() => {
if (!publicClient || !selectedPool || mode !== 'unstake' || !redeemAll) {
setPoolTokens([]);
return;
}
const fetchTokenDetails = async () => {
const tokenInfos: TokenInfo[] = [];
for (const tokenAddress of selectedPool.tokens) {
try {
const [symbol, decimals] = await Promise.all([
publicClient.readContract({
address: tokenAddress as `0x${string}`,
abi: ERC20ABI,
functionName: 'symbol',
}) as Promise<string>,
publicClient.readContract({
address: tokenAddress as `0x${string}`,
abi: ERC20ABI,
functionName: 'decimals',
}) as Promise<number>,
]);
tokenInfos.push({
address: tokenAddress as `0x${string}`,
symbol,
decimals,
});
} catch (err) {
console.error(`Error fetching token details for ${tokenAddress}:`, err);
}
}
setPoolTokens(tokenInfos);
};
fetchTokenDetails();
}, [publicClient, selectedPool, mode, redeemAll]);
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -144,16 +200,23 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
}, []);
const handleStake = async () => {
if (!selectedPool || !selectedToken || !stakeAmount || inputTokenIndex === undefined || !maxAmountIn) {
console.error('Missing required stake parameters');
if (!selectedPool || !stakeAmount || !maxAmountIn) {
console.error('Missing required parameters');
return;
}
if (mode === 'unstake' && !redeemAll) {
if (!selectedToken || inputTokenIndex === undefined) {
console.error('Missing required unstake parameters');
return;
}
}
setTransactionStatus('pending');
setTransactionError(null);
try {
if (mode === 'stake') {
if (!selectedToken || inputTokenIndex === undefined) throw new Error('Missing stake parameters');
// Execute the swap mint transaction and capture actual amounts
const result = await executeSwapMint(
selectedPool.address,
@@ -166,8 +229,19 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
if (result?.actualSwapMintAmounts) {
setActualSwapMintAmounts(result.actualSwapMintAmounts);
}
} else if (redeemAll) {
// Execute full burn (redeem all tokens from the pool)
const result = await executeBurn(
selectedPool.address,
maxAmountIn,
false // unwrap = false by default
);
if (result?.actualBurnAmounts) {
setActualBurnAmounts(result.actualBurnAmounts);
}
} else {
// Execute the burn swap transaction and capture actual amounts
if (inputTokenIndex === undefined) throw new Error('Missing input token index');
const result = await executeBurnSwap(
selectedPool.address,
maxAmountIn,
@@ -183,7 +257,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
setTransactionStatus('success');
} catch (err) {
console.error(`${mode === 'stake' ? 'Stake' : 'Unstake'} failed:`, err);
console.error(`${mode === 'stake' ? 'Stake' : redeemAll ? 'Redeem' : 'Unstake'} failed:`, err);
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
setTransactionStatus('error');
}
@@ -195,6 +269,10 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
setStakeAmount('');
setSelectedPool(null);
setSelectedToken(null);
setRedeemAll(false);
setActualBurnAmounts(null);
setActualBurnSwapAmounts(null);
setActualSwapMintAmounts(null);
}
setTransactionStatus('idle');
setTransactionError(null);
@@ -206,6 +284,10 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
setStakeAmount('');
setSelectedPool(null);
setSelectedToken(null);
setRedeemAll(false);
setActualBurnAmounts(null);
setActualBurnSwapAmounts(null);
setActualSwapMintAmounts(null);
};
return (
@@ -395,9 +477,11 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
variant="secondary"
className="w-full h-16 justify-between"
onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
disabled={!selectedPool}
disabled={!selectedPool || redeemAll}
>
{selectedToken ? (
{redeemAll ? (
<span className="font-medium">All Tokens (Redeem All)</span>
) : selectedToken ? (
<span className="font-medium">{selectedToken.symbol}</span>
) : (
t('stake.selectToken')
@@ -407,7 +491,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
<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 && (
{!redeemAll && 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) => (
@@ -441,21 +525,40 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
<div className="space-y-2">
<div className="flex justify-between items-center text-sm">
<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 className="flex gap-2">
{mode === 'unstake' && (
<Button
variant={redeemAll ? 'default' : 'outline'}
size="sm"
className="h-6 px-2 text-xs"
onClick={() => {
setRedeemAll(!redeemAll);
if (!redeemAll && lpBalance !== null) {
setStakeAmount(formatUnits(lpBalance, 18));
}
}}
disabled={!selectedPool}
title="Burn entire LP token and receive all underlying tokens"
>
{redeemAll ? 'Redeem All: ON' : 'Redeem All'}
</Button>
)}
<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={!selectedPool || (mode === 'stake' ? !selectedToken : lpBalance === null)}
>
MAX
</Button>
</div>
</div>
<Input
type="number"
@@ -463,7 +566,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
className="text-2xl h-16"
disabled={!selectedToken || !selectedPool}
disabled={!selectedPool || (mode === 'stake' && !selectedToken)}
/>
{isAmountExceedingBalance && (
<p className="text-sm text-destructive">
@@ -497,7 +600,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
)}
{/* Burn Swap Amounts Display (Unstake Mode) */}
{mode === 'unstake' && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && (
{mode === 'unstake' && !redeemAll && burnSwapAmounts && selectedToken && !isAmountExceedingBalance && (
<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>
@@ -514,17 +617,48 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
</div>
)}
{/* Redeem All Tokens Display */}
{mode === 'unstake' && redeemAll && poolTokens.length > 0 && (
<div className="px-4 py-3 bg-muted/30 rounded-lg space-y-2">
<div className="text-sm font-medium mb-2">You will receive:</div>
<div className="space-y-1">
{poolTokens.map((token) => (
<div key={token.address} className="text-sm flex justify-between">
<span className="text-muted-foreground">{token.symbol}</span>
<span className="text-xs text-muted-foreground">
(proportional to pool composition)
</span>
</div>
))}
</div>
</div>
)}
{/* Stake/Unstake Button */}
<Button
className="w-full h-14 text-lg"
onClick={handleStake}
disabled={!isConnected || !stakeAmount || !selectedPool || !selectedToken || isAmountExceedingBalance || isSwapMinting || isBurnSwapping}
disabled={
!isConnected ||
!stakeAmount ||
!selectedPool ||
isAmountExceedingBalance ||
(mode === 'stake'
? (!selectedToken || isSwapMinting)
: (redeemAll
? isBurning
: (!selectedToken || inputTokenIndex === undefined || isBurnSwapping)))
}
>
{!isConnected
? t('swap.connectWalletToSwap')
: (isSwapMinting || isBurnSwapping)
? mode === 'stake' ? 'Staking...' : 'Unstaking...'
: mode === 'stake' ? t('stake.stakeButton') : 'Unstake'}
: (mode === 'stake' && isSwapMinting)
? 'Staking...'
: (mode === 'unstake' && (isBurnSwapping || isBurning))
? (redeemAll ? 'Redeeming...' : 'Unstaking...')
: mode === 'stake'
? t('stake.stakeButton')
: (redeemAll ? 'Redeem All' : 'Unstake')}
</Button>
</CardContent>
@@ -536,12 +670,14 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
<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">
{mode === 'stake' ? 'Approving Stake' : 'Approving Unstake'}
{mode === 'stake' ? 'Approving Stake' : redeemAll ? 'Approving Redeem All' : 'Approving Unstake'}
</h3>
<p className="text-sm text-muted-foreground text-center">
{mode === 'stake'
? `Staking ${stakeAmount} ${selectedToken?.symbol} to ${selectedPool?.symbol}`
: `Unstaking ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}`
: redeemAll
? `Redeeming ${stakeAmount} ${selectedPool?.symbol} LP for all pool tokens`
: `Unstaking ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}`
}
</p>
<p className="text-xs text-muted-foreground text-center">
@@ -554,7 +690,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
<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">
{mode === 'stake' ? 'Stake Confirmed!' : 'Unstake Confirmed!'}
{mode === 'stake' ? 'Stake Confirmed!' : redeemAll ? 'Redeem Confirmed!' : 'Unstake Confirmed!'}
</h3>
{/* Display actual amounts or estimates */}
@@ -594,6 +730,44 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
</>
)}
</div>
) : redeemAll ? (
// Redeem All mode success message
<div className="w-full space-y-3">
{actualBurnAmounts && selectedPool ? (
// Show actual amounts from transaction
<>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">LP Burned:</span>
<span className="font-medium">{formatUnits(actualBurnAmounts.lpBurned, 18)} {selectedPool.symbol}</span>
</div>
<div className="text-sm font-medium mt-3 mb-2">Tokens Received:</div>
{actualBurnAmounts.withdrawAmounts.map((amount, index) => {
const token = poolTokens[index];
if (!token) return null;
return (
<div key={index} className="flex justify-between text-sm pl-2">
<span className="text-muted-foreground">{token.symbol}:</span>
<span className="font-medium">
{formatUnits(amount, token.decimals)}
</span>
</div>
);
})}
</>
) : (
// Fallback to estimates
<>
<p className="text-sm text-muted-foreground text-center">
Successfully redeemed {stakeAmount} {selectedPool?.symbol} LP for all pool tokens
<br />
<span className="text-xs italic opacity-70">
*Disclaimer: This is an estimate from the protocol. The actual amounts might be slightly different due to slippage.
</span>
</p>
</>
)}
</div>
) : (
// Unstake mode success message
<div className="w-full space-y-3">