new functionality for governance added

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2026-02-10 20:36:19 +03:00
parent 056177c34b
commit 56c5616d96
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
16 changed files with 1082 additions and 356 deletions

View File

@ -1,7 +1,7 @@
{
"name": "ghost-dao-interface",
"private": true,
"version": "0.5.7",
"version": "0.5.8",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -0,0 +1,72 @@
import { Box, MenuItem, Select as MuiSelect, Typography, useTheme } from "@mui/material";
import { styled } from "@mui/material/styles";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
const StyledSelectInput = styled(MuiSelect, {
shouldForwardProp: prop => prop !== "inputWidth"
})(({ theme, inputWidth }) => ({
width: "100%",
"& .MuiSelect-select": {
padding: 0,
height: "24px !important",
minHeight: "24px !important",
display: "flex",
alignItems: "center",
fontSize: "18px",
fontWeight: 500,
paddingRight: "24px",
},
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
"& .MuiSelect-icon": {
right: 0,
position: "absolute",
color: theme.colors.gray[500],
}
}));
const Select = ({
label,
value,
onChange,
options,
inputWidth,
width = "100%",
}) => {
const theme = useTheme();
return (
<Box
display="flex"
flexDirection="column"
width={width}
sx={{ backgroundColor: theme.colors.gray[750] }}
borderRadius="12px"
padding="15px"
>
{label && (
<Typography color={theme.colors.gray[500]} fontSize="12px" marginBottom="8px">
{label}
</Typography>
)}
<Box display="flex" alignItems="center" justifyContent="space-between">
<StyledSelectInput
value={value}
onChange={onChange}
inputWidth={inputWidth}
IconComponent={KeyboardArrowDownIcon}
displayEmpty
>
{options.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</StyledSelectInput>
</Box>
</Box>
);
};
export default Select;

View File

@ -7,7 +7,7 @@ import Token from "../Token/Token";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })(
export const StyledInputBase = styled(InputBase, { shouldForwardProp: prop => prop !== "inputWidth" })(
({ inputWidth, inputFontSize }) => ({
"& .MuiInputBase-input": {
padding: 0,

View File

@ -1,4 +1,4 @@
import { Box, Tab, Tabs, Container, useMediaQuery } from "@mui/material";
import { Box, Tab, Tabs, Typography, Container, useMediaQuery } from "@mui/material";
import { useEffect, useState } from "react";
import ReactGA from "react-ga4";

View File

@ -1,27 +1,59 @@
import { useState } from "react";
import { useState, useMemo } from "react";
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 Paper from "../../components/Paper/Paper";
import PageTitle from "../../components/PageTitle/PageTitle";
import { PrimaryButton } from "../../components/Button";
import { PrimaryButton, TertiaryButton } from "../../components/Button";
import ProposalModal from "./components/ProposalModal";
import { parseFunctionCalldata } from "./components/functions/index";
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 [descriptionUrl, setDescriptionUrl] = useState("");
const theme = useTheme();
const [isModalOpened, setIsModalOpened] = useState(false);
const [proposalFunctions, setProposalFunctions] = useState([]);
const addCalldata = (calldata) => setProposalFunctions(prev => [...prev, calldata]);
const removeCalldata = (index) => setProposalFunctions(prev => prev.filter((_, i) => i !== index));
const nativeCurrency = useMemo(() => {
const client = config?.getClient();
return client?.chain?.nativeCurrency?.symbol;
}, [config]);
const submitProposal = () => {
alert("Proposal created");
setProposalFunctions([]);
}
return (
<>
<ProposalModal addCalldata={addCalldata} isOpened={isModalOpened} closeModal={() => setIsModalOpened(false)} />
<Box>
<PageTitle name="Create Proposal" subtitle="Cool text describing what's goinf on there" />
<Container
@ -41,35 +73,81 @@ const NewProposal = ({ config, address, connect, chainId }) => {
headerContent={
<Box display="flex" alignItems="center" flexDirection="row" gap="5px">
<Typography variant="h6">
Proposal Overview
Proposal Functions
</Typography>
</Box>
}
>
<BasicInput id="discussion" label="Discussion URL" value={descriptionUrl} eventHandler={setDescriptionUrl} />
<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 functions below
</Typography>
<Link
color={theme.colors.primary[300]}
href="https://forum.ghostchain.io"
target="_blank"
rel="noopener noreferrer"
display="flex"
alignItems="center"
>
Learn more&nbsp;
<GhostStyledIcon
style={{ marginTop: "7px" }}
viewBox="0 0 30 30"
className="external-site-link-icon"
component={ArrowUpIcon}
/>
</Link>
</Box>}
</Box>
<Box>
<TertiaryButton fullWidth onClick={() => setIsModalOpened(true)}>Add New</TertiaryButton>
<PrimaryButton fullWidth onClick={() => submitProposal()}>Submit Proposal</PrimaryButton>
</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" }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>{proposalFunctions.map((calldata, index) => {
return parseFunctionCalldata(calldata, index, nativeCurrency, removeCalldata);
})}</TableBody>
</Table>
</TableContainer>
</Paper>}
</Box>
</Container>
</Box>
)
}
const BasicInput = ({ id, label, value, eventHandler }) => {
return (
<Box>
<InputLabel htmlFor={id}>{label}</InputLabel>
<Box mt="8px">
<FormControl variant="outlined" color="primary" fullWidth>
<OutlinedInput
inputProps={{ "data-testid": `${id}-dex` }}
type="text"
id={`${id}-dex`}
value={value}
onChange={event => eventHandler(event.currentTarget.value)}
/>
</FormControl>
</Box>
</Box>
</>
)
}

