Compare commits

...

2 Commits

Author SHA1 Message Date
d4e41821a6 adding reactivity and error message handling 2025-12-03 17:43:17 -04:00
d07ff55c13 slippage for swaps 2025-12-03 16:53:57 -04:00
4 changed files with 88 additions and 37 deletions

View File

@@ -173,7 +173,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
}, [stakeAmount, selectedToken, mode]); }, [stakeAmount, selectedToken, mode]);
// Fetch swap mint amounts (for stake mode) // Fetch swap mint amounts (for stake mode)
const { swapMintAmounts, loading: swapMintLoading } = useSwapMintAmounts( const { swapMintAmounts, loading: swapMintLoading, error: swapMintError } = useSwapMintAmounts(
mode === 'stake' ? selectedPool?.address : undefined, mode === 'stake' ? selectedPool?.address : undefined,
mode === 'stake' ? inputTokenIndex : undefined, mode === 'stake' ? inputTokenIndex : undefined,
mode === 'stake' ? maxAmountIn : undefined, mode === 'stake' ? maxAmountIn : undefined,
@@ -181,7 +181,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
); );
// Fetch burn swap amounts (for unstake mode, only when not redeeming all) // Fetch burn swap amounts (for unstake mode, only when not redeeming all)
const { burnSwapAmounts, loading: burnSwapLoading } = useBurnSwapAmounts( const { burnSwapAmounts, loading: burnSwapLoading, error: burnSwapError } = useBurnSwapAmounts(
mode === 'unstake' && !redeemAll ? selectedPool?.address : undefined, mode === 'unstake' && !redeemAll ? selectedPool?.address : undefined,
mode === 'unstake' && !redeemAll ? maxAmountIn : undefined, mode === 'unstake' && !redeemAll ? maxAmountIn : undefined,
mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined, mode === 'unstake' && !redeemAll ? inputTokenIndex : undefined,
@@ -726,8 +726,27 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
</div> </div>
)} )}
{/* Error messages for output zero */}
{mode === 'stake' && swapMintError && stakeAmount && (
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium"> Cannot Process Stake</p>
<p className="text-xs text-destructive/80 mt-1">
{swapMintError}
</p>
</div>
)}
{mode === 'unstake' && !redeemAll && burnSwapError && stakeAmount && (
<div className="px-4 py-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium"> Cannot Process Unstake</p>
<p className="text-xs text-destructive/80 mt-1">
{burnSwapError}
</p>
</div>
)}
{/* Slippage warnings - consolidated for both stake and unstake modes */} {/* Slippage warnings - consolidated for both stake and unstake modes */}
{mode === 'stake' && slippageExceedsLimit && swapMintAmounts?.calculatedSlippage !== undefined && ( {mode === 'stake' && !swapMintError && slippageExceedsLimit && swapMintAmounts?.calculatedSlippage !== undefined && (
<SlippageWarning <SlippageWarning
slippage={swapMintAmounts.calculatedSlippage} slippage={swapMintAmounts.calculatedSlippage}
action="stake" action="stake"
@@ -735,7 +754,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
/> />
)} )}
{mode === 'stake' && !slippageExceedsLimit && slippageIsHigh && swapMintAmounts?.calculatedSlippage !== undefined && ( {mode === 'stake' && !swapMintError && !slippageExceedsLimit && slippageIsHigh && swapMintAmounts?.calculatedSlippage !== undefined && (
<SlippageWarning <SlippageWarning
slippage={swapMintAmounts.calculatedSlippage} slippage={swapMintAmounts.calculatedSlippage}
action="stake" action="stake"
@@ -743,7 +762,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
/> />
)} )}
{mode === 'unstake' && !redeemAll && unstakeSlippageExceedsLimit && burnSwapAmounts?.calculatedSlippage !== undefined && ( {mode === 'unstake' && !redeemAll && !burnSwapError && unstakeSlippageExceedsLimit && burnSwapAmounts?.calculatedSlippage !== undefined && (
<SlippageWarning <SlippageWarning
slippage={burnSwapAmounts.calculatedSlippage} slippage={burnSwapAmounts.calculatedSlippage}
action="unstake" action="unstake"
@@ -751,7 +770,7 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
/> />
)} )}
{mode === 'unstake' && !redeemAll && !unstakeSlippageExceedsLimit && slippageIsHigh && burnSwapAmounts?.calculatedSlippage !== undefined && ( {mode === 'unstake' && !redeemAll && !burnSwapError && !unstakeSlippageExceedsLimit && slippageIsHigh && burnSwapAmounts?.calculatedSlippage !== undefined && (
<SlippageWarning <SlippageWarning
slippage={burnSwapAmounts.calculatedSlippage} slippage={burnSwapAmounts.calculatedSlippage}
action="unstake" action="unstake"
@@ -807,10 +826,10 @@ export function StakeForm({ defaultMode = 'stake' }: StakeFormProps) {
!selectedPool || !selectedPool ||
isAmountExceedingBalance || isAmountExceedingBalance ||
(mode === 'stake' (mode === 'stake'
? (!selectedToken || isSwapMinting || slippageExceedsLimit) ? (!selectedToken || isSwapMinting || slippageExceedsLimit || !!swapMintError)
: (redeemAll : (redeemAll
? isBurning ? isBurning
: (!selectedToken || inputTokenIndex === undefined || isBurnSwapping || unstakeSlippageExceedsLimit))) : (!selectedToken || inputTokenIndex === undefined || isBurnSwapping || unstakeSlippageExceedsLimit || !!burnSwapError)))
} }
> >
{!isConnected {!isConnected

View File

@@ -94,6 +94,8 @@ export function SwapForm() {
const swapResult = swapAmounts[0]; // Get the first (and should be only) result const swapResult = swapAmounts[0]; // Get the first (and should be only) result
const formattedAmount = formatUnits(swapResult.amountOut, selectedToToken.decimals); const formattedAmount = formatUnits(swapResult.amountOut, selectedToToken.decimals);
setToAmount(formattedAmount); setToAmount(formattedAmount);
} else {
setToAmount('');
} }
}, [swapAmounts, selectedToToken, hasInsufficientBalance]); }, [swapAmounts, selectedToToken, hasInsufficientBalance]);

