453 lines
20 KiB
JavaScript
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;
|