ghost-dao-interface/src/containers/History/History.jsx
Uncle Fatso 42d5f82040
bridge-in flow finished based on new weaver logic
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-11-06 17:23:41 +03:00

453 lines
20 KiB
JavaScript

import {
Box,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
Container,
FormControlLabel,
FormControl,
Checkbox,
Link,
useMediaQuery,
useTheme,
} from "@mui/material";
import { useLocation } from "react-router-dom";
import { useEffect, useState, useMemo, useCallback } from "react";
import useSWR from 'swr';
import ReactGA from "react-ga4";
import SettingsIcon from '@mui/icons-material/Settings';
import Modal from "../../components/Modal/Modal";
import Paper from "../../components/Paper/Paper";
import PageTitle from "../../components/PageTitle/PageTitle";
import TokenStack from "../../components/TokenStack/TokenStack";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import Chip from "../../components/Chip/Chip";
import { PrimaryButton, TertiaryButton } from "../../components/Button";
import { Tab, Tabs } from "../../components/Tabs/Tabs";
import { useTokenSymbol } from "../../hooks/tokens";
import { useLocalStorage } from "../../hooks/localstorage";
import { useUnstableProvider } from "../../hooks/ghost";
import { shorten } from "../../helpers";
import { networkAvgBlockSpeed } from "../../constants";
import EraseModal from "./EraseModal";
import SettingsModal from "./SettingsModal";
import DexViewModal from "./DexViewModal";
import StakingViewModal from "./StakingViewModal";
import BridgeInModal from "./BridgeInModal";
const HistoryItem = ({ item, chainId, config, currentTime, eraseRecord, setViewModalOpen, isSmallScreen, finalityDelay }) => {
const theme = useTheme();
const isVerySmallScreen = useMediaQuery("(max-width: 450px)");
const localString = new Date(item.submitted).toLocaleString();
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO")
const chainInfo = useMemo(() => {
const client = config?.getClient();
return {
url: client?.chain?.blockExplorers?.default?.url,
symbol: client?.chain?.nativeCurrency?.symbol,
};
}, [config]);
const chipStatus = useMemo(() => {
const delay = Number(finalityDelay * networkAvgBlockSpeed(chainId)) * 1000;
const width = isVerySmallScreen ? "21px" : "100px";
if (item.submitted + delay > currentTime) {
return <Chip sx={{ width: width }} label={isVerySmallScreen ? "" : "Pending"} template={"gray"} />
}
return <Chip sx={{ width: width }} label={isVerySmallScreen ? "" : "Ready"} template={"success"} />
}, [item, currentTime, isVerySmallScreen, finalityDelay])
if (isSmallScreen || isVerySmallScreen) {
return (
<>
<Box display="flex" flexDirection="column" gap="10px">
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
flexDirection={isVerySmallScreen ? "row" : "row"}
>
<Box display="flex" gap="20px" p="10px 0">
<TokenStack chainTokenNames={[chainInfo.symbol]} tokens={["FTSO"]} />
<Box display="flex" flexDirection="column">
<Typography>{localString}</Typography>
<Link
href={`${chainInfo.url}/tx/${item.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{shorten(item.txHash)}</Typography>
</Link>
</Box>
</Box>
<Box display="flex" flexDirection="column" alignItems="flex-end">
<Typography>{item?.name ?? "Unknown"}</Typography>
{chipStatus}
</Box>
</Box>
<Box display="flex" flexDirection="row" gap="20px">
<TertiaryButton fullWidth onClick={eraseRecord}>Erase</TertiaryButton>
<TertiaryButton fullWidth onClick={setViewModalOpen}>View</TertiaryButton>
</Box>
</Box>
</>
)
}
return (
<TableRow>
<TableCell sx={{ padding: "10px 0" }}>
<Box display="flex" alignItems="center">
<TokenStack chainTokenNames={["ETH"]} tokens={["FTSO"]} />
<Box ml="10px" display="flex" flexDirection="column" alignItems="flex-start">
<Typography>{item?.name ?? "Unknown"}</Typography>
<Link
href={`${chainInfo.url}/tx/${item.txHash}`}
target="_blank"
rel="noopener noreferrer"
>
<Typography>{shorten(item.txHash, 7, -5)}</Typography>
</Link>
</Box>
</Box>
</TableCell>
<TableCell align="center" sx={{ padding: "10px 0" }}>
{chipStatus}
</TableCell>
<TableCell sx={{ padding: "10px 0" }}>
<Typography>{localString}</Typography>
</TableCell>
<TableCell sx={{ padding: "10px 0" }}>
<Box display="flex" flexDirection="row" gap="20px">
<TertiaryButton fullWidth onClick={eraseRecord}>Erase</TertiaryButton>
<TertiaryButton fullWidth onClick={setViewModalOpen}>View</TertiaryButton>
</Box>
</TableCell>
</TableRow>
)
}
const HistoryList = ({ chainId, storedTransactions, historyType, config, eraseRecord }) => {
const isSmallScreen = useMediaQuery("(max-width: 900px)");
const [currentTime, setCurrentTime] = useState(Date.now());
const [viewModalOpen, setViewModalOpen] = useState(null);
const { CONSTANTS } = useLocalStorage();
const { casperApi } = useUnstableProvider();
const { data: networkData, error, isLoading } = useSWR(
casperApi ? ['networkData', casperApi, chainId] : null,
async () => {
const key = BigInt(chainId);
const res = await casperApi.query.GhostNetworks.Networks.getValue(key);
return res;
},
{
revalidateOnFocus: true
}
);
const finalityDelay = useMemo(() => {
return networkData ? networkData.finality_delay : 420n;
}, [networkData]);
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentTime(Date.now());
}, 1000);
return () => clearInterval(intervalId);
}, []);
const historyContainer = useMemo(() => {
if (isSmallScreen) {
return (
<Box display="flex" flexDirection="column" gap="50px">
{storedTransactions?.map((item, i) => {
return (
<HistoryItem
key={i}
item={item}
config={config}
chainId={chainId}
currentTime={currentTime}
finalityDelay={finalityDelay}
eraseRecord={() => eraseRecord(i)}
setViewModalOpen={() => setViewModalOpen(i)}
isSmallScreen={isSmallScreen}
/>
)
})}
</Box>
)
} else {
return (
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell style={{ padding: "8px 0" }}>Transaction</TableCell>
<TableCell style={{ padding: "8px 0" }} align="center">Status</TableCell>
<TableCell style={{ padding: "8px 0" }}>Submitted</TableCell>
<TableCell style={{ padding: "8px 0" }}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{storedTransactions?.map((item, i) => {
return (
<HistoryItem
key={i}
item={item}
config={config}
currentTime={currentTime}
finalityDelay={finalityDelay}
eraseRecord={() => eraseRecord(i)}
setViewModalOpen={() => setViewModalOpen(i)}
isSmallScreen={isSmallScreen}
/>
)
})}
</TableBody>
</Table>
</TableContainer>
)
}
}, [isSmallScreen, storedTransactions, config]);
return (
<>
{viewModalOpen !== null && historyType === CONSTANTS.dex.key && (
<DexViewModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{viewModalOpen !== null && historyType === CONSTANTS.stake.key && (
<StakingViewModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{viewModalOpen !== null && historyType === CONSTANTS.bridgeIn.key && (
<BridgeInModal
chainId={chainId}
config={config}
close={() => setViewModalOpen(null)}
isOpen={true}
historyRecord={storedTransactions.at(viewModalOpen)}
/>
)}
{historyContainer}
</>
)
}
const History = ({ chainId, config, tryConnectInjected, address }) => {
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const [historyTypeIndex, setHistoryTypeIndex] = useState(0);
const [erasingIndex, setErasingIndex] = useState(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const [erasingOpen, setErasingOpen] = useState(false);
const location = useLocation();
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: location.pathname });
}, [location]);
const {
CONSTANTS,
useStorageValue,
setStorageValue,
clearPendingHistory
} = useLocalStorage();
const { data: storedHistoryTypes } = useStorageValue(chainId, address, CONSTANTS.historyPrefix, CONSTANTS.defaultHistoryTypes);
const { data: storedTransactions } = useStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []);
const { data: pendingHistory } = useStorageValue(chainId, address, CONSTANTS.pendingHistoryPrefix, {});
const handleHistoryTypesChange = useCallback((values) => {
setStorageValue(chainId, address, CONSTANTS.historyPrefix, values);
}, [chainId, address]);
const handleConfirmErase = useCallback(() => {
if (erasingIndex !== null) {
const updated = storedTransactions.filter((_, index) => index !== erasingIndex);
setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], updated);
} else {
setStorageValue(chainId, address, storedHistoryTypes[historyTypeIndex], []);
}
setErasingOpen(false);
setErasingIndex(null);
}, [storedTransactions, erasingIndex, chainId, address, historyTypeIndex]);
useEffect(() => {
clearPendingHistory(chainId, address, storedHistoryTypes.at(historyTypeIndex));
}, [storedHistoryTypes, historyTypeIndex])
return (
<Box minHeight="calc(100vh - 43px)">
<PageTitle name="History" subtitle="A complete record of your activity on the ghostDAO protocol" />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
}}
>
<EraseModal
key="EraseModal"
isOpen={erasingOpen}
close={() => {
setErasingOpen(false);
setErasingIndex(null);
}}
submit={handleConfirmErase}
isAll={erasingIndex === null}
/>
<SettingsModal
key="SettingsModal"
isOpen={settingsOpen}
close={() => setSettingsOpen(false)}
onChange={handleHistoryTypesChange}
selectedValues={storedHistoryTypes}
possibleValues={CONSTANTS.possibleHistoryTypes}
/>
<Box mt="15px">
<Paper
fullWidth
enableBackground
topRight={
<Box style={{ position: "absolute", top: "28px", right: "20px", zIndex: 10 }}>
<GhostStyledIcon
component={SettingsIcon}
viewBox="0 0 23 23"
style={{ display: "flex", cursor: "pointer" }}
onClick={() => setSettingsOpen(true)}
/>
</Box>
}
headerContent={
<Box
width="100%"
display="flex"
flexDirection="column"
justifyContent="space-between"
alignItems="center"
gap="15px"
>
<Tabs
centered={!isSemiSmallScreen}
orientation={isSemiSmallScreen ? "vertical" : "horizontal"}
textColor="primary"
indicatorColor="primary"
value={storedHistoryTypes[historyTypeIndex]}
aria-label="History menu"
onChange={(_, selectedValue) => {
const index = storedHistoryTypes.indexOf(selectedValue);
if (index !== -1) setHistoryTypeIndex(index);
}}
TabIndicatorProps={{ style: { display: "none" } }}
>
{storedHistoryTypes.map((type, i) => {
return (
<Tab
key={i}
value={type}
aria-label={`history-${i}-button`}
style={{ fontSize: "1.2rem" }}
label={
<Box display="flex" alignItems="center" gap="5px">
<Typography variant="h6">{type}</Typography>
{pendingHistory && pendingHistory[type] > 0 && (
<Chip
template="warning"
strong={true}
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
height: "16px",
minWidth: "16px",
width: "auto",
fontSize: "8px",
borderRadius: "100%"
}}
sx={{
"& .MuiChip-label": {
padding: 0,
lineHeight: "16px",
display: "block"
}
}}
label={pendingHistory[type]}
/>
)}
</Box>
}
/>
)
})}
</Tabs>
<PrimaryButton
fullWidth
onClick={() => setErasingOpen(true)}
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
>
Erase All Records
</PrimaryButton>
</Box>
}
>
<HistoryList
chainId={chainId}
config={config}
eraseRecord={(index) => {
setErasingIndex(index);
setErasingOpen(true);
}}
historyType={storedHistoryTypes[historyTypeIndex]}
storedTransactions={storedTransactions}
/>
</Paper>
</Box>
</Container>
</Box>
);
}
export default History;