View File

@@ -99,6 +99,8 @@ export interface SwapRoute {
poolAddress: `0x${string}`; poolAddress: `0x${string}`;
inputTokenIndex: number; inputTokenIndex: number;
outputTokenIndex: number; outputTokenIndex: number;
inputTokenDecimal: number;
outputTokenDecimal: number;
} }
export interface AvailableToken { export interface AvailableToken {
@@ -234,11 +236,29 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs
functionName: 'symbol', functionName: 'symbol',
}).catch(() => null); }).catch(() => null);
// Skip tokens with the same symbol as the selected token const inputTokenDecimal = await publicClient.readContract({
address: tokenAddress,
abi: ERC20ABI,
functionName: 'decimals',
}).catch(() => null);
const outputTokenDecimal = await publicClient.readContract({
address: outputTokenAddress,
abi: ERC20ABI,
functionName: 'decimals',
}).catch(() => null);
// Skip tokens with the same symbol as the selected token
if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) { if (!outputTokenSymbol || outputTokenSymbol === selectedTokenSymbol) {
continue; continue;
} }
// Skip tokens if decimals failed to load
if (inputTokenDecimal === null || outputTokenDecimal === null) {
console.error(`Failed to load decimals for token ${outputTokenAddress} or ${tokenAddress}`);
continue;
}
// Create or update the available token entry // Create or update the available token entry
const tokenKey = outputTokenAddress.toLowerCase(); const tokenKey = outputTokenAddress.toLowerCase();
if (!tokenRoutesMap.has(tokenKey)) { if (!tokenRoutesMap.has(tokenKey)) {
@@ -254,6 +274,8 @@ export function useGetPoolsByToken(tokenAddress: `0x${string}` | undefined, offs
poolAddress, poolAddress,
inputTokenIndex, inputTokenIndex,
outputTokenIndex, outputTokenIndex,
inputTokenDecimal,
outputTokenDecimal,
}); });
} }
} catch (err) { } catch (err) {
@@ -505,6 +527,8 @@ export function useGetAllPools(offset: number = 0, limit: number = 100) {
// Calculate TVL (approximate by getting first token balance and multiplying by 3) // Calculate TVL (approximate by getting first token balance and multiplying by 3)
const tokenBalance = Number(balance) / Math.pow(10, decimals); const tokenBalance = Number(balance) / Math.pow(10, decimals);
console.log('tokenBalance', tokenBalance);
const approximateTVL = tokenBalance * 3; const approximateTVL = tokenBalance * 3;
tvlStr = formatTVL(approximateTVL); tvlStr = formatTVL(approximateTVL);
} }
@@ -632,7 +656,6 @@ export function useSwapMintAmounts(
const basePrice = Number(poolPriceInt128) / (2 ** 64); const basePrice = Number(poolPriceInt128) / (2 ** 64);
poolPrice = 1 / (basePrice * Math.pow(10, 18 - inputTokenDecimals)); poolPrice = 1 / (basePrice * Math.pow(10, 18 - inputTokenDecimals));
// Calculate slippage // Calculate slippage
const decimalsMultiplier = Math.pow(10, inputTokenDecimals); const decimalsMultiplier = Math.pow(10, inputTokenDecimals);
const lpMinted = Number(result[1]) / Math.pow(10, 18); const lpMinted = Number(result[1]) / Math.pow(10, 18);
@@ -641,7 +664,6 @@ export function useSwapMintAmounts(
const swapPrice = lpMinted / (amountIn - fee); const swapPrice = lpMinted / (amountIn - fee);
calculatedSlippage = ((poolPrice - swapPrice) / poolPrice) * 100; calculatedSlippage = ((poolPrice - swapPrice) / poolPrice) * 100;
} catch (priceErr) { } catch (priceErr) {
console.error('Error fetching poolPrice or calculating slippage:', priceErr); console.error('Error fetching poolPrice or calculating slippage:', priceErr);
} }
@@ -654,7 +676,8 @@ export function useSwapMintAmounts(
}); });
} catch (err) { } catch (err) {
console.error('Error calling swapMintAmounts:', err); console.error('Error calling swapMintAmounts:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch swap mint amounts'); const errorMessage = err instanceof Error ? err.message : 'Failed to fetch swap mint amounts';
setError(errorMessage);
setSwapMintAmounts(null); setSwapMintAmounts(null);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -763,7 +786,8 @@ export function useBurnSwapAmounts(
setBurnSwapAmounts(parsedAmounts); 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'); const errorMessage = err instanceof Error ? err.message : 'Failed to fetch burn swap amounts';
setError(errorMessage);
setBurnSwapAmounts(null); setBurnSwapAmounts(null);
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -21,13 +21,12 @@ const Q96 = 1n << 96n;
*/ */
export function calculateSlippage( export function calculateSlippage(
marketPrice: number, marketPrice: number,
swapOutputAmount: bigint, swapOutputAmount: number,
swapInputAmount: bigint, swapInputAmount: number,
swapFee: bigint swapFee: number
): number { ): number {
// Calculate actual swap price with decimal correction // Calculate actual swap price with decimal correction
const swapPrice = Number(swapOutputAmount) / (Number(swapInputAmount) - Number(swapFee)); const swapPrice = swapOutputAmount / ((swapInputAmount) - (swapFee));
// Calculate slippage percentage: ((swapPrice - marketPrice) / marketPrice) * 100 // Calculate slippage percentage: ((swapPrice - marketPrice) / marketPrice) * 100
const slippage = ((marketPrice - swapPrice) / marketPrice) * 100; const slippage = ((marketPrice - swapPrice) / marketPrice) * 100;
@@ -194,11 +193,18 @@ export function useSwapAmounts(
args: [route.poolAddress, BigInt(route.inputTokenIndex), BigInt(route.outputTokenIndex)], args: [route.poolAddress, BigInt(route.inputTokenIndex), BigInt(route.outputTokenIndex)],
}) as bigint; }) as bigint;
// Convert Q64 format to decimal (price = priceValue / 2^64)
const marketPrice = Number(priceInt128) / 2 ** 64;
// Convert Q128 format to decimal (price = priceValue / 2^128)
// Then apply decimal conversion: 10^18 / 10^outputTokenDecimals
const priceQ128 = Number(priceInt128) / 2 ** 128;
const decimalAdjustment = 10 ** Math.abs(route.outputTokenDecimal - route.inputTokenDecimal);
const marketPrice = priceQ128 / decimalAdjustment;
const swapOutputAmountDecimal = Number(swapOutputAmount) / 10 ** route.outputTokenDecimal;
const swapInputAmountDecimal = Number(swapInputAmount) / 10 ** route.inputTokenDecimal;
const swapFeeDecimal = Number(inFee) / 10 ** route.inputTokenDecimal;
// Calculate slippage using the reusable function // Calculate slippage using the reusable function
calculatedSlippage = calculateSlippage(marketPrice, swapOutputAmount, swapInputAmount, inFee);
calculatedSlippage = calculateSlippage(marketPrice, swapOutputAmountDecimal, swapInputAmountDecimal, swapFeeDecimal);
console.log('calculatedSlippage', calculatedSlippage) console.log('calculatedSlippage', calculatedSlippage)
} catch (slippageErr) { } catch (slippageErr) {
console.error(`Error calculating slippage for ${token.symbol}:`, slippageErr); console.error(`Error calculating slippage for ${token.symbol}:`, slippageErr);