227 lines
11 KiB
JavaScript
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;
|