Compare commits
3 Commits
4ecda7de14
...
0586a09161
| Author | SHA1 | Date | |
|---|---|---|---|
| 0586a09161 | |||
| 6e5eca7543 | |||
| 9795d03493 |
@@ -34,7 +34,7 @@ generate() {
|
||||
|
||||
generate IPartyPlanner
|
||||
generate IPartyPool
|
||||
generate IPartyPoolViewer
|
||||
generate IPartyInfo
|
||||
|
||||
cp "$METADATA_PATH" src/contracts/
|
||||
echo liqp-deployments.json
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { UnstakeBasketForm } from '@/components/unstake-basket-form';
|
||||
|
||||
export default function UnstakeBasketPage() {
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
<UnstakeBasketForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +38,6 @@ export function Header() {
|
||||
{ href: '/', label: 'Swap' },
|
||||
{ href: '/stake', label: 'Stake' },
|
||||
{ href: '/unstake', label: 'Unstake' },
|
||||
{ href: '/unstake-basket', label: 'Unstake Basket' },
|
||||
{ href: '/about', label: 'About' },
|
||||
];
|
||||
|
||||
|
||||
@@ -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,6 +525,24 @@ 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>
|
||||
<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"
|
||||
@@ -452,18 +554,19 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
|
||||
setStakeAmount(formatUnits(lpBalance, 18));
|
||||
}
|
||||
}}
|
||||
disabled={!selectedToken || !selectedPool || (mode === 'stake' ? !selectedToken : lpBalance === null)}
|
||||
disabled={!selectedPool || (mode === 'stake' ? !selectedToken : lpBalance === null)}
|
||||
>
|
||||
MAX
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0.0"
|
||||
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,11 +670,13 @@ 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}`
|
||||
: redeemAll
|
||||
? `Redeeming ${stakeAmount} ${selectedPool?.symbol} LP for all pool tokens`
|
||||
: `Unstaking ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}`
|
||||
}
|
||||
</p>
|
||||
@@ -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">
|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle, XCircle, Loader2, Trash2 } from 'lucide-react';
|
||||
import { useAccount, usePublicClient } from 'wagmi';
|
||||
import { useGetAllPools, useLPTokenBalance, type PoolDetails } from '@/hooks/usePartyPlanner';
|
||||
import { useBurn, type ActualBurnAmounts } from '@/hooks/usePartyPool';
|
||||
import { formatUnits, parseUnits } from 'viem';
|
||||
import IPartyPoolABI from '@/contracts/IPartyPoolABI';
|
||||
import { ERC20ABI } from '@/contracts/ERC20ABI';
|
||||
|
||||
type TransactionStatus = 'idle' | 'pending' | 'success' | 'error';
|
||||
|
||||
interface PoolWithBalance extends PoolDetails {
|
||||
lpBalance: bigint;
|
||||
}
|
||||
|
||||
interface TokenInfo {
|
||||
address: `0x${string}`;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export function UnstakeBasketForm() {
|
||||
const { t } = useTranslation();
|
||||
const { isConnected, address } = useAccount();
|
||||
const publicClient = usePublicClient();
|
||||
const [selectedPools, setSelectedPools] = useState<Set<string>>(new Set());
|
||||
const [poolsWithBalances, setPoolsWithBalances] = useState<PoolWithBalance[]>([]);
|
||||
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
|
||||
const [transactionError, setTransactionError] = useState<string | null>(null);
|
||||
const [actualBurnResults, setActualBurnResults] = useState<{[key: string]: ActualBurnAmounts}>({});
|
||||
const [poolTokens, setPoolTokens] = useState<{[key: string]: TokenInfo[]}>({});
|
||||
|
||||
// Fetch all pools using the hook
|
||||
const { poolDetails, loading: poolsLoading } = useGetAllPools();
|
||||
|
||||
// Initialize burn hook
|
||||
const { executeBurn, isBurning } = useBurn();
|
||||
|
||||
// Fetch LP balances for all pools
|
||||
useEffect(() => {
|
||||
if (!poolDetails || !address || !publicClient) return;
|
||||
|
||||
const fetchBalances = async () => {
|
||||
const poolsWithBal: PoolWithBalance[] = [];
|
||||
|
||||
for (const pool of poolDetails) {
|
||||
try {
|
||||
const balance = await publicClient.readContract({
|
||||
address: pool.address,
|
||||
abi: IPartyPoolABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [address],
|
||||
}) as bigint;
|
||||
|
||||
if (balance > 0n) {
|
||||
poolsWithBal.push({
|
||||
...pool,
|
||||
lpBalance: balance,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error fetching balance for pool ${pool.address}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
setPoolsWithBalances(poolsWithBal);
|
||||
};
|
||||
|
||||
fetchBalances();
|
||||
}, [poolDetails, address, publicClient]);
|
||||
|
||||
// Fetch token details for selected pools
|
||||
useEffect(() => {
|
||||
if (!publicClient || selectedPools.size === 0) return;
|
||||
|
||||
const fetchTokenDetails = async () => {
|
||||
const tokenInfoMap: {[key: string]: TokenInfo[]} = {};
|
||||
|
||||
for (const poolAddress of Array.from(selectedPools)) {
|
||||
const pool = poolsWithBalances.find(p => p.address === poolAddress);
|
||||
if (!pool) continue;
|
||||
|
||||
const tokenInfos: TokenInfo[] = [];
|
||||
|
||||
for (const tokenAddress of pool.tokens) {
|
||||
try {
|
||||
const [symbol, decimals] = await Promise.all([
|
||||
publicClient.readContract({
|
||||
address: tokenAddress as `0x${string}`,
|
||||
abi: ERC20ABI,
|
||||
functionName: 'symbol',
|
||||
}) as Promise<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);
|
||||
}
|
||||
}
|
||||
|
||||
tokenInfoMap[poolAddress] = tokenInfos;
|
||||
}
|
||||
|
||||
setPoolTokens(tokenInfoMap);
|
||||
};
|
||||
|
||||
fetchTokenDetails();
|
||||
}, [publicClient, selectedPools, poolsWithBalances]);
|
||||
|
||||
const togglePoolSelection = (poolAddress: string) => {
|
||||
const newSelected = new Set(selectedPools);
|
||||
if (newSelected.has(poolAddress)) {
|
||||
newSelected.delete(poolAddress);
|
||||
} else {
|
||||
newSelected.add(poolAddress);
|
||||
}
|
||||
setSelectedPools(newSelected);
|
||||
};
|
||||
|
||||
const handleBurnAll = async () => {
|
||||
if (selectedPools.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTransactionStatus('pending');
|
||||
setTransactionError(null);
|
||||
setActualBurnResults({});
|
||||
|
||||
try {
|
||||
const results: {[key: string]: ActualBurnAmounts} = {};
|
||||
|
||||
// Execute burn for each selected pool
|
||||
for (const poolAddress of Array.from(selectedPools)) {
|
||||
const pool = poolsWithBalances.find(p => p.address === poolAddress);
|
||||
if (!pool) continue;
|
||||
|
||||
console.log(`Burning LP tokens for pool ${pool.symbol}...`);
|
||||
|
||||
// Execute the burn transaction
|
||||
const result = await executeBurn(
|
||||
pool.address,
|
||||
pool.lpBalance,
|
||||
false // unwrap = false by default
|
||||
);
|
||||
|
||||
// Store actual burn amounts if available
|
||||
if (result?.actualBurnAmounts) {
|
||||
results[poolAddress] = result.actualBurnAmounts;
|
||||
}
|
||||
}
|
||||
|
||||
setActualBurnResults(results);
|
||||
setTransactionStatus('success');
|
||||
} catch (err) {
|
||||
console.error('Burn failed:', err);
|
||||
setTransactionError(err instanceof Error ? err.message : 'Transaction failed');
|
||||
setTransactionStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (transactionStatus === 'success') {
|
||||
// Clear selections and refresh
|
||||
setSelectedPools(new Set());
|
||||
// Refresh pool balances
|
||||
window.location.reload();
|
||||
}
|
||||
setTransactionStatus('idle');
|
||||
setTransactionError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto relative">
|
||||
<CardHeader>
|
||||
<CardTitle>Unstake Basket</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Select pools to burn all LP tokens and receive all underlying tokens proportionally
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Pool Selection List */}
|
||||
{poolsLoading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : poolsWithBalances.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>No pools with LP token balance found.</p>
|
||||
<p className="text-sm">Stake some tokens first to use this feature.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{poolsWithBalances.map((pool) => (
|
||||
<div
|
||||
key={pool.address}
|
||||
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
|
||||
selectedPools.has(pool.address)
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
onClick={() => togglePoolSelection(pool.address)}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPools.has(pool.address)}
|
||||
onChange={() => {}}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium">{pool.symbol}</div>
|
||||
<div className="text-xs text-muted-foreground">{pool.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-medium">
|
||||
{formatUnits(pool.lpBalance, 18)} LP
|
||||
</div>
|
||||
{poolTokens[pool.address] && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{poolTokens[pool.address].length} tokens
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedPools.has(pool.address) && poolTokens[pool.address] && (
|
||||
<div className="mt-3 pt-3 border-t border-border">
|
||||
<div className="text-xs text-muted-foreground mb-2">You will receive:</div>
|
||||
<div className="space-y-1">
|
||||
{poolTokens[pool.address].map((token) => (
|
||||
<div key={token.address} className="text-xs flex justify-between">
|
||||
<span>{token.symbol}</span>
|
||||
<span className="text-muted-foreground">
|
||||
(proportional to pool composition)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Burn Button */}
|
||||
<Button
|
||||
className="w-full h-14 text-lg"
|
||||
onClick={handleBurnAll}
|
||||
disabled={!isConnected || selectedPools.size === 0 || isBurning}
|
||||
>
|
||||
{!isConnected
|
||||
? 'Connect Wallet'
|
||||
: isBurning
|
||||
? 'Burning...'
|
||||
: `Burn ${selectedPools.size} Pool${selectedPools.size !== 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
|
||||
{selectedPools.size > 0 && (
|
||||
<div className="text-xs text-center text-muted-foreground">
|
||||
This will burn all LP tokens from {selectedPools.size} selected pool{selectedPools.size !== 1 ? 's' : ''}
|
||||
</div>
|
||||
)}
|
||||
</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-md w-full mx-4 shadow-lg max-h-[80vh] overflow-y-auto">
|
||||
{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">
|
||||
Burning LP Tokens
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Burning LP tokens from {selectedPools.size} pool{selectedPools.size !== 1 ? 's' : ''}
|
||||
</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">
|
||||
Burn Confirmed!
|
||||
</h3>
|
||||
|
||||
<div className="w-full space-y-4">
|
||||
{Object.entries(actualBurnResults).map(([poolAddress, burnAmounts]) => {
|
||||
const pool = poolsWithBalances.find(p => p.address === poolAddress);
|
||||
const tokens = poolTokens[poolAddress];
|
||||
|
||||
if (!pool || !tokens) return null;
|
||||
|
||||
return (
|
||||
<div key={poolAddress} className="border rounded-lg p-4">
|
||||
<div className="font-medium mb-2">{pool.symbol}</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">LP Burned:</span>
|
||||
<span className="font-medium">
|
||||
{formatUnits(burnAmounts.lpBurned, 18)} {pool.symbol}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mb-1">Tokens Received:</div>
|
||||
{burnAmounts.withdrawAmounts.map((amount, index) => {
|
||||
const token = tokens[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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
Burn 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>
|
||||
);
|
||||
}
|
||||
296
src/contracts/IPartyInfoABI.ts
Normal file
296
src/contracts/IPartyInfoABI.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
/* GENERATED FILE: DO NOT EDIT! */
|
||||
|
||||
const IPartyInfoABI = [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "burnAmounts",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "lpTokenAmount",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "withdrawAmounts",
|
||||
"type": "uint256[]",
|
||||
"internalType": "uint256[]"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "burnSwapAmounts",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "lpAmount",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputTokenIndex",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "amountOut",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outFee",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "flashFee",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "fee",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "maxFlashLoan",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "mintAmounts",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "lpTokenAmount",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "depositAmounts",
|
||||
"type": "uint256[]",
|
||||
"internalType": "uint256[]"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "poolPrice",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "quoteTokenIndex",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "int128",
|
||||
"internalType": "int128"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "price",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "baseTokenIndex",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "quoteTokenIndex",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "int128",
|
||||
"internalType": "int128"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "swapMintAmounts",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "inputTokenIndex",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "maxAmountIn",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "amountInUsed",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "lpMinted",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inFee",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "swapToLimitAmounts",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
},
|
||||
{
|
||||
"name": "inputTokenIndex",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputTokenIndex",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "limitPrice",
|
||||
"type": "int128",
|
||||
"internalType": "int128"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "amountIn",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "amountOut",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inFee",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "working",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "pool",
|
||||
"type": "address",
|
||||
"internalType": "contract IPartyPool"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
"internalType": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export default IPartyInfoABI;
|
||||
@@ -515,6 +515,11 @@ const IPartyPoolABI = [
|
||||
"type": "address",
|
||||
"internalType": "address"
|
||||
},
|
||||
{
|
||||
"name": "selector",
|
||||
"type": "bytes4",
|
||||
"internalType": "bytes4"
|
||||
},
|
||||
{
|
||||
"name": "receiver",
|
||||
"type": "address",
|
||||
|
||||
Reference in New Issue
Block a user