ghost-dao-interface/src/containers/Governance/NewProposal.jsx
Uncle Fatso 718e9a7be4
app rev.7
Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
2026-02-19 17:37:33 +03:00

227 lines
11 KiB
JavaScript

import { useState, useMemo, useCallback, useEffect } from "react";
import ReactGA from "react-ga4";
import {
Box,
Container,
TableContainer,
Table,
TableRow,
TableBody,
TableHead,
TableCell,
Typography,
Link,
OutlinedInput,
InputLabel,
FormControl,
useMediaQuery,
useTheme
} from "@mui/material";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import ArrowUpIcon from "../../assets/icons/arrow-up.svg?react";
import { GHOST_GOVERNANCE_ADDRESSES, GHST_ADDRESSES } from "../../constants/addresses";
import Paper from "../../components/Paper/Paper";
import PageTitle from "../../components/PageTitle/PageTitle";
import { PrimaryButton, TertiaryButton } from "../../components/Button";
import { TokenAllowanceGuard } from "../../components/TokenAllowanceGuard/TokenAllowanceGuard";
import { useTokenSymbol, useBalance } from "../../hooks/tokens";
import { useProposalThreshold, useProposalHash, propose } from "../../hooks/governance";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import ProposalModal from "./components/ProposalModal";
import { parseFunctionCalldata } from "./components/functions/index";
import { MY_PROPOSALS_PREFIX } from "./helpers";
const NewProposal = ({ config, address, connect, chainId }) => {
const isSemiSmallScreen = useMediaQuery("(max-width: 745px)");
const isSmallScreen = useMediaQuery("(max-width: 650px)");
const isVerySmallScreen = useMediaQuery("(max-width: 379px)");
const theme = useTheme();
const myStoredProposals = localStorage.getItem(`${MY_PROPOSALS_PREFIX}-${address}`);
const [myProposals, setMyProposals] = useState(
myStoredProposals ? JSON.parse(myStoredProposals).map(id => BigInt(id)) : []
);
const [isPending, setIsPending] = useState(false);
const [isModalOpened, setIsModalOpened] = useState(false);
const [proposalFunctions, setProposalFunctions] = useState([]);
const { symbol: ftsoSymbol } = useTokenSymbol(chainId, "FTSO");
const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST");
const { balance: ghstBalance } = useBalance(chainId, ghstSymbol, address)
const { threshold } = useProposalThreshold(chainId, ghstSymbol);
const {
proposalHash,
proposalDescription
} = useProposalHash(chainId, proposalFunctions);
useEffect(() => {
const toStore = JSON.stringify(myProposals.map(id => id.toString()));
localStorage.setItem(`${MY_PROPOSALS_PREFIX}-${address}`, toStore);
}, [myProposals]);
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: "/governance/create" });
}, []);
const addCalldata = (calldata) => setProposalFunctions(prev => [...prev, calldata]);
const removeCalldata = (index) => setProposalFunctions(prev => prev.filter((_, i) => i !== index));
const storeProposal = (proposalId) => setMyProposals(prev => [...prev, proposalId]);
const removeProposal = (proposalId) => setMyProposals(prev => prev.filter(item => item !== proposalId));
const nativeCurrency = useMemo(() => {
const client = config?.getClient();
return client?.chain?.nativeCurrency?.symbol;
}, [config]);
const submitProposal = useCallback(async () => {
setIsPending(true);
const result = await propose(chainId, address, proposalFunctions, proposalDescription);
if (result) {
storeProposal(proposalHash);
setProposalFunctions([]);
}
setIsPending(false);
}, [chainId, address, proposalHash, proposalFunctions, proposalDescription]);
return (
<>
<ProposalModal
nativeCurrency={nativeCurrency}
ftsoSymbol={ftsoSymbol}
chainId={chainId}
addCalldata={addCalldata}
isOpened={isModalOpened}
closeModal={() => setIsModalOpened(false)}
/>
<Box>
<PageTitle name="Create Proposal" subtitle="Submit your proposal to strengthen the DAO" />
<Container
style={{
paddingLeft: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
paddingRight: isSmallScreen || isVerySmallScreen ? "0" : "3.3rem",
minHeight: "calc(100vh - 128px)",
display: "flex",
flexDirection: "column",
justifyContent: "center"
}}
>
<Box sx={{ mt: "15px" }}>
<Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Proposal Functions
</Typography>
</Box>
}
topRight={
<PrimaryButton variant="text" href="http://ghostchain.io/governance">
Explore Governance
</PrimaryButton>
}
>
<Box>
<Box>
{proposalFunctions.length === 0 && <Box
width="100%"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
marginBottom="50px"
marginTop="25px"
>
<Typography variant="subtitle1">
Create new proposal by adding one or more of the functions below.
</Typography>
</Box>}
</Box>
<Box display="flex" flexDirection="column" alignItems="center">
<Box sx={{ width: isSemiSmallScreen ? "100%" : "350px" }}>
<TokenAllowanceGuard
spendAmount={threshold}
tokenName={ghstSymbol}
owner={address}
spender={GHOST_GOVERNANCE_ADDRESSES[chainId]}
decimals={ghstBalance._decimals}
approvalText={`Approve ${ghstSymbol}`}
approvalPendingText={"Approving..."}
connect={connect}
isVertical
>
<Box display="flex" flexDirection="column" alignItems="center">
<TertiaryButton
sx={{ maxWidth: isSemiSmallScreen ? "100%" : "350px" }}
fullWidth
disabled={isPending}
onClick={() => setIsModalOpened(true)}
>
Add New Function
</TertiaryButton>
<PrimaryButton
disabled={
proposalFunctions.length === 0 ||
ghstBalance.lt(threshold) ||
isPending
}
fullWidth
onClick={() => submitProposal()}
>
{isPending ? "Submitting..." : "Submit Proposal"}
</PrimaryButton>
</Box>
</TokenAllowanceGuard>
</Box>
</Box>
</Box>
</Paper>
{proposalFunctions.length > 0 && <Paper
fullWidth
enableBackground
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Proposal Functions
</Typography>
</Box>
}
>
<TableContainer>
<Table aria-label="Available bonds" style={{ tableLayout: "fixed" }}>
<TableHead>
<TableRow>
<TableCell align="center" style={{ padding: "8px 0" }}>Function</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Target</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Calldata</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>{proposalFunctions.map((metadata, index) => {
return parseFunctionCalldata(metadata, index, chainId, nativeCurrency, removeCalldata);
})}</TableBody>
</Table>
</TableContainer>
</Paper>}
</Box>
</Container>
</Box>
</>
)
}
export default NewProposal;