ghost-dao-interface/src/containers/Dex/PoolContainer.jsx
Uncle Fatso d4446f6fb1
version 0.0.22
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2025-04-28 14:03:56 +03:00

306 lines
12 KiB
JavaScript

import { useState, useEffect, useMemo } 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 { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import { formatNumber, formatCurrency } from "../../helpers";
import { useBalance, useTotalSupply } from "../../hooks/tokens";
import { useUniswapV2Pair, useUniswapV2PairReserves, addLiquidity } from "../../hooks/uniswapv2";
const PoolContainer = ({
tokenNameTop,
tokenNameBottom,
onCardsSwap,
address,
chainId,
dexAddresses,
connect,
slippage,
secondsToWait,
setTopTokenListOpen,
setBottomTokenListOpen,
formatDecimals
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 456px)");
const [isPending, setIsPending] = useState(false);
const [amountTop, setAmountTop] = useState("");
const [amountBottom, setAmountBottom] = useState("");
const {
balance: balanceTop,
refetch: balanceRefetchTop,
contractAddress: addressTop,
} = useBalance(chainId, tokenNameTop, address);
const {
balance: balanceBottom,
refetch: balanceRefetchBottom,
contractAddress: addressBottom,
} = useBalance(chainId, tokenNameBottom, address);
const {
pairAddress,
refetch: pairAddressRefetch,
} = useUniswapV2Pair(chainId, dexAddresses.factory, addressTop, addressBottom);
const {
balance: lpBalance,
refetch: lpRefetch,
} = useBalance(chainId, pairAddress, address);
const {
totalSupply: lpTotalSupply,
refetch: lpTotalSupplyRefetch
} = useTotalSupply(chainId, pairAddress);
const {
reserves: pairReserves,
refetch: pairReservesRefetch,
} = useUniswapV2PairReserves(
chainId,
pairAddress,
balanceTop._decimals,
balanceBottom._decimals,
tokenNameTop,
tokenNameBottom,
);
const onSwap = () => {
const oldAmountTop = amountTop;
const oldAmountBottom = amountBottom;
setAmountBottom(oldAmountTop);
setAmountTop(oldAmountBottom);
onCardsSwap();
}
const setMaxTop = () => setAmountTop(balanceTop.toString());
const setMaxBottom = () => setAmountBottom(balanceBottom.toString());
const bigIntSqrt = (n) => {
if (n < 0n) {
throw new Error("Cannot compute the square root of a negative number.");
}
if (n < 2n) {
return n; // The square root of 0 or 1 is the number itself
}
let low = 0n;
let high = n;
let mid;
while (low <= high) {
mid = (low + high) / 2n;
const midSquared = mid * mid;
if (midSquared === n) {
return mid; // Found the exact square root
} else if (midSquared < n) {
low = mid + 1n; // Move to the right half
} else {
high = mid - 1n; // Move to the left half
}
}
return high; // The integer part of the square root
}
const estimatedAmountOut = useMemo(() => {
const zero = new DecimalBigNumber(0n, 0);
const value0 = new DecimalBigNumber(amountTop, balanceTop._decimals)
const value1 = new DecimalBigNumber(amountBottom, balanceBottom._decimals)
const amountToAddA = new DecimalBigNumber(value0._value.toBigInt(), balanceTop._decimals);
const amountToAddB = new DecimalBigNumber(value1._value.toBigInt(), balanceBottom._decimals);
if (
pairReserves.reserve0.gt(zero) &&
pairReserves.reserve1.gt(zero) &&
lpTotalSupply.gt(new DecimalBigNumber(0n, 0))
) {
const lpTokensFromA = (amountToAddA.mul(lpTotalSupply).div(pairReserves.reserve0));
const lpTokensFromB = (amountToAddB.mul(lpTotalSupply).div(pairReserves.reserve1));
const lpTokensToMint = lpTokensFromA.gt(lpTokensFromB) ? lpTokensFromB : lpTokensFromA;
return lpTokensToMint;
} else {
const tokens = bigIntSqrt(BigInt(amountToAddA.mul(amountToAddB)._value));
const lpTokensToMint = new DecimalBigNumber(tokens, 18);
return lpTokensToMint;
}
}, [pairReserves, amountTop, amountBottom, balanceTop, balanceBottom]);
useEffect(() => {
if (pairReserves && pairReserves.reserve1.gt(new DecimalBigNumber(0n, 0))) {
const value = new DecimalBigNumber(amountTop, balanceTop._decimals)
const amountToAdd = new DecimalBigNumber(value._value.toBigInt(), balanceTop._decimals);
const amount = amountToAdd.mul(pairReserves.reserve0).div(pairReserves.reserve1);
setAmountBottom(amount.toString());
}
}, [amountTop, pairReserves])
useEffect(() => {
setAmountTop("");
setAmountBottom("");
}, [tokenNameTop, tokenNameBottom])
const addLiquidityInner = async () => {
setIsPending(true);
const deadline = Math.floor(Date.now() / 1000) + secondsToWait;
const destination = address;
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 amountAMin = amountADesired * bigIntSlippage / one;
const amountBMin = amountBDesired * bigIntSlippage / one;
await addLiquidity(
chainId,
tokenNameTop,
tokenNameBottom,
amountADesired,
amountBDesired,
amountAMin,
amountBMin,
destination,
deadline,
);
await balanceRefetchTop();
await balanceRefetchBottom();
await pairAddressRefetch();
await lpRefetch();
await lpTotalSupplyRefetch();
await pairReservesRefetch();
setAmountTop("");
setAmountBottom("");
setIsPending(false);
}
const emptyPool = useMemo(() => {
return pairAddress === "" || pairAddress === "0x0000000000000000000000000000000000000000"
}, [pairAddress])
return (
<Box maxWidth="356px" display="flex" flexDirection="column" justifyContent="space-between" height="100%">
<SwapCollection
UpperSwapCard={
<SwapCard
id="from"
token={<TokenStack tokens={[tokenNameTop]} sx={{ fontSize: "21px" }} />}
tokenName={tokenNameTop}
info={(!isSmallScreen ? "Balance: " : "") + formatCurrency(balanceTop, formatDecimals, tokenNameTop)}
endString="Max"
endStringOnClick={setMaxTop}
value={amountTop}
onChange={event => setAmountTop(event.currentTarget.value)}
tokenOnClick={() => setTopTokenListOpen(true)}
inputProps={{ "data-testid": "fromInput" }}
/>
}
LowerSwapCard={
<SwapCard
id="to"
token={<TokenStack tokens={[tokenNameBottom]} sx={{ fontSize: "21px" }} />}
tokenName={tokenNameBottom}
value={amountBottom }
onChange={event => emptyPool ? setAmountBottom(event.currentTarget.value) : {}}
endString={emptyPool ? "Max" : undefined}
endStringOnClick={emptyPool ? setMaxBottom : {}}
inputProps={{ "data-testid": "toInput" }}
tokenOnClick={() => setBottomTokenListOpen(true)}
info={(!isSmallScreen ? "Balance: " : "") + formatCurrency(balanceBottom, formatDecimals, tokenNameBottom)}
/>
}
arrowOnClick={onSwap}
/>
{!isSmallScreen && <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">Current Balance:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatNumber(lpBalance, formatDecimals)} LP</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">Total Supply:</Typography>
<Typography fontSize="12px" lineHeight="15px">{formatNumber(lpTotalSupply, formatDecimals)} LP</Typography>
</Box>
<Box width="100%" display="flex" justifyContent="space-between">
<Typography fontSize="12px" lineHeight="15px">Extra Balance:</Typography>
<Typography fontSize="12px" lineHeight="15px">~{formatNumber(estimatedAmountOut, formatDecimals)} LP</Typography>
</Box>
</Box>}
<TokenAllowanceGuard
spendAmount={new DecimalBigNumber(amountTop, balanceTop._decimals)}
tokenName={tokenNameTop}
owner={address}
spender={dexAddresses.router}
decimals={balanceTop._decimals}
approvalText={"Approve " + tokenNameTop}
approvalPendingText={"Approving..."}
connect={connect}
width="100%"
height="60px"
>
<TokenAllowanceGuard
spendAmount={new DecimalBigNumber(amountBottom, balanceBottom._decimals)}
tokenName={tokenNameBottom}
owner={address}
spender={dexAddresses.router}
decimals={balanceBottom._decimals}
approvalText={"Approve " + tokenNameBottom}
approvalPendingText={"Approving..."}
connect={connect}
width="100%"
height="60px"
>
<SecondaryButton
fullWidth
disabled={
address !== "" && (
(new DecimalBigNumber(amountTop, balanceTop._decimals)).eq(new DecimalBigNumber("0", 18)) ||
(new DecimalBigNumber(amountBottom, balanceBottom._decimals)).eq(new DecimalBigNumber("0", 18)) ||
balanceTop?.lt(new DecimalBigNumber(amountTop, balanceTop._decimals)) ||
balanceBottom?.lt(new DecimalBigNumber(amountBottom, balanceBottom._decimals)) ||
isPending
)
}
loading={isPending}
onClick={() => address === "" ? connect() : addLiquidityInner()}
>
{address === "" ?
"Connect"
:
pairAddress === "0x0000000000000000000000000000000000000000" ?
"Create Pool"
:
"Add Liquidity"
}
</SecondaryButton>
</TokenAllowanceGuard>
</TokenAllowanceGuard>
</Box>
)
}
export default PoolContainer;