adding actual amounts and fees to confirmation pages for swap, stake and unstake

This commit is contained in:
2025-11-05 14:31:56 -04:00
parent 9707c4892b
commit 0a518d31f9
5 changed files with 332 additions and 27 deletions

View File

@@ -7,8 +7,8 @@ import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ChevronDown, CheckCircle, XCircle, Loader2, ArrowDownUp } from 'lucide-react'; import { ChevronDown, CheckCircle, XCircle, Loader2, ArrowDownUp } from 'lucide-react';
import { useAccount } from 'wagmi'; import { useAccount } from 'wagmi';
import { useGetAllPools, useTokenDetails, useSwapMintAmounts, useBurnSwapAmounts, useLPTokenBalance, type PoolDetails, type TokenDetails } from '@/hooks/usePartyPlanner'; import { useGetAllPools, useTokenDetails, useSwapMintAmounts, useBurnSwapAmounts, useLPTokenBalance, type PoolDetails, type TokenDetails, type BurnSwapAmounts } from '@/hooks/usePartyPlanner';
import { useSwapMint, useBurnSwap } from '@/hooks/usePartyPool'; import { useSwapMint, useBurnSwap, type ActualSwapMintAmounts, type ActualBurnSwapAmounts } from '@/hooks/usePartyPool';
import { formatUnits, parseUnits } from 'viem'; import { formatUnits, parseUnits } from 'viem';
import IPartyPoolABI from '@/contracts/IPartyPoolABI'; import IPartyPoolABI from '@/contracts/IPartyPoolABI';
@@ -30,6 +30,8 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
const [isTokenDropdownOpen, setIsTokenDropdownOpen] = useState(false); const [isTokenDropdownOpen, setIsTokenDropdownOpen] = useState(false);
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle'); const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
const [transactionError, setTransactionError] = useState<string | null>(null); const [transactionError, setTransactionError] = useState<string | null>(null);
const [actualSwapMintAmounts, setActualSwapMintAmounts] = useState<ActualSwapMintAmounts | null>(null);
const [actualBurnSwapAmounts, setActualBurnSwapAmounts] = useState<ActualBurnSwapAmounts | null>(null);
const poolDropdownRef = useRef<HTMLDivElement>(null); const poolDropdownRef = useRef<HTMLDivElement>(null);
const tokenDropdownRef = useRef<HTMLDivElement>(null); const tokenDropdownRef = useRef<HTMLDivElement>(null);
@@ -152,21 +154,31 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
try { try {
if (mode === 'stake') { if (mode === 'stake') {
// Execute the swap mint transaction // Execute the swap mint transaction and capture actual amounts
await executeSwapMint( const result = await executeSwapMint(
selectedPool.address, selectedPool.address,
selectedToken.address, selectedToken.address,
inputTokenIndex, inputTokenIndex,
maxAmountIn maxAmountIn
); );
// Store actual swap mint amounts if available
if (result?.actualSwapMintAmounts) {
setActualSwapMintAmounts(result.actualSwapMintAmounts);
}
} else { } else {
// Execute the burn swap transaction // Execute the burn swap transaction and capture actual amounts
await executeBurnSwap( const result = await executeBurnSwap(
selectedPool.address, selectedPool.address,
maxAmountIn, maxAmountIn,
inputTokenIndex, inputTokenIndex,
false // unwrap = false by default false // unwrap = false by default
); );
// Store actual burn swap amounts if available
if (result?.actualBurnSwapAmounts) {
setActualBurnSwapAmounts(result.actualBurnSwapAmounts);
}
} }
setTransactionStatus('success'); setTransactionStatus('success');
@@ -490,7 +502,13 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground">{t('stake.amountOut')}:</span> <span className="text-muted-foreground">{t('stake.amountOut')}:</span>
<span className="font-medium"> <span className="font-medium">
{burnSwapLoading ? 'Calculating...' : `${formatUnits(burnSwapAmounts, selectedToken.decimals)} ${selectedToken.symbol}`} {burnSwapLoading ? 'Calculating...' : `${Number(formatUnits(burnSwapAmounts.amountOut, selectedToken.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${selectedToken.symbol}`}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{t('stake.fee')}:</span>
<span className="font-medium">
{burnSwapLoading ? 'Calculating...' : `${Number(formatUnits(burnSwapAmounts.outFee, selectedToken.decimals)).toLocaleString('en-US', { maximumFractionDigits: 6 })} ${selectedToken.symbol}`}
</span> </span>
</div> </div>
</div> </div>
@@ -538,12 +556,82 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
<h3 className="text-xl font-semibold text-center"> <h3 className="text-xl font-semibold text-center">
{mode === 'stake' ? 'Stake Confirmed!' : 'Unstake Confirmed!'} {mode === 'stake' ? 'Stake Confirmed!' : 'Unstake Confirmed!'}
</h3> </h3>
<p className="text-sm text-muted-foreground text-center">
{mode === 'stake' {/* Display actual amounts or estimates */}
? `Successfully staked ${stakeAmount} ${selectedToken?.symbol} to ${selectedPool?.symbol}` {mode === 'stake' ? (
: `Successfully unstaked ${stakeAmount} ${selectedPool?.symbol} LP for ${selectedToken?.symbol}` // Stake mode success message
} <div className="w-full space-y-3">
</p> {actualSwapMintAmounts && selectedToken && selectedPool ? (
// Show actual amounts from transaction
<>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Token Used:</span>
<span className="font-medium">{formatUnits(actualSwapMintAmounts.amountInUsed, selectedToken.decimals)} {selectedToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">LP Received:</span>
<span className="font-medium">{formatUnits(actualSwapMintAmounts.lpMinted, 18)} {selectedPool.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">LP Fee:</span>
<span className="font-medium">{formatUnits(actualSwapMintAmounts.lpFee, selectedToken.decimals)} {selectedToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Protocol Fee:</span>
<span className="font-medium">{formatUnits(actualSwapMintAmounts.protocolFee, selectedToken.decimals)} {selectedToken.symbol}</span>
</div>
</>
) : (
// Fallback to estimates
<>
<p className="text-sm text-muted-foreground text-center">
Successfully staked {stakeAmount} {selectedToken?.symbol} to {selectedPool?.symbol}
<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">
{actualBurnSwapAmounts && selectedToken && 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(actualBurnSwapAmounts.amountIn, 18)} {selectedPool.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Token Received:</span>
<span className="font-medium">{formatUnits(actualBurnSwapAmounts.amountOut, selectedToken.decimals)} {selectedToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">LP Fee:</span>
<span className="font-medium">{formatUnits(actualBurnSwapAmounts.lpFee, selectedToken.decimals)} {selectedToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Protocol Fee:</span>
<span className="font-medium">{formatUnits(actualBurnSwapAmounts.protocolFee, selectedToken.decimals)} {selectedToken.symbol}</span>
</div>
</>
) : (
// Fallback to estimates
<>
<p className="text-sm text-muted-foreground text-center">
Successfully unstaked {stakeAmount} {selectedPool?.symbol} LP for {selectedToken?.symbol}
<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>
)}
<Button <Button
onClick={handleCloseModal} onClick={handleCloseModal}
className="w-full mt-4" className="w-full mt-4"

View File

@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
import { ArrowDownUp, ChevronDown, Settings, CheckCircle, XCircle, Loader2 } 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, type ActualSwapAmounts } 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';
@@ -28,6 +28,7 @@ export function SwapForm() {
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false); const [isReviewModalOpen, setIsReviewModalOpen] = useState(false);
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle'); const [transactionStatus, setTransactionStatus] = useState<TransactionStatus>('idle');
const [transactionError, setTransactionError] = useState<string | null>(null); const [transactionError, setTransactionError] = useState<string | null>(null);
const [actualSwapAmounts, setActualSwapAmounts] = useState<ActualSwapAmounts | null>(null);
const fromDropdownRef = useRef<HTMLDivElement>(null); const fromDropdownRef = useRef<HTMLDivElement>(null);
const toDropdownRef = useRef<HTMLDivElement>(null); const toDropdownRef = useRef<HTMLDivElement>(null);
@@ -114,8 +115,8 @@ export function SwapForm() {
// Use the user's input directly as maxAmountIn // Use the user's input directly as maxAmountIn
const maxAmountIn = parseUnits(fromAmount, selectedFromToken.decimals); const maxAmountIn = parseUnits(fromAmount, selectedFromToken.decimals);
// Execute the swap // Execute the swap and capture actual amounts from the transaction
await executeSwap( const result = await executeSwap(
bestRoute.poolAddress, bestRoute.poolAddress,
selectedFromToken.address, selectedFromToken.address,
bestRoute.inputTokenIndex, bestRoute.inputTokenIndex,
@@ -124,6 +125,11 @@ export function SwapForm() {
currentSlippage currentSlippage
); );
// Store the actual swap amounts if available
if (result?.actualSwapAmounts) {
setActualSwapAmounts(result.actualSwapAmounts);
}
setTransactionStatus('success'); setTransactionStatus('success');
} catch (err) { } catch (err) {
console.error('Swap failed:', err); console.error('Swap failed:', err);
@@ -466,9 +472,43 @@ export function SwapForm() {
<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">Swap Confirmed!</h3> <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} {/* Display actual amounts or estimates */}
</p> <div className="w-full space-y-3">
{actualSwapAmounts && selectedFromToken && selectedToToken ? (
// Show actual amounts from transaction
<>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Swapped:</span>
<span className="font-medium">{formatUnits(actualSwapAmounts.amountIn, selectedFromToken.decimals)} {selectedFromToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Received:</span>
<span className="font-medium">{formatUnits(actualSwapAmounts.amountOut, selectedToToken.decimals)} {selectedToToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">LP Fee:</span>
<span className="font-medium">{formatUnits(actualSwapAmounts.lpFee, selectedFromToken.decimals)} {selectedFromToken.symbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Protocol Fee:</span>
<span className="font-medium">{formatUnits(actualSwapAmounts.protocolFee, selectedFromToken.decimals)} {selectedFromToken.symbol}</span>
</div>
</>
) : (
// Fallback to estimates
<>
<p className="text-sm text-muted-foreground text-center">
Successfully swapped {fromAmount} {selectedFromToken?.symbol} to {toAmount} {selectedToToken?.symbol}
<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>
<Button <Button
onClick={handleCloseModal} onClick={handleCloseModal}
className="w-full mt-4" className="w-full mt-4"

View File

@@ -195,7 +195,12 @@ const IPartyPoolABI = [
], ],
"outputs": [ "outputs": [
{ {
"name": "amountOutUint", "name": "amountOut",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "outFee",
"type": "uint256", "type": "uint256",
"internalType": "uint256" "internalType": "uint256"
} }

View File

@@ -448,6 +448,11 @@ export interface SwapMintAmounts {
lpMinted: bigint; lpMinted: bigint;
} }
export interface BurnSwapAmounts {
amountOut: bigint;
outFee: bigint;
}
export function useSwapMintAmounts( export function useSwapMintAmounts(
poolAddress: `0x${string}` | undefined, poolAddress: `0x${string}` | undefined,
inputTokenIndex: number | undefined, inputTokenIndex: number | undefined,
@@ -531,7 +536,7 @@ export function useBurnSwapAmounts(
) { ) {
const publicClient = usePublicClient(); const publicClient = usePublicClient();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [burnSwapAmounts, setBurnSwapAmounts] = useState<bigint | null>(null); const [burnSwapAmounts, setBurnSwapAmounts] = useState<BurnSwapAmounts | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -567,15 +572,43 @@ export function useBurnSwapAmounts(
return; return;
} }
// Call burnSwapAmounts function // Log inputs
console.log('🔍 burnSwapAmounts INPUTS:', {
chainId: chainId.toString(),
rpcUrl: publicClient.transport?.url || 'Unknown',
poolAddress,
lpAmount: lpAmount.toString(),
inputTokenIndex,
viewerAddress,
});
// Call burnSwapAmounts function - returns [amountOut, outFee]
const result = await publicClient.readContract({ const result = await publicClient.readContract({
address: viewerAddress as `0x${string}`, address: viewerAddress as `0x${string}`,
abi: IPartyPoolViewerABI, abi: IPartyPoolViewerABI,
functionName: 'burnSwapAmounts', functionName: 'burnSwapAmounts',
args: [poolAddress, lpAmount, BigInt(inputTokenIndex)], args: [poolAddress, lpAmount, BigInt(inputTokenIndex)],
}) as bigint; }) as readonly [bigint, bigint];
setBurnSwapAmounts(result); // Log raw result
console.log('📊 burnSwapAmounts RAW RESULT:', {
resultArray: result,
amountOut: result[0].toString(),
outFee: result[1].toString(),
});
const parsedAmounts = {
amountOut: result[0],
outFee: result[1],
};
// Log parsed result
console.log('✅ burnSwapAmounts PARSED:', {
amountOut: parsedAmounts.amountOut.toString(),
outFee: parsedAmounts.outFee.toString(),
});
setBurnSwapAmounts(parsedAmounts);
} catch (err) { } catch (err) {
console.error('Error calling burnSwapAmounts:', err); console.error('Error calling burnSwapAmounts:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch burn swap amounts'); setError(err instanceof Error ? err.message : 'Failed to fetch burn swap amounts');

View File

@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { usePublicClient, useWalletClient } from 'wagmi'; import { usePublicClient, useWalletClient } from 'wagmi';
import { decodeEventLog } from 'viem';
import IPartyPoolABI from '@/contracts/IPartyPoolABI'; import IPartyPoolABI from '@/contracts/IPartyPoolABI';
import chainInfo from '../../../lmsr-amm/liqp-deployments.json'; import chainInfo from '../../../lmsr-amm/liqp-deployments.json';
import type { AvailableToken } from './usePartyPlanner'; import type { AvailableToken } from './usePartyPlanner';
@@ -189,6 +190,27 @@ export interface GasEstimate {
estimatedCostUsd: string; estimatedCostUsd: string;
} }
export interface ActualSwapAmounts {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
}
export interface ActualSwapMintAmounts {
amountInUsed: bigint;
lpMinted: bigint;
lpFee: bigint;
protocolFee: bigint;
}
export interface ActualBurnSwapAmounts {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
}
export function useSwap() { export function useSwap() {
const { data: walletClient } = useWalletClient(); const { data: walletClient } = useWalletClient();
const publicClient = usePublicClient(); const publicClient = usePublicClient();
@@ -338,7 +360,46 @@ export function useSwap() {
const receipt = await publicClient.waitForTransactionReceipt({ hash }); const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('✅ Swap transaction confirmed:', receipt); console.log('✅ Swap transaction confirmed:', receipt);
return receipt; // Parse the Swap event from the receipt logs
let actualSwapAmounts: ActualSwapAmounts | null = null;
for (const log of receipt.logs) {
try {
const decodedLog = decodeEventLog({
abi: IPartyPoolABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'Swap') {
const { amountIn, amountOut, lpFee, protocolFee } = decodedLog.args as {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
};
actualSwapAmounts = {
amountIn,
amountOut,
lpFee,
protocolFee,
};
console.log('📊 Actual swap amounts from event:', {
amountIn: amountIn.toString(),
amountOut: amountOut.toString(),
lpFee: lpFee.toString(),
protocolFee: protocolFee.toString(),
});
break;
}
} catch (err) {
// Skip logs that don't match our ABI
continue;
}
}
return { receipt, actualSwapAmounts };
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Swap failed'; const errorMessage = err instanceof Error ? err.message : 'Swap failed';
setSwapError(errorMessage); setSwapError(errorMessage);
@@ -446,7 +507,46 @@ export function useSwapMint() {
const receipt = await publicClient.waitForTransactionReceipt({ hash }); const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('✅ SwapMint transaction confirmed:', receipt); console.log('✅ SwapMint transaction confirmed:', receipt);
return receipt; // Parse the SwapMint event from the receipt logs
let actualSwapMintAmounts: ActualSwapMintAmounts | null = null;
for (const log of receipt.logs) {
try {
const decodedLog = decodeEventLog({
abi: IPartyPoolABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'SwapMint') {
const { amountIn, amountOut, lpFee, protocolFee } = decodedLog.args as {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
};
actualSwapMintAmounts = {
amountInUsed: amountIn,
lpMinted: amountOut,
lpFee,
protocolFee,
};
console.log('📊 Actual swap mint amounts from event:', {
amountInUsed: amountIn.toString(),
lpMinted: amountOut.toString(),
lpFee: lpFee.toString(),
protocolFee: protocolFee.toString(),
});
break;
}
} catch (err) {
// Skip logs that don't match our ABI
continue;
}
}
return { receipt, actualSwapMintAmounts };
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'SwapMint failed'; const errorMessage = err instanceof Error ? err.message : 'SwapMint failed';
setSwapMintError(errorMessage); setSwapMintError(errorMessage);
@@ -553,7 +653,46 @@ export function useBurnSwap() {
const receipt = await publicClient.waitForTransactionReceipt({ hash }); const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log('✅ BurnSwap transaction confirmed:', receipt); console.log('✅ BurnSwap transaction confirmed:', receipt);
return receipt; // Parse the BurnSwap event from the receipt logs
let actualBurnSwapAmounts: ActualBurnSwapAmounts | null = null;
for (const log of receipt.logs) {
try {
const decodedLog = decodeEventLog({
abi: IPartyPoolABI,
data: log.data,
topics: log.topics,
});
if (decodedLog.eventName === 'BurnSwap') {
const { amountIn, amountOut, lpFee, protocolFee } = decodedLog.args as {
amountIn: bigint;
amountOut: bigint;
lpFee: bigint;
protocolFee: bigint;
};
actualBurnSwapAmounts = {
amountIn,
amountOut,
lpFee,
protocolFee,
};
console.log('📊 Actual burn swap amounts from event:', {
amountIn: amountIn.toString(),
amountOut: amountOut.toString(),
lpFee: lpFee.toString(),
protocolFee: protocolFee.toString(),
});
break;
}
} catch (err) {
// Skip logs that don't match our ABI
continue;
}
}
return { receipt, actualBurnSwapAmounts };
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'BurnSwap failed'; const errorMessage = err instanceof Error ? err.message : 'BurnSwap failed';
setBurnSwapError(errorMessage); setBurnSwapError(errorMessage);