ghost-dao-interface/src/containers/Dex/SwapContainer.jsx
Uncle Fatso 55b047e02f
align the information logic on the DEX
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-03-26 13:33:19 +03:00

303 lines
12 KiB
JavaScript

import { useState, useMemo, useEffect } from "react";
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
import toast from "react-hot-toast";
import TokenStack from "../../components/TokenStack/TokenStack";
import SwapCard from "../../components/Swap/SwapCard";
import SwapCollection from "../../components/Swap/SwapCollection";
import { TokenAllowanceGuard } from "../../components/TokenAllowanceGuard/TokenAllowanceGuard";
import { SecondaryButton } from "../../components/Button";
import { formatCurrency } from "../../helpers";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { prettifySecondsInDays } from "../../helpers/timeUtil";
import { getTokenAddress } from "../../hooks/helpers";
import { useBalance, depositNative, withdrawWeth } from "../../hooks/tokens";
import {
useUniswapV2Pair,
useUniswapV2PairReserves,
swapExactTokensForTokens,
swapExactETHForTokens,
swapExactTokensForETH,
} from "../../hooks/uniswapv2";
import { EMPTY_ADDRESS } from "../../constants/addresses";
const SwapContainer = ({
tokenNameTop,
tokenNameBottom,
address,
chainId,
dexAddresses,
connect,
onCardsSwap,
setTopTokenListOpen,
setBottomTokenListOpen,
slippage,
destination,
secondsToWait,
setIsSwap,
formatDecimals,
isWrapping,
isUnwrapping,
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 456px)");
const [isPending, setIsPending] = useState(false);
const [amountBottom, setAmountBottom] = useState("");
const [amountTop, setAmountTop] = useState("");
const [currentPrice, setCurrentPrice] = useState(new DecimalBigNumber(0n, 0));
const [nextPrice, setNextPrice] = useState(new DecimalBigNumber(0n, 0));
const {
balance: balanceTop,
refetch: balanceRefetchTop,
contractAddress: addressTop,
isNative: topIsNative,
} = useBalance(chainId, tokenNameTop, address);
const {
balance: balanceBottom,
refetch: balanceRefetchBottom,
contractAddress: addressBottom,
isNative: bottomIsNative,
} = useBalance(chainId, tokenNameBottom, address);
const {
pairAddress,
} = useUniswapV2Pair(chainId, dexAddresses.factory, addressTop, addressBottom);
const {
reserves: pairReserves,
tokens: tokenAddresses,
refetch: pairReservesRefetch,
} = useUniswapV2PairReserves(
chainId,
pairAddress,
);
const onSwap = () => {
setAmountTop("");
setAmountBottom("");
onCardsSwap();
}
const setMax = () => setAmountTop(balanceTop.toString());
useEffect(() => {
if (isWrapping || isUnwrapping) {
setAmountBottom(amountTop.toString());
setNextPrice("1");
setCurrentPrice("1");
return;
}
const zero = new DecimalBigNumber(0n, 0);
const raw = new DecimalBigNumber(amountTop, balanceTop._decimals);
const amountInRaw = new DecimalBigNumber(raw._value.toBigInt(), balanceTop._decimals);
const amountInWithFee = amountInRaw.mul(new DecimalBigNumber(997n, 3));
const amountIn = addressTop.toUpperCase() === tokenAddresses.token0.toUpperCase() ? pairReserves.reserve0 : pairReserves.reserve1;
const amountOut = addressBottom.toUpperCase() === tokenAddresses.token1.toUpperCase() ? pairReserves.reserve1 : pairReserves.reserve0;
if (amountOut.eq(zero)) {
setCurrentPrice("");
} else {
setCurrentPrice(amountIn.div(amountOut).toString());
}
if (amountOut.eq(zero) || amountInWithFee.eq(zero)) {
setAmountBottom("");
setNextPrice("");
} else {
const nominator = amountOut.mul(amountInWithFee);
const denominator = amountIn.add(amountInWithFee);
const newAmountOut = nominator.div(denominator);
const newReserveIn = amountIn.add(amountInWithFee);
const newReserveOut = amountOut.sub(newAmountOut);
const nextPrice = newReserveIn.div(newReserveOut);
setAmountBottom(newAmountOut.toString());
setNextPrice(nextPrice.toString());
}
}, [pairReserves, addressBottom, amountTop, addressTop, isWrapping, isUnwrapping]);
const minReceived = useMemo(() => {
const decimals = 7;
const shares = Math.pow(10, decimals);
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
const bigIntSlippage = one - BigInt(Math.round(floatSlippage * shares));
const slippageDecimalBigNumber = new DecimalBigNumber(bigIntSlippage, 2);
const bigIntAmount = BigInt(Math.round(amountBottom * shares));
const amountDecimalBigNumber = new DecimalBigNumber(bigIntAmount, decimals);
const tmpResult = amountDecimalBigNumber.mul(slippageDecimalBigNumber);
const result = new DecimalBigNumber(tmpResult?._value, tmpResult?._decimals + decimals);
return result?.toString();
}, [amountBottom, amountBottom, slippage, balanceBottom]);
const buttonText = useMemo(() => {
let text = "Swap";
if (isWrapping) text = "Wrap";
else if (isUnwrapping) text = "Unwrap";
else if (pairAddress === EMPTY_ADDRESS) text = "Create Pool";
return text;
}, [isWrapping, isUnwrapping, pairAddress]);
const swapTokens = async () => {
setIsPending(true);
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
const shares = 100000;
const one = BigInt(shares * 100);
const floatSlippage = slippage === "" ? 0 : parseFloat(slippage);
const bigIntSlippage = one - BigInt(Math.round(floatSlippage * shares));
if (floatSlippage < 3) toast("Slippage is too low, transaction highly likely will fail.");
const amountADesired = BigInt(Math.round(parseFloat(amountTop) * Math.pow(10, balanceTop._decimals)));
const amountBDesired = BigInt(Math.round(parseFloat(amountBottom) * Math.pow(10, balanceBottom._decimals)));
const amountBMin = amountBDesired * bigIntSlippage / one;
if (isWrapping) {
await depositNative(chainId, address, amountADesired);
} else if (isUnwrapping) {
await withdrawWeth(chainId, address, amountADesired);
} else {
const params = {
chainId,
amountADesired,
amountBMin,
tokenNameTop,
tokenNameBottom,
destination,
address,
deadline
};
if (topIsNative) {
await swapExactETHForTokens(params)
} else if (bottomIsNative) {
await swapExactTokensForETH(params)
} else {
await swapExactTokensForTokens(params);
}
}
await balanceRefetchTop();
await balanceRefetchBottom();
await pairReservesRefetch();
setAmountTop("");
setIsPending(false);
}
return (
<Box maxWidth="356px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
<SwapCollection
UpperSwapCard={
<SwapCard
maxWidth="356px"
id="from"
token={<TokenStack tokens={[tokenNameTop]} sx={{ fontSize: "21px" }} />}
tokenName={tokenNameTop}
info={
(!isSmallScreen ? "Balance: " : "") +
formatCurrency(balanceTop ? balanceTop : "0", formatDecimals, tokenNameTop)
}
endString="Max"
endStringOnClick={setMax}
value={amountTop}
onChange={event => setAmountTop(event.currentTarget.value)}
tokenOnClick={() => setTopTokenListOpen(true)}
inputProps={{ "data-testid": "fromInput" }}
/>
}
LowerSwapCard={
<SwapCard
maxWidth="356px"
id="to"
token={<TokenStack tokens={[tokenNameBottom]} sx={{ fontSize: "21px" }} />}
tokenName={tokenNameBottom}
value={amountBottom}
inputProps={{ "data-testid": "toInput" }}
tokenOnClick={() => setBottomTokenListOpen(true)}
info={
(!isSmallScreen ? "Balance: " : "") +
formatCurrency(balanceBottom ? balanceBottom : "0", formatDecimals, tokenNameBottom)
}
/>
}
arrowOnClick={onSwap}
/>
<Box
m="10px 0"
display="flex"
flexDirection="column"
justifyContent="space-between"
alignItems="center"
gap="0px"
maxWidth="356px"
style={{ fontSize: "12px", color: theme.colors.gray[40] }}
>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameBottom} (Current)`}</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(currentPrice, formatDecimals, tokenNameTop)}</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">{`1 ${tokenNameBottom} (Next)`}</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(nextPrice === "" ? currentPrice : nextPrice, formatDecimals, tokenNameTop)}</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">Min. Receive:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatCurrency(minReceived, formatDecimals, tokenNameBottom)}</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">Tx. Deadline:</Typography>
<Typography fontSize="12px" lineHeight="15px">~{prettifySecondsInDays(secondsToWait)}</Typography>
</Box>
</Box>
<TokenAllowanceGuard
spendAmount={new DecimalBigNumber(amountTop, balanceTop._decimals)}
tokenName={tokenNameTop}
owner={address}
spender={dexAddresses.router}
decimals={balanceTop._decimals}
isNative={topIsNative}
approvalText={"Approve " + tokenNameTop}
approvalPendingText={"Approving..."}
connect={connect}
width="100%"
height="60px"
>
<SecondaryButton
fullWidth
disabled={
address !== "" && (
(new DecimalBigNumber(amountTop, balanceTop._decimals)).eq(new DecimalBigNumber("0", 18)) ||
balanceTop?.lt(new DecimalBigNumber(amountTop, balanceTop._decimals)) ||
isPending
)
}
loading={isPending}
onClick={() => address === "" ?
connect()
:
(!isWrapping && !isUnwrapping) && pairAddress === EMPTY_ADDRESS ? setIsSwap(false) : swapTokens()
}
>
{address === "" ? "Connect" : buttonText }
</SecondaryButton>
</TokenAllowanceGuard>
</Box>
)
}
export default SwapContainer;