ghost-dao-interface/src/containers/Dex/Dex.jsx
Uncle Fatso 8d23d55ae2
integrate native coin into dex
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-03-19 20:48:38 +03:00

443 lines
20 KiB
JavaScript

import {
Box,
Container,
Divider,
Typography,
InputLabel,
FormControl,
OutlinedInput,
useMediaQuery,
useTheme,
} from "@mui/material";
import SettingsIcon from '@mui/icons-material/Settings';
import { useEffect, useMemo, useState } from "react";
import { useParams, useLocation, useSearchParams } from "react-router-dom";
import { Helmet } from "react-helmet";
import ReactGA from "react-ga4";
import InfoTooltip from "../../components/Tooltip/InfoTooltip";
import Modal from "../../components/Modal/Modal";
import PageTitle from "../../components/PageTitle/PageTitle";
import Paper from "../../components/Paper/Paper";
import SwapCard from "../../components/Swap/SwapCard";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import { Tab, Tabs } from "../../components/Tabs/Tabs";
import {
UNISWAP_V2_ROUTER,
UNISWAP_V2_FACTORY,
RESERVE_ADDRESSES,
FTSO_ADDRESSES,
EMPTY_ADDRESS,
WETH_ADDRESSES,
} from "../../constants/addresses";
import { useTokenSymbol } from "../../hooks/tokens";
import { getTokenAddress } from "../../hooks/helpers";
import PoolContainer from "./PoolContainer";
import SwapContainer from "./SwapContainer";
import TokenModal from "./TokenModal";
const Dex = ({ chainId, address, connect, config }) => {
const location = useLocation();
const pathname = useParams();
const theme = useTheme();
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters = new URLSearchParams();
const [isSwap, setIsSwap] = useState(false);
const [settingsOpen, handleSettingsOpen] = useState(false);
const [topTokenListOpen, setTopTokenListOpen] = useState(false);
const [bottomTokenListOpen, setBottomTokenListOpen] = useState(false);
const [secondsToWait, setSecondsToWait] = useState(localStorage.getItem("dex-deadline") || "60");
const [slippage, setSlippage] = useState(localStorage.getItem("dex-slippage") || "5");
const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("dex-decimals") || "5");
const [actualDestinationAddress, setActualDestinationAddress] = useState(localStorage.getItem("dex-destination"));
const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress);
const [tokenAddressTop, setTokenAddressTop] = useState(EMPTY_ADDRESS);
const [tokenAddressBottom, setTokenAddressBottom] = useState(FTSO_ADDRESSES[chainId]);
const { symbol: tokenNameTopInner } = useTokenSymbol(chainId, tokenAddressTop);
const { symbol: tokenNameBottomInner } = useTokenSymbol(chainId, tokenAddressBottom);
const chainSymbol = useMemo(() => {
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
if (chainSymbol) return chainSymbol;
return "WTF";
}, [config])
const tokenNameTop = useMemo(() => {
if (chainSymbol && tokenAddressTop === EMPTY_ADDRESS) {
return chainSymbol;
}
return tokenNameTopInner;
}, [tokenAddressTop, tokenNameTopInner, chainSymbol]);
const tokenNameBottom = useMemo(() => {
const chainSymbol = config?.getClient()?.chain?.nativeCurrency?.symbol;
if (chainSymbol && tokenAddressBottom === EMPTY_ADDRESS) {
return chainSymbol;
}
return tokenNameBottomInner;
}, [tokenAddressBottom, tokenNameBottomInner, config]);
const isWrapping = useMemo(() => {
const isNative = tokenAddressTop === EMPTY_ADDRESS;
const isWrappedNative = tokenAddressBottom === WETH_ADDRESSES[chainId];
return isNative && isWrappedNative;
}, [chainId, tokenAddressTop, tokenAddressBottom]);
const isUnwrapping = useMemo(() => {
const isWrappedNative = tokenAddressTop === WETH_ADDRESSES[chainId];
const isNative = tokenAddressBottom === EMPTY_ADDRESS;
return isNative && isWrappedNative;
}, [chainId, tokenAddressTop, tokenAddressBottom]);
useEffect(() => {
if (currentQueryParameters.has("pool")) {
setIsSwap(false);
newQueryParameters.set("pool", true);
} else {
setIsSwap(true);
newQueryParameters.delete("pool");
}
if (currentQueryParameters.has("from")) {
setTokenAddressTop(currentQueryParameters.get("from"));
newQueryParameters.set("from", currentQueryParameters.get("from"));
} else {
setTokenAddressTop(EMPTY_ADDRESS);
newQueryParameters.set("from", EMPTY_ADDRESS);
}
if (currentQueryParameters.has("to")) {
setTokenAddressBottom(currentQueryParameters.get("to"));
newQueryParameters.set("to", currentQueryParameters.get("to"));
} else {
setTokenAddressBottom(FTSO_ADDRESSES[chainId]);
newQueryParameters.set("to", FTSO_ADDRESSES[chainId]);
}
setSearchParams(newQueryParameters)
}, [currentQueryParameters])
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname + location.search });
}, [location]);
const dexAddresses = {
router: UNISWAP_V2_ROUTER[chainId],
factory: UNISWAP_V2_FACTORY[chainId],
}
const onCardsSwap = () => {
const tmpFrom = currentQueryParameters.get("from");
const tmpTo = currentQueryParameters.get("to");
if (currentQueryParameters.has("pool")) newQueryParameters.set("pool", true);
else newQueryParameters.delete("pool");
newQueryParameters.set("from", tmpTo);
newQueryParameters.set("to", tmpFrom);
setSearchParams(newQueryParameters);
}
const changeSwapTab = (swap) => {
if (swap || (isWrapping || isUnwrapping)) newQueryParameters.delete("pool");
else newQueryParameters.set("pool", true);
newQueryParameters.set("from", currentQueryParameters.get("from"));
newQueryParameters.set("to", currentQueryParameters.get("to"));
setSearchParams(newQueryParameters);
}
const setInnerTokenAddressTop = (tokenAddress) => {
if (currentQueryParameters.has("pool")) newQueryParameters.set("pool", true);
else newQueryParameters.delete("pool");
if (currentQueryParameters.get("to") === tokenAddress) {
newQueryParameters.set("from", currentQueryParameters.get("from"));
} else newQueryParameters.set("from", tokenAddress);
newQueryParameters.set("to", currentQueryParameters.get("to"));
setSearchParams(newQueryParameters);
}
const setInnerTokenAddressBottom = (tokenAddress) => {
if (currentQueryParameters.has("pool")) newQueryParameters.set("pool", true);
else newQueryParameters.delete("pool");
newQueryParameters.set("from", currentQueryParameters.get("from"));
if (currentQueryParameters.get("from") === tokenAddress) {
newQueryParameters.set("to", currentQueryParameters.get("to"));
} else newQueryParameters.set("to", tokenAddress);
setSearchParams(newQueryParameters);
}
const setSlippageInner = (value) => {
const maybeValue = parseFloat(value);
if (!maybeValue || parseFloat(value) <= 100) {
setSlippage(value);
localStorage.setItem("dex-slippage", value);
}
}
const setSecondsToWaitInner = (value) => {
localStorage.setItem("dex-deadline", value);
setSecondsToWait(value);
}
const setFormatDecimalsInner = (value) => {
if (Number(value) <= 17) {
localStorage.setItem("dex-decimals", value);
setFormatDecimals(value);
}
}
const setDestinationAddressInner = (value) => {
const cleanedValue = value.trim();
const isEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(cleanedValue);
if (isEvmAddress) {
localStorage.setItem("dex-destination", value);
setActualDestinationAddress(value);
} else if (!isEvmAddress && actualDestinationAddress) {
localStorage.removeItem("dex-destination");
setActualDestinationAddress(undefined);
}
setDestinationAddress(value);
}
const handleCloseSetting = () => {
setDestinationAddress(undefined);
handleSettingsOpen(false);
}
return (
<Box height="calc(100vh - 43px)">
<Helmet>
<title>ghostSwap | The pure web3 legacy v2 swap</title>
<meta name="description" content="ghostSwap carries the legacy of V2 DEX interfaces - no KYC, no bloatware, just pure decentralized swapping. Enjoy gas-efficient trades, deep liquidity, and full self-custody, enabling token swaps as DeFi intended." />
<meta name="keywords" content="ghostSwap, Uniswap, Uniswap V2, Sushiswap, Sushiswap v2, Pancakeswap, Pancakeswap v2, Bancor, DEX, swap, web3, LP, liquidity provider, decentralized exchange, legacy v2, legacy v2 swap" />
<meta property="og:image" content="https://ghostchain.io/wp-content/uploads/2025/03/ghostSwap-Featured_Image.png" />
<meta property="og:title" content="ghostDAO | The DeFi 2.0 cross-chain reserve currency" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:type" content="website" />
<meta property="og:description" content="ghostSwap carries the legacy of V2 DEX interfaces - no KYC, no bloatware, just pure decentralized swapping. Enjoy gas-efficient trades, deep liquidity, and full self-custody, enabling token swaps as DeFi intended." />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@realGhostChain" />
<meta name="twitter:title" content="ghostDAO | The DeFi 2.0 cross-chain reserve currency" />
<meta name="twitter:description" content="ghostSwap carries the legacy of V2 DEX interfaces - no KYC, no bloatware, just pure decentralized swapping. Enjoy gas-efficient trades, deep liquidity, and full self-custody, enabling token swaps as DeFi intended." />
<meta name="twitter:image" content="https://ghostchain.io/wp-content/uploads/2025/03/ghostSwap-Featured_Image.png" />
</Helmet>
<PageTitle
name={`${pathname.name.charAt(0).toUpperCase() + pathname.name.slice(1).toLowerCase()} V2 Classic`}
subtitle="Swap via Uniswap V2 Fork"
/>
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
height: "calc(100vh - 153px)",
display: "flex",
justifyContent: "center",
alignItems: "center"
}}
>
<Modal
maxWidth="376px"
minHeight="200px"
open={settingsOpen}
headerText={"Settings"}
onClose={() => handleCloseSetting()}
>
<Box>
<InputLabel htmlFor="slippage">Slippage</InputLabel>
<Box mt="8px">
<FormControl variant="outlined" color="primary" fullWidth>
<OutlinedInput
inputProps={{ "data-testid": "slippage-dex" }}
type="text"
id="slippage-dex"
value={slippage}
onChange={event => setSlippageInner(event.currentTarget.value)}
endAdornment="%"
/>
</FormControl>
</Box>
<Box mt="8px">
<Typography variant="body2" color="textSecondary">
Transaction may revert if price changes by more than slippage %
</Typography>
</Box>
</Box>
<Box mt="32px">
<InputLabel htmlFor="recipient">Transaction deadline</InputLabel>
<Box mt="8px">
<FormControl variant="outlined" color="primary" fullWidth>
<OutlinedInput
inputProps={{ "data-testid": "seconds-to-wait" }}
type="number"
id="seconds-to-wait"
value={secondsToWait}
onChange={event => setSecondsToWaitInner(event.currentTarget.value)}
endAdornment="seconds"
/>
</FormControl>
</Box>
<Box mt="8px">
<Typography variant="body2" color="textSecondary">
How long transaction is valid in the transaction pool
</Typography>
</Box>
</Box>
<Box mt="32px">
<InputLabel htmlFor="recipient">Decimal representation</InputLabel>
<Box mt="8px">
<FormControl variant="outlined" color="primary" fullWidth>
<OutlinedInput
inputProps={{ "data-testid": "decimals-to-wait" }}
type="number"
id="decimals-to-wait"
value={formatDecimals}
onChange={event => setFormatDecimalsInner(event.currentTarget.value)}
endAdornment="decimals"
/>
</FormControl>
</Box>
<Box mt="8px">
<Typography variant="body2" color="textSecondary">
Number of decimals to be shown in token balances
</Typography>
</Box>
</Box>
<Box mt="32px">
<InputLabel htmlFor="recipient">
{`${actualDestinationAddress ? "Custom" : "Default"} destination address`}
</InputLabel>
<Box mt="8px">
<FormControl variant="outlined" color="primary" fullWidth>
<OutlinedInput
inputProps={{ "data-testid": "decimals-to-wait" }}
type="text"
id="destination-to-wait"
value={destinationAddress
? destinationAddress
: actualDestinationAddress ?? address
}
onChange={event => setDestinationAddressInner(event.currentTarget.value)}
/>
</FormControl>
</Box>
<Box mt="8px">
<Typography variant="body2" color="textSecondary">
Recipient address of swapped assets and liquidity tokens
</Typography>
</Box>
</Box>
</Modal>
<TokenModal
chainSymbol={chainSymbol}
account={address}
chainId={chainId}
listOpen={topTokenListOpen}
setListOpen={setTopTokenListOpen}
setTokenAddress={setInnerTokenAddressTop}
/>
<TokenModal
chainSymbol={chainSymbol}
account={address}
chainId={chainId}
listOpen={bottomTokenListOpen}
setListOpen={setBottomTokenListOpen}
setTokenAddress={setInnerTokenAddressBottom}
/>
<Box sx={{ width: "100%", maxWidth: "420px", mt: "15px" }}>
<Paper
fullWidth
enableBackground
topRight={
<GhostStyledIcon
component={SettingsIcon}
viewBox="0 0 23 23"
style={{ display: "flex", alignItems: "center", cursor: "pointer" }}
onClick={handleSettingsOpen}
/>
}
headerContent={
<Tabs
centered
textColor="primary"
indicatorColor="primary"
value={isSwap ? 0 : 1}
aria-label="Dex swap menu"
onChange={(_, view) => changeSwapTab(view === 0)}
TabIndicatorProps={{ style: { display: "none" } }}
>
<Tab aria-label="dex-swap-button" label="Swap" style={{ fontSize: "1.5rem" }} />
<Tab aria-label="dex-add-button" label="Pool" style={{ fontSize: "1.5rem" }} />
</Tabs>
}
>
<Box height="350px">
{isSwap ?
<SwapContainer
tokenNameTop={tokenNameTop}
tokenNameBottom={tokenNameBottom}
onCardsSwap={onCardsSwap}
chainId={chainId}
address={address}
dexAddresses={dexAddresses}
connect={connect}
slippage={slippage}
destination={actualDestinationAddress ? actualDestinationAddress : address}
secondsToWait={secondsToWait}
setTopTokenListOpen={setTopTokenListOpen}
setBottomTokenListOpen={setBottomTokenListOpen}
setIsSwap={setIsSwap}
formatDecimals={formatDecimals}
isWrapping={isWrapping}
isUnwrapping={isUnwrapping}
/>
:
<PoolContainer
tokenNameTop={tokenNameTop}
tokenNameBottom={tokenNameBottom}
onCardsSwap={onCardsSwap}
chainId={chainId}
address={address}
dexAddresses={dexAddresses}
connect={connect}
slippage={slippage}
destination={actualDestinationAddress ? actualDestinationAddress : address}
secondsToWait={secondsToWait}
setTopTokenListOpen={setTopTokenListOpen}
setBottomTokenListOpen={setBottomTokenListOpen}
formatDecimals={formatDecimals}
/>
}
</Box>
</Paper>
</Box>
</Container>
</Box>
)
}
export default Dex;