View File

@ -2,7 +2,7 @@ import { useEffect, useState, useMemo } from "react";
import ReactGA from "react-ga4";
import { useParams } from 'react-router-dom';
import { Box, Container, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Box, Container, Typography, Link, useMediaQuery, useTheme } from "@mui/material";
import Paper from "../../components/Paper/Paper";
import PageTitle from "../../components/PageTitle/PageTitle";
@ -11,12 +11,13 @@ import InfoTooltip from "../../components/Tooltip/InfoTooltip";
import Chip from "../../components/Chip/Chip";
import { SecondaryButton } from "../../components/Button";
import GhostStyledIcon from "../../components/Icon/GhostIcon";
import ArrowUpIcon from "../../assets/icons/arrow-up.svg?react";
import { formatNumber } from "../../helpers";
import { prettifySecondsInDays } from "../../helpers/timeUtil";
import { DecimalBigNumber } from "../../helpers/DecimalBigNumber";
import ProposalDiscussionModal from "./components/ProposalDiscussionModal";
import ProposalDiscussion from "./components/ProposalDiscussion";
import { convertStatusToTemplate } from "./helpers";
import { useTokenSymbol, useTotalSupply, useBalance } from "../../hooks/tokens";
@ -65,7 +66,7 @@ const ProposalDetails = ({ chainId, address }) => {
const { proposer: proposalProposer } = useProposalProposer(chainId, id);
const { locked: proposalLocked } = useProposalLocked(chainId, id);
const { quorum: proposalQuorum } = useProposalQuorum(chainId, id);
const { forVotes, againstVotes } = useProposalVotes(chainId, id);
const { forVotes, againstVotes, totalVotes } = useProposalVotes(chainId, id);
useEffect(() => {
ReactGA.send({ hitType: "pageview", page: `/governance/${id}` });
@ -75,13 +76,22 @@ const ProposalDetails = ({ chainId, address }) => {
return selectedDiscussionUrl !== undefined;
}, [selectedDiscussionUrl]);
const quorumPercentage = useMemo(() => {
if (totalSupply._value === 0n) return 0;
return proposalQuorum / totalSupply * HUNDRED;
}, [proposalQuorum, totalSupply]);
const votePercentage = useMemo(() => {
if (totalSupply._value === 0n) return 0;
return totalVotes / totalSupply * HUNDRED;
}, [totalVotes, totalSupply]);
const voteWeightPercentage = useMemo(() => {
if (totalSupply._value === 0n) return 0;
return balance / totalSupply * HUNDRED;
}, [balance, totalSupply]);
return (
<>
<ProposalDiscussionModal
url={selectedDiscussionUrl}
isOpened={isDiscussionModalOpened}
closeModal={() => setSelectedDiscussionUrl(undefined)}
/>
<Box>
<PageTitle name={`GBP: ${id} - NAME`} subtitle={`By: ${proposalProposer} | BONDED: $${proposalLocked} ${ghstSymbol}`} />
<Container
@ -112,16 +122,31 @@ const ProposalDetails = ({ chainId, address }) => {
</Box>
}
topRight={
<ProposalDiscussion onClick={() => setSelectedDiscussionUrl("dicks")} />
<Link
color={theme.colors.primary[300]}
href="https://forum.ghostchain.io"
target="_blank"
rel="noopener noreferrer"
display="flex"
alignItems="center"
>
View Forum&nbsp;
<GhostStyledIcon
style={{ marginTop: "7px" }}
viewBox="0 0 30 30"
className="external-site-link-icon"
component={ArrowUpIcon}
/>
</Link>
}
>
<Box height="200px" display="flex" flexDirection="column" justifyContent="space-between" gap="20px">
<Box display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between">
<Typography sx={{ textShadow: "0 0 black", fontWeight: 600 }} variant="body2" color={theme.colors.feedback.success}>
<Typography sx={{ textShadow: "0 0 black", fontWeight: 600 }} variant="body1" color={theme.colors.feedback.success}>
For: {formatNumber(forVotes.toString(), 2)} ({formatNumber(forVotes * HUNDRED / proposalQuorum, 1)}%)
</Typography>
<Typography sx={{ textShadow: "0 0 black", fontWeight: 600 }} variant="body2" color={theme.colors.feedback.error}>
<Typography sx={{ textShadow: "0 0 black", fontWeight: 600 }} variant="body1" color={theme.colors.feedback.error}>
Against: {formatNumber(againstVotes.toString(), 2)} ({formatNumber(againstVotes * HUNDRED / proposalQuorum, 1)}%)
</Typography>
</Box>
@ -138,25 +163,25 @@ const ProposalDetails = ({ chainId, address }) => {
<Box display="flex" justifyContent="space-between">
<Box display="flex" flexDirection="row">
<Typography>Quorum</Typography>
<InfoTooltip message="Minimum number of voting power required to be present to make proposal executable" />
<InfoTooltip message={`Minimum $${ghstSymbol} turnout required for the proposal to become valid, as percentage of the total $${ghstSymbol} supply at the time when proposal was created`} />
</Box>
<Typography>{formatNumber(proposalQuorum.toString(), 4)}</Typography>
<Typography>{formatNumber(proposalQuorum.toString(), 4)} ({formatNumber(quorumPercentage, 1)}%)</Typography>
</Box>
<Box display="flex" justifyContent="space-between">
<Box display="flex" flexDirection="row">
<Typography>Total</Typography>
<InfoTooltip message="Total number of votes available for proposal" />
<InfoTooltip message={`Total votes for the proposal, as percentage of the total $${ghstSymbol} supply at the time when proposal was created`}/>
</Box>
<Typography>{formatNumber(totalSupply.toString(), 4)}</Typography>
<Typography>{formatNumber(totalSupply.toString(), 4)} ({formatNumber(votePercentage, 1)}%)</Typography>
</Box>
<Box display="flex" justifyContent="space-between">
<Box display="flex" flexDirection="row">
<Typography>Votes</Typography>
<InfoTooltip message="Voting power of the connected wallet" />
<InfoTooltip message={`Your voting power, as percentage of total $${ghstSymbol} at the time when proposal was created`} />
</Box>
<Typography>{formatNumber(balance.toString(), 4)}</Typography>
<Typography>{formatNumber(balance.toString(), 4)} ({formatNumber(voteWeightPercentage, 1)}%)</Typography>
</Box>
</Box>
@ -198,7 +223,6 @@ const ProposalDetails = ({ chainId, address }) => {
</Paper>
</Container>
</Box>
</>
)
}

View File

@ -37,7 +37,7 @@ export const ProposalThreshold = props => {
const _props = {
...props,
label: `$${props.ghstSymbol} Threshold`,
label: "Min Collateral",
tooltip: `Minimum $${props.ghstSymbol} required to be locked to create a proposal`,
};
@ -56,7 +56,7 @@ export const ProposalsCount = props => {
tooltip: `Total proposals created`,
};
if (proposalsCount) _props.metric = proposalsCount.toString();
if (proposalsCount) _props.metric = `${formatNumber(proposalsCount.toString(), 0)}`;
else _props.isLoading = true;
return <Metric {..._props} />;

View File

@ -1,29 +0,0 @@
import { Link } from "@mui/material";
import GhostStyledIcon from "../../../components/Icon/GhostIcon";
import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
const ProposalDiscussion = (linkProps) => {
return (
<Link
{...linkProps}
underline="hover"
sx={{ fontFamily: "Ubuntu", fontSize: "14px", marginLeft: "9px" }}
display="flex"
flexDirection="row"
alignItems="center"
alignContent="center"
justifyContent={linkProps.isSmallScreen ? "start" : "center"}
className="link-container"
>
Learn more&nbsp;
<GhostStyledIcon
style={{ marginTop: "7px" }}
viewBox="0 0 30 30"
className="external-site-link-icon"
component={ArrowUpIcon}
/>
</Link>
)
}
export default ProposalDiscussion;

View File

@ -1,64 +0,0 @@
import { useState } from "react";
import { Box, Typography, Link } from "@mui/material";
import Modal from "../../../components/Modal/Modal";
import { PrimaryButton } from "../../../components/Button";
const ProposalDiscussionModal = ({ isOpened, closeModal, url }) => {
const [isCopied, setIsCopied] = useState(false);
const copyToClipboard = () => {
navigator.clipboard.writeText(url)
.then(() => {
isCopied(true);
setTimeout(() => setIsCopied(false), 2000);
})
.catch(err => console.error(err));
}
return (
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">Discussion URL</Typography>
</Box>
}
open={isOpened}
onClose={closeModal}
maxWidth="460px"
minHeight="200px"
>
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column">
<Box marginBottom="20px" display="flex" flexDirection="column" alignItems="center" gap="10px">
<Typography align="center">
You are leaving the ghost dao app. Check the link on your own, we are not in charge of your destiny.
</Typography>
<Link
onClick={copyToClipboard}
underline="hover"
sx={{
maxWidth: '360px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'inline-block',
verticalAlign: 'middle',
fontStyle: 'italic'
}}
>
{url}
</Link>
</Box>
<PrimaryButton
fullWidth
onClick={() => window.open(url, '_blank', 'noopener,noreferrer')}
>
Open
</PrimaryButton>
</Box>
</Modal>
)
}
export default ProposalDiscussionModal;

View File

@ -0,0 +1,100 @@
import { useState, useEffect, useMemo, useCallback } from "react";
import { Box, Typography } from "@mui/material";
import Modal from "../../../components/Modal/Modal";
import Select from "../../../components/Select/Select";
import { PrimaryButton, TertiaryButton } from "../../../components/Button";
import {
getFunctionArguments,
getFunctionCalldata,
getFunctionDescription,
allPossibleFunctions
} from "./functions";
const ProposalModal = ({ isOpened, closeModal, addCalldata }) => {
const [selectedOption, setSelectedOption] = useState();
const [renderArguments, setRenderArguments] = useState(false);
const handleChange = (event) => {
setSelectedOption(event.target.value);
};
const headerLabel = useMemo(() => {
const data = allPossibleFunctions.find(obj => obj.value === selectedOption);
if (data && renderArguments) {
return data.label;
}
return "Executable Code";
}, [selectedOption, renderArguments]);
const handleCalldata = useCallback(() => {
addCalldata(getFunctionCalldata(selectedOption));
setSelectedOption(null);
closeModal();
}, [selectedOption]);
const handleClose = () => {
setSelectedOption(null);
setRenderArguments(false);
closeModal();
}
const handleAddCalldata = (calldata) => {
addCalldata(calldata);
handleClose();
}
const ArgumentsSteps = useMemo(() => getFunctionArguments(selectedOption), [selectedOption]);
return (
<Modal
headerContent={
<Box display="flex" justifyContent="center" alignItems="center" gap="15px">
<Typography variant="h4">{headerLabel}</Typography>
</Box>
}
open={isOpened}
onClose={handleClose}
maxWidth="460px"
minHeight="200px"
>
<Box minHeight="220px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
{renderArguments
? <ArgumentsSteps
addCalldata={handleAddCalldata}
toInitialStep={() => setRenderArguments(false)}
/>
: <InitialStep
selectedOption={selectedOption}
handleChange={handleChange}
handleCalldata={handleCalldata}
handleProceed={() => setRenderArguments(true)}
ready={ArgumentsSteps !== null}
/>
}
</Box>
</Modal>
)
}
const InitialStep = ({ selectedOption, handleChange, handleCalldata, handleProceed, ready }) => {
const functionDescription = useMemo(() => getFunctionDescription(selectedOption), [selectedOption]);
return (
<Box minHeight="220px" display="flex" alignItems="start" justifyContent="space-between" flexDirection="column">
<Select
value={selectedOption ?? ""}
onChange={handleChange}
options={allPossibleFunctions}
inputWidth="100%"
/>
<Typography align="center" variant="body2">{functionDescription}</Typography>
{ready
? <TertiaryButton disabled={!selectedOption} onClick={() => handleProceed()} fullWidth>Proceed</TertiaryButton>
: <PrimaryButton disabled={!selectedOption} onClick={() => handleCalldata()} fullWidth>Create Function</PrimaryButton>
}
</Box>
)
}
export default ProposalModal;

View File

@ -18,6 +18,9 @@ import {
} from "@mui/material";
import { getBlockNumber } from "@wagmi/core";
import GhostStyledIcon from "../../../components/Icon/GhostIcon";
import ArrowUpIcon from "../../../assets/icons/arrow-up.svg?react";
import { networkAvgBlockSpeed } from "../../../constants";
import { prettifySecondsInDays } from "../../../helpers/timeUtil";
@ -27,8 +30,6 @@ import Paper from "../../../components/Paper/Paper";
import LinearProgressBar from "../../../components/Progress/LinearProgressBar";
import { PrimaryButton, TertiaryButton } from "../../../components/Button";
import ProposalDiscussionModal from "./ProposalDiscussionModal";
import ProposalDiscussion from "./ProposalDiscussion";
import ProposalInfoText from "./ProposalInfoText";
import { convertStatusToTemplate } from "../helpers";
@ -46,16 +47,11 @@ const ProposalsList = ({ chainId, config }) => {
const theme = useTheme();
const [blockNumber, setBlockNumber] = useState(0n);
const [selectedDiscussionUrl, setSelectedDiscussionUrl] = useState(undefined);
const [proposalsFilter, setProposalFilter] = useState("active");
const { proposals } = useProposals(chainId, MAX_PROPOSALS_TO_SHOW);
getBlockNumber(config).then(block => setBlockNumber(block));
const isDiscussionModalOpened = useMemo(() => {
return selectedDiscussionUrl !== undefined;
}, [selectedDiscussionUrl]);
const filteredProposals = useMemo(() => {
switch (proposalsFilter) {
case "voted":
@ -77,12 +73,6 @@ const ProposalsList = ({ chainId, config }) => {
if (isSmallScreen) {
return (
<>
<ProposalDiscussionModal
url={selectedDiscussionUrl}
isOpened={isDiscussionModalOpened}
closeModal={() => setSelectedDiscussionUrl(undefined)}
/>
<Paper
fullWidth
enableBackground
@ -99,7 +89,6 @@ const ProposalsList = ({ chainId, config }) => {
<ProposalCard
key={proposal.id}
proposal={proposal}
setActive={setSelectedDiscussionUrl}
blockNumber={blockNumber}
chainId={chainId}
openProposal={() => navigate(`/governance/${proposal.id}`)}
@ -111,17 +100,10 @@ const ProposalsList = ({ chainId, config }) => {
<ProposalInfoText />
</Box>}
</Paper>
</>
);
}
return (
<>
<ProposalDiscussionModal
url={selectedDiscussionUrl}
isOpened={isDiscussionModalOpened}
closeModal={() => setSelectedDiscussionUrl(undefined)}
/>
<Paper
fullWidth
enableBackground
@ -140,13 +122,19 @@ const ProposalsList = ({ chainId, config }) => {
<Link
color={theme.colors.primary[300]}
href="https://ghostchain.io/ghostdao_litepaper"
href="https://forum.ghostchain.io"
target="_blank"
rel="noopener noreferrer"
display="flex"
alignItems="center"
>
<Typography variant="h7">
Discussion Forum
</Typography>
View Forum&nbsp;
<GhostStyledIcon
style={{ marginTop: "7px" }}
viewBox="0 0 30 30"
className="external-site-link-icon"
component={ArrowUpIcon}
/>
</Link>
</Box>
}
@ -158,7 +146,6 @@ const ProposalsList = ({ chainId, config }) => {
<ProposalRow
key={proposal.id}
proposal={proposal}
setActive={setSelectedDiscussionUrl}
blockNumber={blockNumber}
chainId={chainId}
openProposal={() => navigate(`/governance/${proposal.id}`)}
@ -170,7 +157,6 @@ const ProposalsList = ({ chainId, config }) => {
<ProposalInfoText />
</Box>}
</Paper>
</>
);
}
@ -180,8 +166,7 @@ const ProposalTable = ({ children }) => (
<TableHead>
<TableRow>
<TableCell align="center" style={{ width: "100px", padding: "8px 0" }}>Proposal ID</TableCell>
<TableCell align="center" style={{ width: "120px", padding: "8px 0" }}>Status</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Discussion</TableCell>
<TableCell align="center" style={{padding: "8px 0" }}>Status</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Vote Ends</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>Voting Stats</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}></TableCell>
@ -193,7 +178,7 @@ const ProposalTable = ({ children }) => (
</TableContainer>
);
const ProposalRow = ({ proposal, setActive, blockNumber, openProposal, chainId }) => {
const ProposalRow = ({ proposal, blockNumber, openProposal, chainId }) => {
const theme = useTheme();
return (
@ -204,16 +189,12 @@ const ProposalRow = ({ proposal, setActive, blockNumber, openProposal, chainId }
<TableCell align="center" style={{ padding: "8px 0" }}>
<Chip
sx={{ width: "88px" }}
sx={{ width: "100px" }}
label={proposal.status}
template={convertStatusToTemplate(proposal.status)}
/>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<ProposalDiscussion onClick={() => setActive(proposal.discussion)} />
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>
{convertVoteEnds(
@ -257,7 +238,7 @@ const ProposalRow = ({ proposal, setActive, blockNumber, openProposal, chainId }
);
}
const ProposalCard = ({ proposal, setActive, blockNumber, openProposal, chainId }) => {
const ProposalCard = ({ proposal, blockNumber, openProposal, chainId }) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery('(max-width: 450px)');
@ -282,12 +263,6 @@ const ProposalCard = ({ proposal, setActive, blockNumber, openProposal, chainId
)}
</Typography>
</Box>
<Box width="150px">
<ProposalDiscussion
isSmallScreen={isSmallScreen}
onClick={() => setActive(proposal.discussion)}
/>
</Box>
</Box>
<Box marginTop="15px" marginBottom="15px">

View File

@ -0,0 +1,45 @@
import { Box, Typography, TableCell, TableRow, useTheme } from "@mui/material";
import { TertiaryButton } from "../../../../components/Button";
export const prepareAuditReservesCalldata = (chainId) => {
return "0xauditReserves";
}
export const prepareAuditReservesDescription = "Explanation about what is audit reserves, pros and cons should be here, we need to fill as much text here as possible to make initial screen usable and not empty.";
export const AuditReservesSteps = () => {
return null;
}
export const AuditReservesParsed = (props) => {
return (
<>
{props.isTable && <AuditReservesParsedCell {...props} />}
</>
)
}
const AuditReservesParsedCell = (props) => {
return (
<TableRow id={props.id + `--proposalFunction`} data-testid={props.id + `--proposalFunction`}>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>Audit Reserves</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.target}</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.value} {props.nativeCoin}</Typography>
</TableCell>
{props.remove && <TableCell>
<div style={{ display: 'flex', gap: '8px' }}>
<TertiaryButton fullWidth onClick={() => alert("Do we need it????")}>Edit</TertiaryButton>
<TertiaryButton fullWidth onClick={() => props.remove()}>Delete</TertiaryButton>
</div>
</TableCell>}
</TableRow>
)
}

View File

@ -0,0 +1,244 @@
import { useRef, useState } from "react";
import { Box, Typography, TableRow, TableCell, useTheme } from "@mui/material";
import { PrimaryButton, TertiaryButton } from "../../../../components/Button";
import { BooleanTrigger, ArgumentInput } from "./index";
export const prepareCreateBondCalldata = (chainId) => {
return "0xcreateBond";
}
export const prepareCreateBondDescription = "Explanation about what is bond creation, pros and cons should be here, we need to fill as much text here as possible to make initial screen usable and not empty.";
export const CreateBondParsed = (props) => {
return (
<>
{props.isTable && <CreateBondParsedCell {...props} />}
</>
)
}
const CreateBondParsedCell = (props) => {
return (
<TableRow id={props.id + `--proposalFunction`} data-testid={props.id + `--proposalFunction`}>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>Create Bond</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.target}</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.value} {props.nativeCoin}</Typography>
</TableCell>
{props.remove && <TableCell>
<div style={{ display: 'flex', gap: '8px' }}>
<TertiaryButton fullWidth onClick={() => alert("Do we need it????")}>Edit</TertiaryButton>
<TertiaryButton fullWidth onClick={() => props.remove()}>Delete</TertiaryButton>
</div>
</TableCell>}
</TableRow>
)
}
export const CreateBondSteps = ({ toInitialStep, addCalldata }) => {
const [step, setStep] = useState(1);
const [capacity, setCapacity] = useState();
const [initialPrice, setInitialPrice] = useState();
const [debtBuffer, setDebtBuffer] = useState();
const [bondVesting, setBondVesting] = useState();
const [bondDuration, setBondDuration] = useState();
const [tokenAddress, setTokenAddress] = useState("");
const [capacityInQuote, setCapacityInQuote] = useState(true);
const [fixedTerm, setFixedTerm] = useState(false);
const [vestingLength, setVestingLength] = useState();
const [conclusionTimestamp, setConclusionTimestamp] = useState();
const handleProceed = () => {
addCalldata(prepareCreateBondCalldata())
}
const incrementStep = () => {
setStep(prev => prev + 1);
}
const decrementStep = () => {
if (step > 1) setStep(prev => prev - 1);
else toInitialStep();
}
return (
<Box height="100%" width="100%" display="flex" flexDirection="column" justifyContent="space-between">
{step === 1 && <AddressAndBooleansArguments
tokenAddress={tokenAddress}
setTokenAddress={setTokenAddress}
capacityInQuote={capacityInQuote}
setCapacityInQuote={setCapacityInQuote}
fixedTerm={fixedTerm}
setFixedTerm={setFixedTerm}
/>}
{step === 2 && <MarketArguments
capacity={capacity}
setCapacity={setCapacity}
initialPrice={initialPrice}
setInitialPrice={setInitialPrice}
debtBuffer={debtBuffer}
setDebtBuffer={setDebtBuffer}
/>}
{step === 3 && <TermsArguments
bondVesting={bondVesting}
setBondVesting={setBondVesting}
bondDuration={bondDuration}
setBondDuration={setBondDuration}
/>}
{step === 4 && <IntervalsArguments
fixedTerm={fixedTerm}
vestingLength={vestingLength}
setVestingLength={setVestingLength}
conclusionTimestamp={conclusionTimestamp}
setConclusionTimestamp={setConclusionTimestamp}
/>}
<Box width="100%" sx={{ marginTop: "20px" }} display="flex" flexDirection="column" gap="5px">
<Box display="flex" gap="10px">
<TertiaryButton onClick={() => decrementStep()} fullWidth>Back</TertiaryButton>
<TertiaryButton disabled={step === 4} onClick={() => incrementStep()} fullWidth>Next</TertiaryButton>
</Box>
{step === 4 && <PrimaryButton onClick={() => handleProceed()} fullWidth>Create Function</PrimaryButton>}
</Box>
</Box>
);
}
const MarketArguments = ({
capacity,
setCapacity,
initialPrice,
setInitialPrice,
debtBuffer,
setDebtBuffer
}) => {
return (
<Box>
<ArgumentInput
endString="CSPR or LP"
label="Bond Capacity"
value={capacity}
setValue={setCapacity}
tooltip="Bond capacity"
/>
<ArgumentInput
endString="???"
label="Bond Initial Price"
value={initialPrice}
setValue={setInitialPrice}
tooltip="Bond initial price"
/>
<ArgumentInput
endString="%"
label="Debt buffer"
value={debtBuffer}
setValue={setDebtBuffer}
tooltip="Debt buffer description"
/>
</Box>
)
}
const TermsArguments = ({
bondVesting,
setBondVesting,
bondDuration,
setBondDuration,
}) => {
return (
<Box>
<ArgumentInput
endString="seconds"
label="Bond Vesting"
value={bondVesting}
setValue={setBondVesting}
tooltip="Bond vesting"
/>
<ArgumentInput
endString="seconds"
label="Bond Duration"
value={bondDuration}
setValue={setBondDuration}
tooltip="Bond duration"
/>
</Box>
)
}
const IntervalsArguments = ({
fixedTerm,
vestingLength,
setVestingLength,
conclusionTimestamp,
setConclusionTimestamp,
}) => {
return (
<Box>
<ArgumentInput
endString="seconds"
label={fixedTerm ? "Vesting Length" : "Vested Timestamp"}
value={vestingLength}
setValue={setVestingLength}
tooltip={fixedTerm ? "Vesting length what is" : "Vested timestamp what is"}
/>
<ArgumentInput
endString="seconds"
label="Bond Conclusion"
value={conclusionTimestamp}
setValue={setConclusionTimestamp}
tooltip="Bond conclusion timestamp"
/>
</Box>
)
}
const AddressAndBooleansArguments = ({
tokenAddress,
setTokenAddress,
capacityInQuote,
setCapacityInQuote,
fixedTerm,
setFixedTerm,
}) => {
return (
<Box>
<BooleanTrigger
value={capacityInQuote}
label="Capacity in Quote"
leftText="CSPR?"
rightText="LP?"
setLeftValue={() => setCapacityInQuote(true)}
setRightValue={() => setCapacityInQuote(false)}
tooltip="WTF is this"
/>
<BooleanTrigger
value={fixedTerm}
label="Fixed Term"
leftText="Yes"
rightText="No"
setLeftValue={() => setFixedTerm(true)}
setRightValue={() => setFixedTerm(false)}
tooltip="Fixed term... No idea what it is"
/>
<ArgumentInput
placeholder="0x"
inputType="text"
label="Token Address"
value={tokenAddress}
setValue={setTokenAddress}
tooltip="Token used as deposit for the bond"
/>
</Box>
)
}

View File

@ -0,0 +1,92 @@
import { useState } from "react";
import { Box, Typography, TableRow, TableCell, useTheme } from "@mui/material";
import { PrimaryButton, TertiaryButton } from "../../../../components/Button";
import { BooleanTrigger, ArgumentInput } from "./index";
export const prepareSetAdjustmentCalldata = (chainId) => {
return "0xsetAdjustment";
}
export const prepareSetAdjustmentDescription = "Explanation about what is set adjustment (APY change), pros and cons should be here, we need to fill as much text here as possible to make initial screen usable and not empty.";
export const SetAdjustmentParsed = (props) => {
return (
<>
{props.isTable && <SetAdjustmentParsedCell {...props} />}
</>
)
}
const SetAdjustmentParsedCell = (props) => {
return (
<TableRow id={props.id + `--proposalFunction`} data-testid={props.id + `--proposalFunction`}>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>Set Adjustment</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.target}</Typography>
</TableCell>
<TableCell align="center" style={{ padding: "8px 0" }}>
<Typography>{props.value} {props.nativeCoin}</Typography>
</TableCell>
{props.remove && <TableCell>
<div style={{ display: 'flex', gap: '8px' }}>
<TertiaryButton fullWidth onClick={() => alert("Do we need it????")}>Edit</TertiaryButton>
<TertiaryButton fullWidth onClick={() => props.remove()}>Delete</TertiaryButton>
</div>
</TableCell>}
</TableRow>
)
}
export const SetAdjustmentSteps = ({ toInitialStep, addCalldata }) => {
const [rate, setRate] = useState();
const [target, setTarget] = useState();
const [increase, setIncrease] = useState(false);
const handleProceed = () => {
addCalldata(prepareSetAdjustmentCalldata())
}
return (
<Box height="100%" width="100%" display="flex" flexDirection="column" justifyContent="space-between">
<Box>
<BooleanTrigger
value={increase}
leftText="Add"
rightText="Sub"
setLeftValue={() => setIncrease(true)}
setRightValue={() => setIncrease(false)}
label="Direction"
tooltip="To add or not to add"
/>
<ArgumentInput
endString="%"
label="Change Rate"
value={rate}
setValue={setRate}
tooltip="APY will change each time by this value, by basis points"
/>
<ArgumentInput
endString="%"
label="Target Rate"
value={target}
setValue={setTarget}
tooltip="APY will change until it reaches this value, it's in basis points"
/>
</Box>
<Box width="100%" sx={{ marginTop: "20px" }} display="flex" flexDirection="column" gap="5px">
<Box display="flex" gap="10px">
<TertiaryButton onClick={toInitialStep} fullWidth>Back</TertiaryButton>
<TertiaryButton disabled fullWidth>Next</TertiaryButton>
</Box>
<PrimaryButton onClick={() => handleProceed()} fullWidth>Create Function</PrimaryButton>
</Box>
</Box>
);
}

View File

@ -0,0 +1,188 @@
import { useRef } from "react";
import { Box, Typography, useTheme } from "@mui/material";
import { StyledInputBase } from "../../../../components/Swap/SwapCard";
import InfoTooltip from "../../../../components/Tooltip/InfoTooltip";
import { prepareAuditReservesDescription, prepareAuditReservesCalldata, AuditReservesSteps, AuditReservesParsed } from "./AuditReserves";
import { prepareSetAdjustmentDescription, prepareSetAdjustmentCalldata, SetAdjustmentSteps, SetAdjustmentParsed } from "./SetAdjustment";
import { prepareCreateBondDescription, prepareCreateBondCalldata, CreateBondSteps, CreateBondParsed } from "./CreateBond";
const DEFAULT_DESCRIPTION = "Select function for the proposal to start. Make sure you are ready to fill the proposal, here should be pretty long text just to fill space. Here we are trying to make everything looks cool."
export const allPossibleFunctions = [
{ value: "auditReserves", label: "Audit Reserves" },
{ value: "setAdjustment", label: "Change APY" },
{ value: "createBond", label: "Create Bond" },
];
export const parseFunctionCalldata = (calldata, index, nativeCoin, removeCalldata) => {
const target = "0x5324..123123";
const value = 0;
const remove = () => removeCalldata(index);
switch (true) {
case calldata.includes("auditReserves"):
return <AuditReservesParsed
isTable
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
case calldata.includes("setAdjustment"):
return <SetAdjustmentParsed
isTable
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
case calldata.includes("createBond"):
return <CreateBondParsed
isTable
remove={remove}
nativeCoin={nativeCoin}
value={value}
target={target}
id={index}
/>;
default:
return null;
}
}
export const getFunctionArguments = (functionName) => {
switch (functionName) {
case "auditReserves":
return null;
case "setAdjustment":
return SetAdjustmentSteps;
case "createBond":
return CreateBondSteps;
default:
return null;
}
}
export const getFunctionCalldata = (functionName) => {
switch (functionName) {
case "auditReserves":
return prepareAuditReservesCalldata();
case "setAdjustment":
return prepareSetAdjustmentCalldata();
case "createBond":
return prepareCreateBondCalldata();
default:
return "";
}
}
export const getFunctionDescription = (functionName) => {
switch (functionName) {
case "auditReserves":
return prepareAuditReservesDescription;
case "setAdjustment":
return prepareSetAdjustmentDescription;
case "createBond":
return prepareCreateBondDescription;
default:
return DEFAULT_DESCRIPTION;
}
}
export const BooleanValue = ({ left, text, isSelected, setSelected }) => {
const theme = useTheme();
return (
<Box
onClick={() => setSelected()}
display="flex"
justifyContent="center"
alignItems="center"
sx={{
cursor: "pointer",
borderRadius: left ? "12px 0 0 12px" : "0 12px 12px 0",
flex: 1,
border: `1px solid #fff`
}}
>
<Typography color={`${isSelected ? theme.colors.primary[300] : "#fff"}`} align="center">{text}</Typography>
</Box>
)
}
export const ArgumentsWrapper = ({ label, tooltip, children }) => {
return (
<Box sx={{ marginBottom: "18px" }}>
<Box sx={{ marginLeft: "5px", marginBottom: "5px" }} display="flex" flexDirection="row" alignItems="center">
<Typography>{label}</Typography>
<InfoTooltip message={tooltip} />
</Box>
{children}
</Box>
)
}
export const BooleanTrigger = ({ value, label, tooltip, leftText, rightText, setLeftValue, setRightValue }) => {
return (
<ArgumentsWrapper label={label} tooltip={tooltip}>
<Box display="flex" width="100%" height="40px">
<BooleanValue setSelected={setLeftValue} left isSelected={value} text={leftText} />
<BooleanValue setSelected={setRightValue} isSelected={!value} text={rightText} />
</Box>
</ArgumentsWrapper>
)
}
export const ArgumentInput = ({
endString,
label,
tooltip,
value,
setValue,
inputType = "number",
placeholder = "0",
maxWidth = "100%"
}) => {
const theme = useTheme();
const ref = useRef(null);
return (
<Box sx={{ marginBottom: "15px" }}>
<Box sx={{ marginLeft: "5px", marginBottom: "5px" }} display="flex" flexDirection="row" alignItems="center">
<Typography>{label}</Typography>
<InfoTooltip message={tooltip} />
</Box>
<Box
display="flex"
flexDirection="column"
maxWidth={maxWidth}
sx={{ backgroundColor: theme.colors.gray[750] }}
borderRadius="12px"
padding="6px"
onClick={() => {
ref.current && ref.current.focus();
}}
>
<Box width="100%" display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
<StyledInputBase
placeholder={placeholder}
type={inputType}
fontSize="20px"
value={value}
onChange={(e) => setValue(e.target.value)}
sx={{ flex: 1 }}
/>
{endString && (
<Box sx={{ paddingRight: "10px", color: theme.colors.gray[500] }} fontSize="12px" lineHeight="15px">
{endString}
</Box>
)}
</Box>
</Box>
</Box>
)
}

View File

@ -52,7 +52,8 @@ export const useProposalVotes = (chainId, proposalId) => {
const decimals = getTokenDecimals(name);
const forVotes = new DecimalBigNumber(420_000_000_000_000_000_000n, decimals);
const againstVotes = new DecimalBigNumber(69_000_000_000_000_000_000n, decimals);
return { forVotes, againstVotes }
const totalVotes = new DecimalBigNumber(489_000_000_000_000_000_000n, decimals);
return { forVotes, againstVotes, totalVotes }
}
export const useProposalSnapshot = (chainId, proposalId) => {