diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ee7c6098 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contributing to tezos homebase + +Thank you for your interest in contributing to tezos homebase, a web app that allows anyone to create and manage DAOs on Tezos. We welcome and appreciate any contributions, whether it is code, documentation, design, feedback, or ideas. + + +## How to contribute + +There are many ways you can contribute to our project: + +- Report bugs or suggest features by opening an [issue](https://github.com/dOrgTech/homebase-app/issues). +- Fix bugs or implement features by submitting a [pull request](https://github.com/dOrgTech/homebase-app/pulls). +- Improve the documentation or the user interface by editing the files directly on GitHub or forking the repo. +- Join our [Discord server](https://discord.gg/9cduRr5) and chat with us about the project. + +## Development setup + +To set up your local development environment, follow these steps: + +1. Fork and clone the repo: `git clone https://github.com//homebase-app.git` +2. Install the dependencies: `yarn install` +3. Create a `.env` file in the root directory and add the required environment variables (see `.env.example` for reference) +4. Run the app in development mode: `yarn start` +5. Open http://localhost:3000 to view the app in the browser + +## Pull request guidelines + +Before you submit a pull request, please make sure that: + +- Your code follows the [Prettier](https://prettier.io/) code style and format +- Your code passes the [ESLint](https://eslint.org/) checks and has no errors or warnings +- Your code is well-tested and has good coverage +- Your code is documented with comments and JSDoc annotations +- Your commit messages are clear and descriptive +- Your branch is up-to-date with the `develop` branch + +To submit a pull request, follow these steps: + +1. Create a new branch from the `develop` branch: `git checkout -b ` +2. Make your changes and commit them: `git commit -m ""` +3. Push your branch to your fork: `git push origin ` +4. Go to https://github.com/dOrgTech/homebase-app and create a new pull request from your branch to the `develop` branch +5. Fill out the pull request template and wait for a review + +## Review process + +We will review your pull request as soon as possible and provide feedback or suggestions if needed. We may ask you to make some changes before we merge your pull request. Please be patient and respectful with us and other contributors. + +Thank you for reading this guide and for contributing to tezos homebase! diff --git a/src/assets/img/lite-dao.svg b/src/assets/img/lite-dao.svg index c3bfe185..4a78a7d7 100644 --- a/src/assets/img/lite-dao.svg +++ b/src/assets/img/lite-dao.svg @@ -1,8 +1,3 @@ - - - - - - - + + diff --git a/src/assets/img/managed.svg b/src/assets/img/managed.svg new file mode 100644 index 00000000..b7f7df8d --- /dev/null +++ b/src/assets/img/managed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/self-deployed.svg b/src/assets/img/self-deployed.svg new file mode 100644 index 00000000..c5786f01 --- /dev/null +++ b/src/assets/img/self-deployed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/modules/common/ChangeNetworkButton.tsx b/src/modules/common/ChangeNetworkButton.tsx index 38db6d9c..165c1b27 100644 --- a/src/modules/common/ChangeNetworkButton.tsx +++ b/src/modules/common/ChangeNetworkButton.tsx @@ -1,6 +1,7 @@ import { Box, capitalize, Grid, styled, Typography, Theme } from "@material-ui/core" import { ActionSheet, useActionSheet } from "modules/explorer/context/ActionSheets" import React from "react" +import { useLocation } from "react-router-dom" import { Network } from "services/beacon" import { useTezos } from "services/beacon/hooks/useTezos" @@ -41,16 +42,25 @@ export const ChangeNetworkButton = () => { const { network } = useTezos() const { open } = useActionSheet(ActionSheet.Network) + const location = useLocation() + + const canShow = + location.pathname.indexOf("/explorer/dao/") === -1 && location.pathname.indexOf("/explorer/lite/dao/") === -1 + return ( - open()}> - - - - - - {capitalize(network)} - - - + <> + {canShow ? ( + open()}> + + + + + + {capitalize(network)} + + + + ) : null} + ) } diff --git a/src/modules/common/SmallButton.tsx b/src/modules/common/SmallButton.tsx index 39d060bb..e3dd4d6b 100644 --- a/src/modules/common/SmallButton.tsx +++ b/src/modules/common/SmallButton.tsx @@ -7,6 +7,8 @@ export const SmallButton = styled(Button)({ "transition": ".15s ease-out", "textTransform": "capitalize", "borderRadius": 8, + "backgroundColor": "#81feb7 !important", + "color": "#1c1f23", "&$disabled": { boxShadow: "none" diff --git a/src/modules/common/TitleBlock.tsx b/src/modules/common/TitleBlock.tsx index 247a852d..2eed2299 100644 --- a/src/modules/common/TitleBlock.tsx +++ b/src/modules/common/TitleBlock.tsx @@ -1,4 +1,4 @@ -import { Grid, Paper, styled, Tooltip, Typography } from "@material-ui/core" +import { Grid, Link, Paper, styled, Tooltip, Typography } from "@material-ui/core" import React from "react" import { ReactElement } from "react-markdown/lib/react-markdown" import { InfoRounded } from "@material-ui/icons" @@ -7,14 +7,9 @@ import { CopyButton } from "./CopyButton" const StyledGrid = styled(Grid)({ height: "fit-content", - background: "#2F3438", borderRadius: 8, - padding: "30px 40px", - marginBottom: 38 -}) - -const CustomTypography = styled(Typography)({ - marginTop: 27 + padding: "0", + gap: 16 }) const CustomTooltip = styled(Tooltip)({ @@ -33,7 +28,7 @@ const InfoIconInput = styled(InfoRounded)(({ theme }) => ({ })) const CustomTooltipText = styled(Typography)({ - fontSize: 12, + fontSize: 14, marginLeft: 2 }) @@ -50,9 +45,16 @@ interface Props { description: ReactElement | string tooltip?: boolean tooltipText?: string + tooltipLink?: string } -export const TitleBlock: React.FC = ({ title = "", description, tooltip = false, tooltipText = "" }) => { +export const TitleBlock: React.FC = ({ + title = "", + description, + tooltip = false, + tooltipText = "", + tooltipLink = "" +}) => { return ( @@ -66,9 +68,9 @@ export const TitleBlock: React.FC = ({ title = "", description, tooltip = - + {tooltipText} - + ) : null} @@ -79,9 +81,9 @@ export const TitleBlock: React.FC = ({ title = "", description, tooltip = {description} ) : description ? ( - + {description} - + ) : null} diff --git a/src/modules/creator/components/NavigationBar.tsx b/src/modules/creator/components/NavigationBar.tsx index 218709f6..c74bac0b 100644 --- a/src/modules/creator/components/NavigationBar.tsx +++ b/src/modules/creator/components/NavigationBar.tsx @@ -3,6 +3,7 @@ import React from "react" import { ArrowBackIos } from "@material-ui/icons" import { NavigationBarProps } from "modules/creator/state" +import { MainButton } from "modules/common/MainButton" const Footer = styled(Grid)(({ theme }) => ({ background: theme.palette.primary.main, @@ -27,14 +28,12 @@ const BackButton = styled(Paper)({ width: "fit-content" }) -const NextButton = styled(Paper)(({ theme }) => ({ - boxShadow: "none", - borderRadius: 4, +const NextButton = styled(MainButton)(({ theme }) => ({ textAlign: "center", float: "right", cursor: "pointer", background: theme.palette.secondary.light, - padding: 8 + padding: "8px 16px" })) const BackButtonIcon = styled(ArrowBackIos)(({ theme }) => ({ @@ -58,14 +57,18 @@ export const NavigationBar: React.FC = ({ back, next }) => { {back && ( - {back.text} + + {back.text} + )} {next && ( - {next.text} + + {next.text} + )} diff --git a/src/modules/creator/deployment/steps/Distribution.tsx b/src/modules/creator/deployment/steps/Distribution.tsx index 08eb4f82..05aa892a 100644 --- a/src/modules/creator/deployment/steps/Distribution.tsx +++ b/src/modules/creator/deployment/steps/Distribution.tsx @@ -122,6 +122,9 @@ const validateForm = (values: TokenDistributionSettings) => { if (values.totalAmount && values.totalAmount.minus(new BigNumber(getTotal(values.holders))) < new BigNumber(0)) { errors.totalAmount = "Available balance has to be greater that the total supply" } + if (values.totalAmount && values.totalAmount.gt(new BigNumber(getTotal(values.holders)))) { + errors.totalAmount = "Total Supply not fully allocated" + } }) return errors diff --git a/src/modules/creator/deployment/steps/Summary.tsx b/src/modules/creator/deployment/steps/Summary.tsx index 78178d4b..5f06350c 100644 --- a/src/modules/creator/deployment/steps/Summary.tsx +++ b/src/modules/creator/deployment/steps/Summary.tsx @@ -98,7 +98,7 @@ export const ContractSummary: React.FC = () => { } const { - mutation: { mutate, data, error } + mutation: { mutate, data } } = useTokenOriginate(state.data) useEffect(() => { @@ -107,19 +107,10 @@ export const ContractSummary: React.FC = () => { type: ActionTypes.CLEAR_CACHE }) history.push("/creator/success", { address: data.address }) - } - }, [data, dispatch, history]) - - useEffect(() => { - if (error) { + } else if (data && !data.address) { setIsLoading(false) - openNotification({ - message: "Error deploying token... try again later", - variant: "error", - autoHideDuration: 2000 - }) } - }, [error, openNotification]) + }, [data, dispatch, history]) useEffect(() => { dispatch({ diff --git a/src/modules/creator/index.tsx b/src/modules/creator/index.tsx index 18461a78..20985732 100644 --- a/src/modules/creator/index.tsx +++ b/src/modules/creator/index.tsx @@ -10,13 +10,14 @@ import { styled, useMediaQuery, useTheme, - Theme + Theme, + Link } from "@material-ui/core" import ProgressBar from "react-customizable-progressbar" import { useHistory } from "react-router" import { CreatorContext, StepInfo } from "modules/creator/state" -import { StepRouter, STEPS, useStepNumber } from "modules/creator/steps" +import { StepRouter, STEPS, urlToStepMap, useStepNumber } from "modules/creator/steps" import { NavigationBar } from "modules/creator/components/NavigationBar" import { Navbar } from "modules/common/Toolbar" import mixpanel from "mixpanel-browser" @@ -71,25 +72,17 @@ const FAQClickToAction = styled(Typography)(({ theme }) => ({ color: theme.palette.secondary.main, fontSize: "14px", cursor: "pointer", - textAlign: "center", - textDecoration: "underline" -})) - -const FAQClickText = styled(Typography)(({ theme }) => ({ - color: theme.palette.secondary.main, - fontSize: "14px", - cursor: "pointer", - textAlign: "center" + fontWeight: 300 })) const ProgressContainer = styled(Grid)(({ theme }) => ({ background: "#2F3438", display: "grid", borderRadius: 8, - maxHeight: 585, + maxHeight: 650, paddingTop: 20, position: "sticky", - top: 153 + top: 130 })) const custom = (theme: Theme) => ({ @@ -133,10 +126,6 @@ export const DAOCreate: React.FC = () => { const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down("sm")) - const goToFAQ = (): void => { - history.push("/faq") - } - useEffect(() => { mixpanel.unregister("daoAddress") mixpanel.unregister("daoType") @@ -152,7 +141,7 @@ export const DAOCreate: React.FC = () => { { trackStrokeColor={"rgba(255, 255, 255, 0.2)"} > - {progress === 0.5 ? 0 : step * 20}% + {Math.floor((step / (Object.keys(urlToStepMap).length - 1)) * 100)}% - - New to DAOs? - Read our FAQ + + New to DAOs? + + Read our FAQ + {STEPS.map(({ title, path }: StepInfo, index: number) => ( @@ -188,7 +179,7 @@ export const DAOCreate: React.FC = () => { - {step < 5 && } + {step < 6 && } diff --git a/src/modules/creator/state/types.ts b/src/modules/creator/state/types.ts index 3ecee0a3..6bbe0081 100644 --- a/src/modules/creator/state/types.ts +++ b/src/modules/creator/state/types.ts @@ -68,6 +68,8 @@ export interface NavigationBarProps { export type DAOTemplate = "lambda" | "lite" | "" +export type DeploymentMethod = "managed" | "self-deployed" + type DeploymentStatus = { deploying: boolean successful: boolean diff --git a/src/modules/creator/steps/DaoSettings.tsx b/src/modules/creator/steps/DaoSettings.tsx index 1e192561..cd100deb 100644 --- a/src/modules/creator/steps/DaoSettings.tsx +++ b/src/modules/creator/steps/DaoSettings.tsx @@ -74,6 +74,9 @@ const CustomFormikTextField = withStyles({ "& .MuiInputBase-input": { textAlign: "initial" }, + "& .MuiInputBase-root": { + textWeight: 300 + }, "& .MuiInput-underline:before": { borderBottom: "none !important" }, @@ -97,11 +100,13 @@ const CustomTextarea = styled(withTheme(TextareaAutosize))(props => ({ "marginTop": 14, "fontWeight": 300, "padding": "21px 20px", - "fontFamily": "Roboto Mono", "border": "none", "fontSize": 16, + "fontFamily": "Roboto Mono", "color": props.theme.palette.text.secondary, "background": "#2F3438", + "lineHeight": "135%", + "letterSpacing": -0.18, "borderRadius": 8, "paddingRight": 40, "wordBreak": "break-word", @@ -163,6 +168,22 @@ const DaoSettingsForm = withRouter(({ submitForm, values, setFieldValue, errors, return ( <> + + + {" "} + DAO Name{" "} + + + + + {errors.name && touched.name ? {errors.name} : null} + {" "} @@ -220,55 +241,12 @@ const DaoSettingsForm = withRouter(({ submitForm, values, setFieldValue, errors, )} - - - - {" "} - DAO Name{" "} - - - - - {errors.name && touched.name ? {errors.name} : null} - - - - - - Description - - - - - {() => ( - { - setFieldValue("description", newValue.target.value) - }} - /> - )} - - - - - - {errors.description && touched.description ? {errors.description} : null} {" "} - Guardian{" "} + Guardian Address{" "} @@ -299,6 +277,32 @@ const DaoSettingsForm = withRouter(({ submitForm, values, setFieldValue, errors, {errors.guardian && touched.guardian ? {errors.guardian} : null} + + + + DAO Description + + + + + {() => ( + { + setFieldValue("description", newValue.target.value) + }} + /> + )} + + + + + + {errors.description && touched.description ? {errors.description} : null} + ) }) @@ -388,7 +392,7 @@ export const DaoSettings = (): JSX.Element => { return ( These settings will define the name, symbol, and initial distribution of your token. You will need a diff --git a/src/modules/creator/steps/DeploymentType.tsx b/src/modules/creator/steps/DeploymentType.tsx new file mode 100644 index 00000000..8fd999d4 --- /dev/null +++ b/src/modules/creator/steps/DeploymentType.tsx @@ -0,0 +1,156 @@ +import React, { useContext, useEffect, useState } from "react" +import { Grid, styled, Typography, Box, useMediaQuery, useTheme, makeStyles, Link } from "@material-ui/core" +import { useHistory } from "react-router" + +import { ReactComponent as ManagedIcon } from "assets/img/managed.svg" +import { ReactComponent as SelfDeployedIcon } from "assets/img/self-deployed.svg" + +import { ActionTypes, CreatorContext, DeploymentMethod } from "modules/creator/state" +import { TitleBlock } from "modules/common/TitleBlock" +import { useRouteMatch } from "react-router-dom" + +const LambdaCustomBox = styled(Grid)(({ theme }) => ({ + "height": 480, + "marginTop": 30, + "background": "#2F3438", + "borderRadius": 8, + "maxWidth": 320, + "width": "-webkit-fill-available", + "padding": "40px 44px", + "textAlign": "start", + "cursor": "pointer", + "paddingBottom": 0, + "&:hover": { + border: "3px solid rgba(129, 254, 183, 0.4)", + paddingTop: 37, + paddingBottom: 0, + paddingRight: 41, + paddingLeft: 41 + }, + ["@media (max-width:1167px)"]: { + marginBottom: 20, + marginTop: 20 + } +})) + +const styles = makeStyles({ + selected: { + border: "3px solid rgba(129, 254, 183, 0.4)", + padding: "37px 41px" + } +}) + +const BoxTitle = styled(Typography)({ + fontSize: 18, + fontWeight: 500, + fontFamily: "Roboto Mono", + marginBottom: 10 +}) + +const BoxDescription = styled(Typography)({ + fontWeight: 300, + fontSize: 16 +}) + +export const DeploymentType = (): JSX.Element => { + const { state, dispatch, updateCache } = useContext(CreatorContext) + const { template } = state.data + + const history = useHistory() + + const match = useRouteMatch() + + const theme = useTheme() + const style = styles() + + const isMobileSmall = useMediaQuery(theme.breakpoints.down("xs")) + + const [selectedTemplate, setTemplate] = useState("managed") + const [error, setError] = useState(false) + + useEffect(() => { + dispatch({ + type: ActionTypes.UPDATE_NAVIGATION_BAR, + next: { + handler: () => { + history.push(`review`, { method: selectedTemplate }) + }, + text: "Deploy DAO" + }, + back: { + text: "Back", + handler: () => history.push(`summary`) + } + }) + }, [dispatch, history, match.path, match.url, selectedTemplate]) + + const update = (templateValue: DeploymentMethod) => { + setError(false) + setTemplate(templateValue) + } + + return ( + + + Learn more about the two available deployment options in{" "} + + this article + {" "} + + } + /> + + update("managed")} + className={selectedTemplate === "managed" ? style.selected : ""} + > + + Managed + + + Homebase will deploy a contract on-chain with your parameters using a dedicated endpoint.{" "} + + + Requires upfront payment for the transaction fees (7 XTZ).{" "} + + Takes between 3 and 5 minutes. + + {" "} + update("self-deployed")} + className={selectedTemplate === "self-deployed" ? style.selected : ""} + > + + Self-Deployed + + Use your private key to sign four transactions. + + May need multiple tries as bytecode can get stuck between app and wallet. If it’s not working, please use + the managed option. + + Can take up to 15 minutes. + + {" "} + + + ) +} diff --git a/src/modules/creator/steps/Governance.tsx b/src/modules/creator/steps/Governance.tsx index 7feb04f0..e8b42475 100644 --- a/src/modules/creator/steps/Governance.tsx +++ b/src/modules/creator/steps/Governance.tsx @@ -14,7 +14,6 @@ import dayjs from "dayjs" import { TitleBlock } from "modules/common/TitleBlock" import BigNumber from "bignumber.js" import { mutezToXtz, parseUnits } from "services/contracts/utils" -import { formatUnits } from "services/contracts/utils" import { FieldChange, handleChange } from "../utils" const TimeBox = styled(Grid)(({ theme }) => ({ @@ -28,7 +27,8 @@ const TimeBox = styled(Grid)(({ theme }) => ({ const TimeText = styled(Typography)({ marginTop: -20, - marginLeft: 16 + marginLeft: 16, + fontWeight: 300 }) const CustomTooltip = styled(Tooltip)({ @@ -152,7 +152,8 @@ const Value = styled(Typography)({ const styles = { voting: { marginTop: 6, - marginBottom: 16 + marginBottom: 16, + fontWeight: 400 } } @@ -178,8 +179,13 @@ const GridNoPadding = styled(Grid)({ const InfoBox = styled(Paper)({ boxShadow: "none", border: "none", - background: "inherit", - marginTop: 20 + marginTop: 20, + justifyContent: "center", + alignItems: "center", + gap: 10, + backgroundColor: "#2F3438", + borderRadius: 8, + padding: "32px 48px" }) const validateForm = (values: VotingSettings) => { @@ -377,14 +383,6 @@ const GovernanceForm = ({ submitForm, values, setFieldValue, errors, touched, se return "0 minutes" } - // const controlMaxFieldLimit = (field: string, value: any) => { - // const itemValue = value.target.value.split(".") - // if ((itemValue[0] && itemValue[0].length > 18) || (itemValue[1] && itemValue[1].length > 8)) { - // return value.preventDefault() - // } - // setFieldValue(field, value.target.value) - // } - useEffect(() => { if (values) { dispatch({ @@ -727,16 +725,16 @@ const GovernanceForm = ({ submitForm, values, setFieldValue, errors, touched, se - + You will need to wait for a full cycle before making your first proposal. - + {`A proposal will accept votes for ${formatDate(votingTime)} after it is created. Once the voting cycle ends, if the proposal is accepted, it will become executable after another ${formatDate( flushDelayTime )}.`} - + If not executed within {formatDate(expiryDelayTime)} after voting ends, the proposal will expire and won't be available for execution anymore. @@ -745,7 +743,7 @@ const GovernanceForm = ({ submitForm, values, setFieldValue, errors, touched, se > - + Required Stake to Propose @@ -797,12 +795,12 @@ const GovernanceForm = ({ submitForm, values, setFieldValue, errors, touched, se - + Returned Stake After Proposal Rejection - + {() => ( @@ -898,8 +896,9 @@ export const Governance: React.FC = () => { diff --git a/src/modules/creator/steps/Quorum.tsx b/src/modules/creator/steps/Quorum.tsx index d1298a2d..92b9edfa 100644 --- a/src/modules/creator/steps/Quorum.tsx +++ b/src/modules/creator/steps/Quorum.tsx @@ -15,12 +15,15 @@ const ErrorText = styled(Typography)({ minWidth: "100%", fontSize: 14, color: "red", - marginTop: 4 + height: 42 }) -const SpacingContainer = styled(Grid)({ - marginTop: 25 -}) +const SpacingContainer = styled(Grid)(({ theme }) => ({ + marginTop: 25, + [theme.breakpoints.down("sm")]: { + gap: 16 + } +})) const AdditionContainer = styled(Grid)(({ theme }) => ({ marginTop: 14, @@ -64,11 +67,6 @@ const ValueText = styled(Typography)({ } }) -const GridItemContainer = styled(Grid)(() => ({ - display: "flex", - alignItems: "center" -})) - const InfoIconInput = styled(InfoRounded)(({ theme }) => ({ cursor: "default", color: theme.palette.secondary.light, @@ -92,33 +90,6 @@ const ParentContainer = styled(Grid)({ } }) -const CustomInputContainer = styled(Grid)(({ theme }) => ({ - border: "none", - height: 54, - marginTop: 14, - background: "#2F3438", - borderRadius: 8, - alignItems: "center", - justifyContent: "end", - padding: "12px 25px", - minWidth: 150, - maxWidth: 150, - fontWeight: 300, - ["@media (max-width:1167px)"]: { - maxWidth: "100%", - minWidth: "100%", - paddingLeft: 25, - paddingRight: 25 - } -})) - -const InputContainer = styled(Grid)({ - paddingRight: 15, - ["@media (max-width:1167px)"]: { - paddingRight: 0 - } -}) - const validateForm = (values: QuorumSettings) => { const errors: FormikErrors = {} @@ -194,181 +165,199 @@ const QuorumForm = ({ submitForm, values, errors, touched, setFieldValue, setFie return ( <> - - - Quorum Threshold - + + + + Initial Quorum Threshold + + + + + + handleChange(e)} + placeholder="00" + inputProps={{ min: 0, max: 100, step: 1 }} + component={TextField} + InputProps={{ + endAdornment: % + }} + onClick={() => setFieldTouched("quorumThreshold")} + // onChange={(e: any) => controlMaxFieldLimit("quorumThreshold", e)} + /> + + + + + + + + + + {errors.quorumThreshold && touched.quorumThreshold ? {errors.quorumThreshold} : null} + + + + + + Initial Quorum Change + + + + + + handleChange(e)} + placeholder="00" + inputProps={{ min: 0, max: 100, step: 1 }} + component={TextField} + InputProps={{ + endAdornment: % + }} + onClick={() => setFieldTouched("quorumChange")} + // onChange={(e: any) => controlMaxFieldLimit("quorumChange", e)} + /> + + + + + + + + + + {errors.quorumChange && touched.quorumChange ? {errors.quorumChange} : null} + + - - - - - - handleChange(e)} - placeholder="00" - inputProps={{ min: 0, max: 100, step: 1 }} - component={TextField} - InputProps={{ - endAdornment: % - }} - onClick={() => setFieldTouched("quorumThreshold")} - // onChange={(e: any) => controlMaxFieldLimit("quorumThreshold", e)} - /> - - - - - - - - - - - - - - - handleChange(e)} - placeholder="00" - inputProps={{ min: 0, max: 100, step: 1 }} - component={TextField} - InputProps={{ - endAdornment: % - }} - onClick={() => setFieldTouched("minQuorumAmount")} - // onChange={(e: any) => controlMaxFieldLimit("minQuorumAmount", e)} - > - - - Min - - - - - - - - - - - - handleChange(e)} - placeholder="00" - inputProps={{ min: 0, max: 100, step: 1 }} - component={TextField} - InputProps={{ - endAdornment: % - }} - onClick={() => setFieldTouched("maxQuorumAmount")} - // onChange={(e: any) => controlMaxFieldLimit("maxQuorumAmount", e)} - > - - - Max - - - - - - - - {errors.quorumThreshold && touched.quorumThreshold ? {errors.quorumThreshold} : null} - {errors.minQuorumAmount && touched.minQuorumAmount ? {errors.minQuorumAmount} : null} - {errors.maxQuorumAmount && touched.maxQuorumAmount ? {errors.maxQuorumAmount} : null} - - - - - Quorum Change - + + + + Min. Quorum Threshold + + + + + + handleChange(e)} + placeholder="00" + inputProps={{ min: 0, max: 100, step: 1 }} + component={TextField} + InputProps={{ + endAdornment: % + }} + onClick={() => setFieldTouched("minQuorumAmount")} + // onChange={(e: any) => controlMaxFieldLimit("minQuorumAmount", e)} + > + + + + + + + + + + {errors.minQuorumAmount && touched.minQuorumAmount ? {errors.minQuorumAmount} : null} + + + + + + Max. Quorum Change + + + + + + handleChange(e)} + placeholder="00" + inputProps={{ min: 0, max: 100, step: 1 }} + component={TextField} + InputProps={{ + endAdornment: % + }} + onClick={() => setFieldTouched("quorumMaxChange")} + // onChange={(e: any) => controlMaxFieldLimit("quorumMaxChange", e)} + /> + + + + + + + + + + {errors.quorumMaxChange && touched.quorumMaxChange ? {errors.quorumMaxChange} : null} + + - - - - - handleChange(e)} - placeholder="00" - inputProps={{ min: 0, max: 100, step: 1 }} - component={TextField} - InputProps={{ - endAdornment: % - }} - onClick={() => setFieldTouched("quorumChange")} - // onChange={(e: any) => controlMaxFieldLimit("quorumChange", e)} - /> - - - - - - - - - - {errors.quorumChange && touched.quorumChange ? {errors.quorumChange} : null} - - - - - Quorum Max Change - + + + + Max. Quorum Threshold + + + + + + handleChange(e)} + placeholder="00" + inputProps={{ min: 0, max: 100, step: 1 }} + component={TextField} + InputProps={{ + endAdornment: % + }} + onClick={() => setFieldTouched("maxQuorumAmount")} + // onChange={(e: any) => controlMaxFieldLimit("maxQuorumAmount", e)} + > + + + + + + + + + + {errors.maxQuorumAmount && touched.maxQuorumAmount ? {errors.maxQuorumAmount} : null} + + - - - - - - handleChange(e)} - placeholder="00" - inputProps={{ min: 0, max: 100, step: 1 }} - component={TextField} - InputProps={{ - endAdornment: % - }} - onClick={() => setFieldTouched("quorumMaxChange")} - // onChange={(e: any) => controlMaxFieldLimit("quorumMaxChange", e)} - /> - - - - - - - - - - {errors.quorumMaxChange && touched.quorumMaxChange ? {errors.quorumMaxChange} : null} - ) } @@ -401,8 +390,9 @@ export const Quorum: React.FC = () => { "\u0027" + "s total supply" } - tooltipText={"Quorum Settings"} + tooltipText={"Quorum"} tooltip={true} + tooltipLink={"how-to-configure-your-dao-in-homebase/configure-proposal-and-voting"} > { activeState } = useOriginate(state.data.template) const history = useHistory() - console.log("states: ", states) + + const historyState = history.location?.state as { method: DeploymentMethod } + const deploymentMethod = historyState.method // TODO: Fix infinite calling here useEffect(() => { ;(async () => { - if (!validDAOData && info && metadataCarrierParams) { + if (!validDAOData && info && metadataCarrierParams && deploymentMethod) { mutate({ metadataParams: metadataCarrierParams, - params: info + params: info, + deploymentMethod }) } })() @@ -100,39 +103,44 @@ export const Review: React.FC = () => { - - - Deploying - - - {" "} - {state.data.orgSettings.name} - - - {" "} - to the Tezos Network - - - - - {data && data.address ? ( - history.push("/explorer/dao/" + data.address)} - > - Go to my DAO - - ) : null} - - + {data && !data.address ? ( + + + Deploying + + + {" "} + {state.data.orgSettings.name} + + + {" "} + to the Tezos Network + + + ) : null} + + + {data && data.address ? ( + history.push("/explorer/dao/" + data.address)} + > + Go to my DAO + + ) : null} + + + {states[0].activeText !== "" && states[2].completedText === "" && error === null ? ( - - This may take several minutes - + data && data.address ? null : ( + + This may take several minutes, Do not close the tab + + ) ) : null} ) : ( diff --git a/src/modules/creator/steps/Summary.tsx b/src/modules/creator/steps/Summary.tsx index 6704bdb4..65b9935b 100644 --- a/src/modules/creator/steps/Summary.tsx +++ b/src/modules/creator/steps/Summary.tsx @@ -113,9 +113,9 @@ export const Summary = (): JSX.Element => { type: ActionTypes.UPDATE_NAVIGATION_BAR, next: { handler: () => { - history.push(`review`) + history.push(`type`) }, - text: "Launch" + text: "Continue" }, back: { handler: () => history.push(`quorum`), diff --git a/src/modules/creator/steps/Template.tsx b/src/modules/creator/steps/Template.tsx index db9dc8d6..629265e2 100644 --- a/src/modules/creator/steps/Template.tsx +++ b/src/modules/creator/steps/Template.tsx @@ -1,7 +1,6 @@ import React, { useContext, useEffect, useState } from "react" import { Grid, styled, Typography, Box, useMediaQuery, useTheme, makeStyles } from "@material-ui/core" import { useHistory } from "react-router" -import { ReactComponent as LambdaIcon } from "assets/img/lambda.svg" import { ReactComponent as LiteIcon } from "assets/img/lite-dao.svg" import { ReactComponent as FullIcon } from "assets/img/full-dao.svg" @@ -57,7 +56,10 @@ const BoxTitle = styled(Typography)({ const BoxDescription = styled(Typography)({ fontWeight: 300, - fontSize: 16 + fontSize: 16, + lineHeight: "135%", + letterSpacing: -0.18, + alignSelf: "stretch" }) export const Template = (): JSX.Element => { @@ -95,6 +97,10 @@ export const Template = (): JSX.Element => { return history.push("/lite") }, text: "Continue" + }, + back: { + text: "Back", + handler: () => history.push("/creator/ownership") } }) }, [dispatch, history, match.path, match.url, selectedTemplate]) @@ -118,7 +124,7 @@ export const Template = (): JSX.Element => { onClick={() => update("lambda")} className={selectedTemplate === "lambda" ? style.selected : ""} > - + Full DAO Contract interaction. Transfer assets based on vote outcomes. @@ -134,7 +140,7 @@ export const Template = (): JSX.Element => { onClick={() => update("lite")} className={selectedTemplate === "lite" ? style.selected : ""} > - + Lite DAO Off-chain weighted voting. Multiple voting strategies. No treasury.{" "} diff --git a/src/modules/creator/steps/index.tsx b/src/modules/creator/steps/index.tsx index 5deeaa22..efc92a1f 100644 --- a/src/modules/creator/steps/index.tsx +++ b/src/modules/creator/steps/index.tsx @@ -9,22 +9,25 @@ import { ProtectedRoute } from "modules/creator/components/ProtectedRoute" import { Quorum } from "./Quorum" import mixpanel from "mixpanel-browser" import { Template } from "./Template" +import { DeploymentType } from "./DeploymentType" export const STEPS: StepInfo[] = [ - { title: "Select template", index: 0, path: "template" }, - { title: "Configure DAO settings", index: 1, path: "dao" }, - { title: "Configure Proposal & Voting", index: 2, path: "voting" }, - { title: "Adjust Quorum", index: 3, path: "quorum" }, - { title: "Review information", index: 4, path: "summary" } + { title: "DAO Template", index: 0, path: "template" }, + { title: "DAO Basics", index: 1, path: "dao" }, + { title: "Proposals & Voting", index: 2, path: "voting" }, + { title: "Quorum", index: 3, path: "quorum" }, + { title: "Review Information", index: 4, path: "summary" }, + { title: "Deployment Type", index: 5, path: "type" } ] -const urlToStepMap: Record = { +export const urlToStepMap: Record = { template: 0, dao: 1, voting: 2, quorum: 3, summary: 4, - review: 5 + type: 5, + review: 6 } const AnalyticsWrappedStep: React.FC<{ name: string; index: number }> = ({ name, index, children }) => { @@ -69,8 +72,13 @@ export const StepRouter: React.FC = () => { + + + + + - + @@ -99,3 +107,4 @@ export { Template } from "modules/creator/steps/Template" export { DaoSettings } from "modules/creator/steps/DaoSettings" export { Governance } from "modules/creator/steps/Governance" export { Review } from "modules/creator/steps/Review" +export { DeploymentType } from "modules/creator/steps/DeploymentType" diff --git a/src/modules/explorer/components/ChangeNetworkButton.tsx b/src/modules/explorer/components/ChangeNetworkButton.tsx index 6428ab0b..ac15c03d 100644 --- a/src/modules/explorer/components/ChangeNetworkButton.tsx +++ b/src/modules/explorer/components/ChangeNetworkButton.tsx @@ -1,8 +1,9 @@ import { Box, capitalize, Grid, styled, Typography, Theme } from "@material-ui/core" -import React from "react" +import React, { useEffect } from "react" import { Network } from "services/beacon" import { useTezos } from "services/beacon/hooks/useTezos" import { ActionSheet, useActionSheet } from "../context/ActionSheets" +import { useLocation } from "react-router-dom" const StyledConnectedButton = styled(Box)(({ theme }: { theme: Theme }) => ({ "& > *": { @@ -40,16 +41,25 @@ export const ChangeNetworkButton = () => { const { network } = useTezos() const { open } = useActionSheet(ActionSheet.Network) + const location = useLocation() + + const canShow = + location.pathname.indexOf("/explorer/dao/") === -1 && location.pathname.indexOf("/explorer/lite/dao/") === -1 + return ( - open()}> - - - - - - {capitalize(network)} - - - + <> + {canShow ? ( + open()}> + + + + + + {capitalize(network)} + + + + ) : null} + ) } diff --git a/src/modules/explorer/components/NetworkSheet.tsx b/src/modules/explorer/components/NetworkSheet.tsx index 551a1ec1..5dce70b2 100644 --- a/src/modules/explorer/components/NetworkSheet.tsx +++ b/src/modules/explorer/components/NetworkSheet.tsx @@ -42,7 +42,6 @@ export const NetworkSheet: React.FC = props => { dispatch({ type: ActionTypes.CLEAR_CACHE }) - window.location.href = `/explorer` }} > diff --git a/src/modules/explorer/components/ProposalActionsDialog.tsx b/src/modules/explorer/components/ProposalActionsDialog.tsx index 99d194ce..fcd5ff25 100644 --- a/src/modules/explorer/components/ProposalActionsDialog.tsx +++ b/src/modules/explorer/components/ProposalActionsDialog.tsx @@ -15,6 +15,7 @@ import { SupportedLambdaProposalKey } from "services/bakingBad/lambdas" import { ProposalAction, ProposalFormLambda } from "modules/explorer/components/ConfigProposalFormLambda" import { useDAO } from "services/services/dao/hooks/useDAO" import { ProposalCreatorModal } from "modules/lite/explorer/pages/CreateProposal/ProposalCreatorModal" +import { useIsProposalButtonDisabled } from "services/contracts/baseDAO/hooks/useCycleInfo" type RecursivePartial = { [P in keyof T]?: RecursivePartial @@ -123,6 +124,7 @@ export const ProposalActionsDialog: React.FC = ({ open, handleClose }) => const [openProposalFormLambda, setOpenProposalFormLambda] = useState(false) const [openLiteProposal, setOpenLiteProposal] = useState(false) const liteDAOId = data?.liteDAOData?._id + const shouldDisable = useIsProposalButtonDisabled(daoId) const handleOpenCustomProposalModal = (key: ProposalAction) => { setProposalAction(key) @@ -167,13 +169,21 @@ export const ProposalActionsDialog: React.FC = ({ open, handleClose }) => onClick={() => elem.id === "off-chain" ? handleLiteProposal() - : elem.isLambda - ? handleOpenCustomProposalModal(elem.id) - : handleOpenSupportedExecuteProposalModal(elem.id) + : !shouldDisable + ? elem.isLambda + ? handleOpenCustomProposalModal(elem.id) + : handleOpenSupportedExecuteProposalModal(elem.id) + : null } > - {elem.name} - {elem.description} + + {elem.name} + + + {elem.description}{" "} + ) diff --git a/src/modules/explorer/components/ResponsiveDialog.tsx b/src/modules/explorer/components/ResponsiveDialog.tsx index eaddf628..7af33113 100644 --- a/src/modules/explorer/components/ResponsiveDialog.tsx +++ b/src/modules/explorer/components/ResponsiveDialog.tsx @@ -13,6 +13,7 @@ const TitleText = styled(Typography)(({ theme }) => ({ fontWeight: 550, lineHeight: ".80", textTransform: "capitalize", + fontSize: 20, [theme.breakpoints.down("sm")]: { fontSize: 18 } diff --git a/src/modules/explorer/components/Toolbar.tsx b/src/modules/explorer/components/Toolbar.tsx index 1c729734..63ada175 100644 --- a/src/modules/explorer/components/Toolbar.tsx +++ b/src/modules/explorer/components/Toolbar.tsx @@ -11,7 +11,6 @@ import { useMediaQuery, Theme } from "@material-ui/core" -import { useHistory } from "react-router-dom" import HomeButton from "assets/logos/homebase_logo.svg" import { useTezos } from "services/beacon/hooks/useTezos" @@ -21,7 +20,6 @@ import { ProfileAvatar } from "modules/explorer/components/styled/ProfileAvatar" import { NavigationMenu } from "modules/explorer/components/NavigationMenu" import { ActionSheet, useActionSheet } from "../context/ActionSheets" import { SmallButton } from "../../common/SmallButton" -import { EnvKey, getEnv } from "services/config" const Header = styled(Grid)(({ theme }) => ({ width: "1000px", @@ -105,8 +103,6 @@ export const Navbar: React.FC<{ disableMobileMenu?: boolean }> = ({ disableMobil const { open: openUserMenuSheet } = useActionSheet(ActionSheet.UserMenu) - const history = useHistory() - return ( @@ -141,6 +137,7 @@ export const Navbar: React.FC<{ disableMobileMenu?: boolean }> = ({ disableMobil + openUserMenuSheet()}> = ({ daoId, children }) = userBalances.available.balance = userLedger.available_balance.dp(10, 1).toString() userBalances.pending.balance = userLedger.pending_balance.dp(10, 1).toString() userBalances.staked.balance = userLedger.staked.dp(10, 1).toString() - return userBalances }, [account, ledger]) diff --git a/src/modules/explorer/pages/Proposals/index.tsx b/src/modules/explorer/pages/Proposals/index.tsx index bbdb671b..606590cd 100644 --- a/src/modules/explorer/pages/Proposals/index.tsx +++ b/src/modules/explorer/pages/Proposals/index.tsx @@ -241,12 +241,7 @@ export const Proposals: React.FC = () => { direction={isMobileSmall ? "column" : "row"} xs={isMobileSmall ? undefined : true} > - setOpenDialog(true)} - > + setOpenDialog(true)}> New Proposal diff --git a/src/modules/explorer/pages/User/components/DelegationBanner.tsx b/src/modules/explorer/pages/User/components/DelegationBanner.tsx new file mode 100644 index 00000000..dcc3775a --- /dev/null +++ b/src/modules/explorer/pages/User/components/DelegationBanner.tsx @@ -0,0 +1,141 @@ +import React, { Fragment, useEffect, useState } from "react" +import { Grid, Theme, Typography, styled } from "@material-ui/core" +import { useDAO } from "services/services/dao/hooks/useDAO" +import { Edit } from "@material-ui/icons" +import { DelegationDialog } from "./DelegationModal" +import { useDelegationStatus } from "services/contracts/token/hooks/useDelegationStatus" +import { useTezos } from "services/beacon/hooks/useTezos" +import { useDelegationVoteWeight } from "services/contracts/token/hooks/useDelegationVoteWeight" +import BigNumber from "bignumber.js" +import { parseUnits } from "services/contracts/utils" + +export enum DelegationsType { + ACCEPTING_DELEGATION = "ACCEPTING_DELEGATION", + NOT_ACCEPTING_DELEGATION = "NOT_ACCEPTING_DELEGATION", + DELEGATING = "DELEGATING" +} + +const DelegationBox = styled(Grid)(({ theme }: { theme: Theme }) => ({ + minHeight: "178px", + padding: "46px 55px", + background: theme.palette.primary.main, + boxSizing: "border-box", + borderRadius: 8, + boxShadow: "none", + gap: 32 +})) + +const Subtitle = styled(Typography)({ + fontWeight: 200, + color: "#fff", + fontSize: 16 +}) + +const Balance = styled(Typography)({ + fontSize: 24, + fontWeight: 200 +}) + +export const matchTextToStatus = (value: DelegationsType | undefined) => { + switch (value) { + case DelegationsType.ACCEPTING_DELEGATION: + return "Accepting delegations" + case DelegationsType.NOT_ACCEPTING_DELEGATION: + return "Not currently accepting delegations or delegating" + case DelegationsType.DELEGATING: + return "Delegating to " + default: + return + } +} + +export const Delegation: React.FC<{ daoId: string }> = ({ daoId }) => { + const { data: dao } = useDAO(daoId) + const { network, tezos, account, connect } = useTezos() + + const { data: delegatedTo } = useDelegationStatus(dao?.data.token.contract) + const [delegationStatus, setDelegationStatus] = useState(DelegationsType.NOT_ACCEPTING_DELEGATION) + const [openModal, setOpenModal] = useState(false) + const { data: delegateVoteBalances } = useDelegationVoteWeight(dao?.data.token.contract) + const [voteWeight, setVoteWeight] = useState(new BigNumber(0)) + console.log("voteWeight: ", voteWeight.toString()) + + const onCloseAction = () => { + setOpenModal(false) + } + + useEffect(() => { + if (delegatedTo === account) { + setDelegationStatus(DelegationsType.ACCEPTING_DELEGATION) + } else if (delegatedTo && delegatedTo !== account) { + setDelegationStatus(DelegationsType.DELEGATING) + } else { + setDelegationStatus(DelegationsType.NOT_ACCEPTING_DELEGATION) + } + }, [delegatedTo, account]) + + useEffect(() => { + let totalVoteWeight = new BigNumber(0) + delegateVoteBalances?.forEach(delegatedVote => { + const balance = new BigNumber(delegatedVote.balance) + totalVoteWeight = totalVoteWeight.plus(balance) + }) + setVoteWeight(totalVoteWeight) + }, [delegateVoteBalances]) + + return ( + + + + Off-chain Delegation + + These settings only affect your participation in off-chain polls + + {dao && ( + + Voting Weight + + {!voteWeight || voteWeight.eq(new BigNumber(0)) ? ( + "-" + ) : ( + <>{`${parseUnits(voteWeight, dao.data.token.decimals).toString()} ${dao.data.token.symbol}`} + )} + + + )} + + + + Delegation Status + + + setOpenModal(true)} /> + setOpenModal(true)}> + Edit + + + + + {matchTextToStatus(delegationStatus)} + {delegationStatus === DelegationsType.DELEGATING ? delegatedTo : null} + + + + + ) +} diff --git a/src/modules/explorer/pages/User/components/DelegationModal.tsx b/src/modules/explorer/pages/User/components/DelegationModal.tsx new file mode 100644 index 00000000..d5b10b82 --- /dev/null +++ b/src/modules/explorer/pages/User/components/DelegationModal.tsx @@ -0,0 +1,171 @@ +import { Grid, Radio, TextField, Typography, styled } from "@material-ui/core" +import React, { useEffect, useState } from "react" +import { DelegationsType, matchTextToStatus } from "./DelegationBanner" +import { ResponsiveDialog } from "modules/explorer/components/ResponsiveDialog" +import { SmallButton } from "modules/common/SmallButton" +import { useTokenDelegate } from "services/contracts/token/hooks/useTokenDelegate" +import { useDAO } from "services/services/dao/hooks/useDAO" +import { useDAOID } from "../../DAO/router" +import { useTezos } from "services/beacon/hooks/useTezos" + +const AddressTextField = styled(TextField)({ + "backgroundColor": "#2f3438", + "borderRadius": 8, + "height": 56, + "padding": "0px 24px", + "alignItems": "flex-start", + "boxSizing": "border-box", + "justifyContent": "center", + "display": "flex", + "& .MuiInputBase-root": { + "width": "100%", + "& input": { + textAlign: "initial" + } + } +}) + +export enum ActionTypes { + ACCEPT_DELEGATIONS = "ACCEPT_DELEGATIONS", + DELEGATE = "DELEGATE", + CHANGE_DELEGATE = "CHANGE_DELEGATE", + STOP_ACCEPTING_DELEGATIONS = "STOP_ACCEPTING_DELEGATIONS", + STOP_DELEGATING = "STOP_DELEGATING" +} + +const matchTextToAction = (value: ActionTypes) => { + switch (value) { + case ActionTypes.ACCEPT_DELEGATIONS: + return "Accept Delegations" + case ActionTypes.DELEGATE: + return "Delegate" + case ActionTypes.CHANGE_DELEGATE: + return "Change Delegate" + case ActionTypes.STOP_ACCEPTING_DELEGATIONS: + return "Stop Accepting Delegations" + case ActionTypes.STOP_DELEGATING: + return "Stop Delegating" + default: + return + } +} + +export const DelegationDialog: React.FC<{ + open: boolean + onClose: () => void + status: DelegationsType | undefined + setDelegationStatus: (value: DelegationsType) => void + delegationStatus: DelegationsType + delegatedTo: string | null | undefined +}> = ({ status, onClose, open, setDelegationStatus, delegationStatus, delegatedTo }) => { + const [options, setOptions] = useState([]) + const [selectedOption, setSelectedOption] = useState() + const { mutate: delegateToken } = useTokenDelegate() + const daoId = useDAOID() + const { data, cycleInfo } = useDAO(daoId) + const { tezos, connect, network, account } = useTezos() + const [newDelegate, setNewDelegate] = useState("") + + useEffect(() => { + getOptionsByStatus(status) + }, [status]) + + const closeDialog = () => { + setSelectedOption(undefined) + onClose() + } + + const saveInfo = () => { + updateStatus() + closeDialog() + } + + const updateStatus = () => { + if (selectedOption === ActionTypes.DELEGATE || selectedOption === ActionTypes.CHANGE_DELEGATE) { + if (newDelegate && data?.data.token.contract) { + delegateToken({ tokenAddress: data?.data.token.contract, delegateAddress: newDelegate }) + } + } else if ( + selectedOption === ActionTypes.STOP_ACCEPTING_DELEGATIONS || + selectedOption === ActionTypes.STOP_DELEGATING + ) { + if (data?.data.token.contract) { + delegateToken({ tokenAddress: data?.data.token.contract, delegateAddress: null }) + } + } else if (selectedOption === ActionTypes.ACCEPT_DELEGATIONS) { + if (data?.data.token.contract && account) { + delegateToken({ tokenAddress: data?.data.token.contract, delegateAddress: account }) + } + } + } + + const getOptionsByStatus = (status: DelegationsType | undefined) => { + switch (status) { + case DelegationsType.NOT_ACCEPTING_DELEGATION: + const optionsOne = [ActionTypes.ACCEPT_DELEGATIONS, ActionTypes.DELEGATE] + setOptions(optionsOne) + break + case DelegationsType.ACCEPTING_DELEGATION: + const optionsTwo = [ActionTypes.STOP_ACCEPTING_DELEGATIONS] + setOptions(optionsTwo) + break + case DelegationsType.DELEGATING: + const optionsThree = [ActionTypes.CHANGE_DELEGATE, ActionTypes.STOP_DELEGATING, ActionTypes.ACCEPT_DELEGATIONS] + setOptions(optionsThree) + break + } + } + + return ( + + + + Current Status + + {matchTextToStatus(status)} {delegationStatus === DelegationsType.DELEGATING ? delegatedTo : null} + + + + {options.map(item => { + return ( + <> + + {matchTextToAction(item)} + setSelectedOption(e.target.value)} + value={item} + name="radio-buttons" + inputProps={{ "aria-label": "A" }} + /> + {item === selectedOption && + (selectedOption === ActionTypes.DELEGATE || selectedOption === ActionTypes.CHANGE_DELEGATE) ? ( + { + setNewDelegate(e.target.value) + }} + type="text" + placeholder="Enter Address" + InputProps={{ disableUnderline: true }} + /> + ) : null} + + + ) + })} + + + Submit + + + + ) +} diff --git a/src/modules/explorer/pages/User/index.tsx b/src/modules/explorer/pages/User/index.tsx index 08e332c7..ec243953 100644 --- a/src/modules/explorer/pages/User/index.tsx +++ b/src/modules/explorer/pages/User/index.tsx @@ -1,7 +1,7 @@ import { Box, Grid, Theme, Typography, styled } from "@material-ui/core" import dayjs from "dayjs" import { useDAOID } from "modules/explorer/pages/DAO/router" -import React, { useCallback, useEffect, useMemo } from "react" +import React, { useCallback, useEffect, useMemo, useState } from "react" import { useHistory } from "react-router" import { useAgoraTopic } from "services/agora/hooks/useTopic" import { useTezos } from "services/beacon/hooks/useTezos" @@ -18,6 +18,7 @@ import { UserBalances } from "../../components/UserBalances" import { UserProfileName } from "../../components/UserProfileName" import { DropButton } from "../Proposals" import { usePolls } from "modules/lite/explorer/hooks/usePolls" +import { Delegation } from "./components/DelegationBanner" const ContentBlockItem = styled(Grid)({ padding: "35px 52px", @@ -103,9 +104,6 @@ export const User: React.FC = () => { const { data, cycleInfo } = useDAO(daoId) const { data: proposals } = useProposals(daoId) const history = useHistory() - const { data: activeProposals } = useProposals(daoId, ProposalStatus.ACTIVE) - const { data: executableProposals } = useProposals(daoId, ProposalStatus.EXECUTABLE) - const { data: expiredProposals } = useProposals(daoId, ProposalStatus.EXPIRED) const { data: executedProposals } = useProposals(daoId, ProposalStatus.EXECUTED) const { data: droppedProposals } = useProposals(daoId, ProposalStatus.DROPPED) const { mutate: unstakeFromAllProposals } = useUnstakeFromAllProposals() @@ -208,6 +206,8 @@ export const User: React.FC = () => { + + {proposalsCreated && cycleInfo && ( ({ "& > *": { @@ -40,16 +41,25 @@ export const ChangeNetworkButton = () => { const { network } = useTezos() const { open } = useActionSheet(ActionSheet.Network) + const location = useLocation() + + const canShow = + location.pathname.indexOf("/explorer/dao/") === -1 && location.pathname.indexOf("/explorer/lite/dao/") === -1 + return ( - open()}> - - - - - - {capitalize(network)} - - - + <> + {canShow ? ( + open()}> + + + + + + {capitalize(network)} + + + + ) : null} + ) } diff --git a/src/services/bakingBad/delegations/index.ts b/src/services/bakingBad/delegations/index.ts index 2cab41bd..a2a4a7eb 100644 --- a/src/services/bakingBad/delegations/index.ts +++ b/src/services/bakingBad/delegations/index.ts @@ -1,6 +1,7 @@ import { Network } from "services/beacon" import { networkNameMap } from ".." -import { DelegationDTO } from "./types" +import { DelegationDTO, TokenDelegationDTO, UserDelegateBalance } from "./types" +import { getUserTokenBalance } from "../tokenBalances" export const getLatestDelegation = async (daoAddress: string, network: Network) => { const url = `https://api.${networkNameMap[network]}.tzkt.io/v1/operations/delegations?sender=${daoAddress}&status=applied` @@ -17,3 +18,64 @@ export const getLatestDelegation = async (daoAddress: string, network: Network) return resultingDelegations[0] } + +export const getTokenDelegation = async (tokenAddress: string, account: string, network: Network) => { + const url = `https://api.${networkNameMap[network]}.tzkt.io/v1/contracts/${tokenAddress}/bigmaps/delegates/keys?key.eq=${account}&active=true` + const response = await fetch(url) + + if (!response.ok) { + throw new Error("Failed to fetch token delegations from TZKT API") + } + + const resultingDelegations: TokenDelegationDTO[] = await response.json() + + if (resultingDelegations.length === 0) { + return null + } + + const delegatedTo = resultingDelegations[0].value + + return delegatedTo +} + +export const getTokenDelegationVoteWeight = async (tokenAddress: string, account: string, network: Network) => { + const selfBalance = await getUserTokenBalance(account, network, tokenAddress) + + if (!selfBalance) { + throw new Error("Could not fetch delegate token balance from the TZKT API") + } + + const url = `https://api.${networkNameMap[network]}.tzkt.io/v1/contracts/${tokenAddress}/bigmaps/delegates/keys?value.eq=${account}&active=true` + const response = await fetch(url) + + if (!response.ok) { + throw new Error("Failed to fetch token delegations from TZKT API") + } + + const resultingDelegations: TokenDelegationDTO[] = await response.json() + + const delegateBalance: UserDelegateBalance = { + address: account, + balance: selfBalance + } + + if (resultingDelegations.length === 0) { + return [delegateBalance] + } + + const delegatedAddressBalances: UserDelegateBalance[] = [] + + await Promise.all( + resultingDelegations.map(async del => { + const balance = await getUserTokenBalance(del.key, network, tokenAddress) + if (balance) { + delegatedAddressBalances.push({ + address: del.key, + balance: balance + }) + } + }) + ) + + return delegatedAddressBalances +} diff --git a/src/services/bakingBad/delegations/types.ts b/src/services/bakingBad/delegations/types.ts index 21b8ffea..a22e35a3 100644 --- a/src/services/bakingBad/delegations/types.ts +++ b/src/services/bakingBad/delegations/types.ts @@ -23,3 +23,19 @@ export interface DelegationDTO { } status: string } + +export interface TokenDelegationDTO { + id: number + active: boolean + hash: string + key: string + value: string + firstLevel: number + lastLevel: number + updates: number +} + +export interface UserDelegateBalance { + address: string + balance: string +} diff --git a/src/services/beacon/hooks/useTezos.ts b/src/services/beacon/hooks/useTezos.ts index c1dcca37..4c50e130 100644 --- a/src/services/beacon/hooks/useTezos.ts +++ b/src/services/beacon/hooks/useTezos.ts @@ -1,7 +1,7 @@ import { useQueryClient } from "react-query" import { useCallback, useContext } from "react" import { MichelCodecPacker, TezosToolkit } from "@taquito/taquito" -import { connectWithBeacon, Network, rpcNodes, TezosActionType } from "services/beacon" +import { connectWithBeacon, createTezos, Network, rpcNodes, TezosActionType } from "services/beacon" import { TezosContext } from "services/beacon/context" import { Tzip16Module } from "@taquito/tzip16" import mixpanel from "mixpanel-browser" @@ -17,14 +17,6 @@ type WalletConnectReturn = { wallet: BeaconWallet | undefined } -export const initTezosInstance = (network: Network) => { - const newTezos = new TezosToolkit(rpcNodes[network]) - newTezos.setPackerProvider(new MichelCodecPacker()) - newTezos.addExtension(new Tzip16Module()) - - return newTezos -} - export const useTezos = (): WalletConnectReturn => { const { state: { tezos, network, account, wallet }, @@ -37,7 +29,7 @@ export const useTezos = (): WalletConnectReturn => { async (newNetwork?: Network) => { const { wallet } = await connectWithBeacon(network) - const newTezos: TezosToolkit = initTezosInstance(network || newNetwork) + const newTezos: TezosToolkit = createTezos(network || newNetwork) newTezos.setProvider({ wallet }) const account = await newTezos.wallet.pkh() @@ -74,28 +66,35 @@ export const useTezos = (): WalletConnectReturn => { }, [dispatch, wallet]), changeNetwork: async (newNetwork: Network) => { mixpanel.register({ Network: newNetwork }) - localStorage.setItem("homebase:network", newNetwork) - - if (!("_pkh" in tezos.wallet)) { - const Tezos = new TezosToolkit(rpcNodes[newNetwork]) - Tezos.setPackerProvider(new MichelCodecPacker()) - Tezos.addExtension(new Tzip16Module()) - + const newTezos: TezosToolkit = createTezos(newNetwork) + if (!account) { dispatch({ type: TezosActionType.UPDATE_TEZOS, payload: { network: newNetwork, - tezos: Tezos, - account, + tezos: newTezos, + account: "", wallet: undefined } }) } else { - await connect(newNetwork) + const { wallet } = await connectWithBeacon(newNetwork) + newTezos.setProvider({ wallet }) + const newAccount = await newTezos.wallet.pkh() + + dispatch({ + type: TezosActionType.UPDATE_TEZOS, + payload: { + network: newNetwork, + tezos: newTezos, + account: newAccount, + wallet + } + }) } + queryClient.resetQueries() - location.reload() }, account, network, diff --git a/src/services/beacon/utils.ts b/src/services/beacon/utils.ts index 4c200dc1..179ae0ff 100644 --- a/src/services/beacon/utils.ts +++ b/src/services/beacon/utils.ts @@ -31,23 +31,16 @@ export const getTezosNetwork = (): Network => { return envNetwork } -let beaconWallet: BeaconWallet - -export const createWallet = (network: Network) => { - if (!beaconWallet) { - beaconWallet = new BeaconWallet({ - name: "Homebase", - iconUrl: "https://tezostaquito.io/img/favicon.png", - preferredNetwork: network as NetworkType, - walletConnectOptions: { - projectId: "1641355e825aeaa926e843dd38b04f6f", // Project ID can be customised - relayUrl: "wss://relay.walletconnect.com" // WC2 relayUrl can be customised - } - }) - } - - return beaconWallet -} +export const createWallet = (network: Network) => + new BeaconWallet({ + name: "Homebase", + iconUrl: "https://tezostaquito.io/img/favicon.png", + preferredNetwork: network as NetworkType, + walletConnectOptions: { + projectId: "1641355e825aeaa926e843dd38b04f6f", // Project ID can be customised + relayUrl: "wss://relay.walletconnect.com" // WC2 relayUrl can be customised + } + }) export const createTezos = (network: Network) => { const tezos = new TezosToolkit(rpcNodes[network]) diff --git a/src/services/config/constants.ts b/src/services/config/constants.ts index 725b4a5e..562c6290 100644 --- a/src/services/config/constants.ts +++ b/src/services/config/constants.ts @@ -15,7 +15,8 @@ export enum EnvKey { REACT_APP_V2_URL = "REACT_APP_V2_URL", REACT_APP_LITE_API_URL = "REACT_APP_LITE_API_URL", REACT_APP_API_URL = "REACT_APP_API_URL", - REACT_APP_BASE_URL = "REACT_APP_BASE_URL" + REACT_APP_BASE_URL = "REACT_APP_BASE_URL", + REACT_APP_DAO_DEPLOYER_API = "REACT_APP_DAO_DEPLOYER_API" } export enum FeatureFlag { diff --git a/src/services/contracts/baseDAO/hooks/useOriginate.ts b/src/services/contracts/baseDAO/hooks/useOriginate.ts index 0e01e961..c39d21dc 100644 --- a/src/services/contracts/baseDAO/hooks/useOriginate.ts +++ b/src/services/contracts/baseDAO/hooks/useOriginate.ts @@ -5,8 +5,8 @@ import { ContractAbstraction, ContractProvider, TezosToolkit, Wallet } from "@ta import { useMutation, useQueryClient } from "react-query" import { deployMetadataCarrier } from "services/contracts/metadataCarrier/deploy" -import { initTezosInstance, useTezos } from "services/beacon/hooks/useTezos" -import { BaseDAO } from ".." +import { useTezos } from "services/beacon/hooks/useTezos" +import { BaseDAO, replacer } from ".." import { getDAO } from "services/services/dao/services" import mixpanel from "mixpanel-browser" import { InMemorySigner } from "@taquito/signer" @@ -14,6 +14,7 @@ import { ALICE_PRIV_KEY } from "services/beacon" import { getSignature } from "services/lite/utils" import { saveLiteCommunity } from "services/services/lite/lite-services" import { Community } from "models/Community" +import { EnvKey, getEnv } from "services/config" const INITIAL_STATES = [ { @@ -28,6 +29,14 @@ const INITIAL_STATES = [ activeText: "", completedText: "" }, + { + activeText: "", + completedText: "" + }, + { + activeText: "", + completedText: "" + }, { activeText: "", completedText: "" @@ -69,108 +78,154 @@ export const useOriginate = (template: DAOTemplate) => { const { tezos, connect, network, account, wallet } = useTezos() const result = useMutation, Error, OriginateParams>( - async ({ metadataParams, params }) => { + async ({ metadataParams, params, deploymentMethod }) => { const updatedStates = INITIAL_STATES - updatedStates[0] = { - activeText: "Deploying Metadata Carrier Contract", - completedText: "" - } + let contract - setActiveState(0) - setStates(updatedStates) + if (deploymentMethod === "managed") { + const deployParams: any = { + params: { ...params }, + metadataParams: { ...metadataParams } + } - let newTezos: TezosToolkit = tezos + updatedStates[0] = { + activeText: `Deploying ${template} DAO Contract`, + completedText: "" + } - if (network !== "mainnet") { - newTezos = initTezosInstance(network) - const signer = await InMemorySigner.fromSecretKey(ALICE_PRIV_KEY) - newTezos.setProvider({ signer }) + setActiveState(0) + setStates(updatedStates) - params.orgSettings.administrator = await newTezos.wallet.pkh() - } + const resp = await fetch(`${getEnv(EnvKey.REACT_APP_DAO_DEPLOYER_API)}/deploy`, { + method: "POST", + body: JSON.stringify({ deployParams: deployParams }, replacer), + headers: { "Content-Type": "application/json" } + }) - mixpanel.track("Started DAO origination", { - contract: "MetadataCarrier", - daoName: params.orgSettings.name, - daoType: params.template - }) + const data = await resp.json() + console.log(data) + const address = data.address - const metadata = await deployMetadataCarrier({ - ...metadataParams, - tezos: newTezos, - connect - }) + contract = await tezos.wallet.at(address) - if (!metadata) { - throw new Error(`Could not deploy ${template}DAO because MetadataCarrier contract deployment failed`) - } + mixpanel.track("Started DAO origination", { + contract: "BaseDAO", + daoName: params.orgSettings.name + }) - updatedStates[0] = { - ...updatedStates[0], - completedText: `Deployed Metadata Carrier with address "${metadata.deployAddress}" and key "${metadata.keyName}"` - } + if (!contract) { + throw new Error(`Error deploying ${template}DAO`) + } - updatedStates[1] = { - activeText: `Deploying ${template} DAO Contract`, - completedText: "" - } + updatedStates[1] = { + ...updatedStates[1], + completedText: `Deployed ${template} DAO contract with address "${contract.address}"` + } - setActiveState(1) - setStates(updatedStates) + setActiveState(1) + setStates(updatedStates) - mixpanel.track("Started DAO origination", { - contract: "BaseDAO", - daoName: params.orgSettings.name - }) + updatedStates[2] = { + activeText: `Waiting for DAO to be indexed`, + completedText: "" + } - const contract = await BaseDAO.baseDeploy(template, { - tezos: newTezos, - metadata, - params, - network - }) + setActiveState(2) + setStates(updatedStates) + } else { + updatedStates[0] = { + activeText: "Deploying Metadata Carrier Contract", + completedText: "" + } - if (!contract) { - throw new Error(`Error deploying ${template}DAO`) - } + setActiveState(0) + setStates(updatedStates) - updatedStates[1] = { - ...updatedStates[1], - completedText: `Deployed ${template} DAO contract with address "${contract.address}"` - } + mixpanel.track("Started DAO origination", { + contract: "MetadataCarrier", + daoName: params.orgSettings.name, + daoType: params.template + }) - updatedStates[2] = { - activeText: `Waiting for DAO ownership to be transferred`, - completedText: "" - } + const metadata = await deployMetadataCarrier({ + ...metadataParams, + tezos, + connect + }) - setActiveState(2) - setStates(updatedStates) + if (!metadata) { + throw new Error(`Could not deploy ${template}DAO because MetadataCarrier contract deployment failed`) + } - const tx = await BaseDAO.transfer_ownership(contract.address, contract.address, newTezos) + updatedStates[0] = { + ...updatedStates[0], + completedText: `Deployed Metadata Carrier with address "${metadata.deployAddress}" and key "${metadata.keyName}"` + } - if (!tx) { - throw new Error(`Error transferring ownership of ${template}DAO to itself`) - } + updatedStates[1] = { + activeText: `Deploying ${template} DAO Contract`, + completedText: "" + } - updatedStates[2] = { - ...updatedStates[2], - completedText: `Ownership of ${template} DAO transferred to the DAO "${contract.address}"` - } + setActiveState(1) + setStates(updatedStates) - updatedStates[3] = { - activeText: `Waiting for DAO to be indexed`, - completedText: "" - } + mixpanel.track("Started DAO origination", { + contract: "BaseDAO", + daoName: params.orgSettings.name + }) - setActiveState(3) - setStates(updatedStates) + contract = await BaseDAO.baseDeploy(template, { + tezos, + metadata, + params, + network + }) - mixpanel.track("Completed DAO creation", { - daoName: params.orgSettings.name, - daoType: params.template - }) + if (!contract) { + throw new Error(`Error deploying ${template}DAO`) + } + + updatedStates[1] = { + ...updatedStates[1], + completedText: `Deployed ${template} DAO contract with address "${contract.address}"` + } + + updatedStates[2] = { + activeText: `Waiting for DAO ownership to be transferred`, + completedText: "" + } + + setActiveState(2) + setStates(updatedStates) + + const tx = await BaseDAO.transfer_ownership(contract.address, contract.address, tezos) + + if (!tx) { + throw new Error(`Error transferring ownership of ${template}DAO to itself`) + } + + updatedStates[2] = { + ...updatedStates[2], + completedText: `Ownership of ${template} DAO transferred to the DAO "${contract.address}"` + } + + updatedStates[3] = { + activeText: `Waiting for DAO to be indexed`, + completedText: "" + } + + setActiveState(3) + setStates(updatedStates) + + mixpanel.track("Completed DAO creation", { + daoName: params.orgSettings.name, + daoType: params.template + }) + } + + console.log("This is the contract deployed", contract) mixpanel.track("Waiting for DAO indexation", { daoName: params.orgSettings.name, @@ -179,11 +234,9 @@ export const useOriginate = (template: DAOTemplate) => { const indexed = await waitForIndexation(contract.address) - updatedStates[3] = { - ...updatedStates[3], - completedText: indexed - ? `Deployed ${metadataParams.metadata.unfrozenToken.name} successfully` - : `Deployed ${metadataParams.metadata.unfrozenToken.name} successfully, but metadata has not been indexed yet. This usually takes a few minutes, your DAO page may not be available yet.` + updatedStates[4] = { + activeText: `Deployling Lite DAO`, + completedText: "" } setActiveState(4) @@ -208,7 +261,25 @@ export const useOriginate = (template: DAOTemplate) => { const { signature, payloadBytes } = await getSignature(account, wallet, JSON.stringify(values)) const publicKey = (await wallet?.client.getActiveAccount())?.publicKey - const resp = await saveLiteCommunity(signature, publicKey, payloadBytes) + await saveLiteCommunity(signature, publicKey, payloadBytes) + + updatedStates[4] = { + activeText: "", + completedText: "Successfully deployed Lite DAO" + } + + setActiveState(5) + setStates(updatedStates) + + updatedStates[5] = { + ...updatedStates[5], + completedText: indexed + ? `Deployed ${metadataParams.metadata.unfrozenToken.name} successfully` + : `Deployed ${metadataParams.metadata.unfrozenToken.name} successfully, but metadata has not been indexed yet. This usually takes a few minutes, your DAO page may not be available yet.` + } + + setActiveState(6) + setStates(updatedStates) } mixpanel.track("Completed DAO indexation", { diff --git a/src/services/contracts/baseDAO/types.ts b/src/services/contracts/baseDAO/types.ts index 95b426a5..527caa16 100644 --- a/src/services/contracts/baseDAO/types.ts +++ b/src/services/contracts/baseDAO/types.ts @@ -2,7 +2,7 @@ import { ContractAbstraction, TezosToolkit, Wallet } from "@taquito/taquito" import { MetadataCarrierDeploymentData, MetadataCarrierParameters } from "services/contracts/metadataCarrier/types" import { BigNumber } from "bignumber.js" -import { MigrationParams } from "modules/creator/state" +import { DeploymentMethod, MigrationParams } from "modules/creator/state" import { Token as TokenModel } from "models/Token" export type Contract = ContractAbstraction | undefined @@ -60,6 +60,7 @@ export interface ConfigProposalParams { export interface OriginateParams { metadataParams: MetadataCarrierParameters params: MigrationParams + deploymentMethod: DeploymentMethod } export interface VoteParams { diff --git a/src/services/contracts/baseDAO/utils.ts b/src/services/contracts/baseDAO/utils.ts index ec096355..ee93996d 100644 --- a/src/services/contracts/baseDAO/utils.ts +++ b/src/services/contracts/baseDAO/utils.ts @@ -61,3 +61,12 @@ export const calculateCycleInfo = (originationTime: string, votingPeriod: number export const unpackExtraNumValue = (bytes: string): BigNumber => { return new BigNumber((unpackDataBytes({ bytes }) as { int: string }).int) } + +export const replacer = (name: any, val: any) => { + // convert Number to string + if (val && val.constructor === BigNumber) { + return val.toString() + } else { + return val // return as is + } +} diff --git a/src/services/contracts/token/hooks/useDelegationStatus.ts b/src/services/contracts/token/hooks/useDelegationStatus.ts new file mode 100644 index 00000000..9158a87a --- /dev/null +++ b/src/services/contracts/token/hooks/useDelegationStatus.ts @@ -0,0 +1,27 @@ +import { useQuery } from "react-query" +import { getTokenDelegation } from "services/bakingBad/delegations" +import { getDAOBalances } from "services/bakingBad/tokenBalances" +import { useTezos } from "services/beacon/hooks/useTezos" + +export const useDelegationStatus = (tokenAddress: string | undefined) => { + const { network, tezos, account, connect } = useTezos() + + const { data, ...rest } = useQuery( + ["tokenDelegations", tokenAddress], + async () => { + if (!tokenAddress) { + return null + } else { + return await getTokenDelegation(tokenAddress, account, network) + } + }, + { + enabled: !!tokenAddress + } + ) + + return { + data, + ...rest + } +} diff --git a/src/services/contracts/token/hooks/useDelegationVoteWeight.ts b/src/services/contracts/token/hooks/useDelegationVoteWeight.ts new file mode 100644 index 00000000..93a26dff --- /dev/null +++ b/src/services/contracts/token/hooks/useDelegationVoteWeight.ts @@ -0,0 +1,25 @@ +import { useQuery } from "react-query" +import { getTokenDelegationVoteWeight } from "services/bakingBad/delegations" +import { UserDelegateBalance } from "services/bakingBad/delegations/types" +import { useTezos } from "services/beacon/hooks/useTezos" + +export const useDelegationVoteWeight = (tokenAddress: string | undefined) => { + const { network, account } = useTezos() + + const { data, ...rest } = useQuery( + ["delegationVoteWeight", tokenAddress], + async () => { + if (tokenAddress) { + return await getTokenDelegationVoteWeight(tokenAddress, account, network) + } + }, + { + enabled: !!tokenAddress + } + ) + + return { + data, + ...rest + } +} diff --git a/src/services/contracts/token/hooks/useToken.ts b/src/services/contracts/token/hooks/useToken.ts index 66526503..ba2058d1 100644 --- a/src/services/contracts/token/hooks/useToken.ts +++ b/src/services/contracts/token/hooks/useToken.ts @@ -9,51 +9,62 @@ import mixpanel from "mixpanel-browser" import { TokenContractParams } from "modules/creator/deployment/state/types" import { getCurrentBlock } from "services/utils/utils" import { deployTokenContract } from "services/contracts/token" +import { useNotification } from "modules/common/hooks/useNotification" export const useTokenOriginate = (tokenData: TokenContractParams) => { const queryClient = useQueryClient() const { tezos, connect, network, account } = useTezos() + const openNotification = useNotification() const result = useMutation, Error, TokenContractParams>( async ({ tokenDistribution, tokenSettings }) => { - let tezosToolkit = tezos + try { + let tezosToolkit = tezos - if (!account) { - tezosToolkit = await connect() - } + if (!account) { + tezosToolkit = await connect() + } - mixpanel.track("Started Token origination", { - contract: "FA2Token", - tokenName: tokenSettings.name, - tokenSymbol: tokenSettings.symbol - }) + mixpanel.track("Started Token origination", { + contract: "FA2Token", + tokenName: tokenSettings.name, + tokenSymbol: tokenSettings.symbol + }) - const mutateTokenData: TokenContractParams = { - tokenDistribution, - tokenSettings - } + const mutateTokenData: TokenContractParams = { + tokenDistribution, + tokenSettings + } - const currentBlock = await getCurrentBlock(network) + const currentBlock = await getCurrentBlock(network) - const contract = await deployTokenContract({ - ...mutateTokenData, - tezos: tezosToolkit, - account, - currentBlock - }) + const contract = await deployTokenContract({ + ...mutateTokenData, + tezos: tezosToolkit, + account, + currentBlock + }) - if (!contract) { - throw new Error(`Error deploying ${tokenData.tokenSettings.name} Token`) - } + if (!contract) { + throw new Error(`Error deploying ${tokenData.tokenSettings.name} Token`) + } - mixpanel.track("Completed Token Deployment", { - contract: "FA2Token", - tokenName: tokenSettings.name, - tokenSymbol: tokenSettings.symbol - }) + mixpanel.track("Completed Token Deployment", { + contract: "FA2Token", + tokenName: tokenSettings.name, + tokenSymbol: tokenSettings.symbol + }) - return contract + return contract + } catch (error) { + openNotification({ + message: (error as Error).message, + variant: "error", + autoHideDuration: 2000 + }) + return error + } }, { onSuccess: () => { diff --git a/src/services/contracts/token/hooks/useTokenDelegate.ts b/src/services/contracts/token/hooks/useTokenDelegate.ts new file mode 100644 index 00000000..415d5d46 --- /dev/null +++ b/src/services/contracts/token/hooks/useTokenDelegate.ts @@ -0,0 +1,63 @@ +import { useNotification } from "modules/common/hooks/useNotification" +import { useMutation, useQueryClient } from "react-query" +import { useTezos } from "services/beacon/hooks/useTezos" +import { networkNameMap } from "../../../bakingBad" +import { setDelegate } from ".." +import { WalletOperation } from "@taquito/taquito" + +export const useTokenDelegate = () => { + const queryClient = useQueryClient() + const openNotification = useNotification() + const { network, tezos, account, connect } = useTezos() + + return useMutation( + async params => { + const { tokenAddress, delegateAddress } = params + // const { key: flushNotification, closeSnackbar: closeFlushNotification } = openNotification({ + // message: "Please sign the transaction to flush", + // persist: true, + // variant: "info" + // }) + try { + let tezosToolkit = tezos + + if (!account) { + tezosToolkit = await connect() + } + + const tx = await setDelegate({ + tokenAddress, + tezos: tezosToolkit, + delegateAddress + }) + // closeFlushNotification(flushNotification) + + if (!tx) { + throw new Error(`Error making delegate transaction`) + } + + openNotification({ + message: "Delegate transaction confirmed!", + autoHideDuration: 5000, + variant: "success", + detailsLink: `https://${networkNameMap[network]}.tzkt.io/` + (tx as WalletOperation).opHash + }) + + return tx + } catch (e: any) { + // closeFlushNotification(flushNotification) + openNotification({ + message: (e as Error).message, + variant: "error", + autoHideDuration: 5000 + }) + return new Error((e as Error).message) + } + }, + { + onSuccess: () => { + queryClient.resetQueries() + } + } + ) +} diff --git a/src/services/contracts/token/index.ts b/src/services/contracts/token/index.ts index b235a152..a8904a4a 100644 --- a/src/services/contracts/token/index.ts +++ b/src/services/contracts/token/index.ts @@ -3,6 +3,7 @@ import BigNumber from "bignumber.js" import { TokenContractParams } from "modules/creator/deployment/state/types" import { formatUnits } from "../utils" import fa2_single_asset_delegated from "./assets/fa2_single_asset_delegated" +import { getContract } from "../baseDAO" interface Tezos { tezos: TezosToolkit @@ -91,5 +92,24 @@ export const deployTokenContract = async ({ return contract } catch (e) { console.error(e) + return e + } +} + +export const setDelegate = async ({ + tokenAddress, + delegateAddress, + tezos +}: { + tokenAddress: string + delegateAddress: string | null + tezos: TezosToolkit +}) => { + try { + const contract = await getContract(tezos, tokenAddress) + return contract.methods.set_delegate(delegateAddress).send() + } catch (e) { + console.error(e) + return e } } diff --git a/src/services/services/lite/lite-services.ts b/src/services/services/lite/lite-services.ts index af2f767d..52d8e308 100644 --- a/src/services/services/lite/lite-services.ts +++ b/src/services/services/lite/lite-services.ts @@ -70,7 +70,14 @@ export const getLiteDAOs = async (network: string) => { name: dao.name, decimals: Number(dao.decimals), standard: dao.tokenType - } + }, + ledgers: dao.members.map(member => { + return { + holder: { + address: member + } + } + }) } return new_dao }) diff --git a/src/theme/legacy.ts b/src/theme/legacy.ts index b4a1b84c..e24bee3e 100644 --- a/src/theme/legacy.ts +++ b/src/theme/legacy.ts @@ -32,7 +32,7 @@ export const legacyTheme = createMuiTheme({ }, subtitle1: { fontSize: 18, - fontWeight: 400, + fontWeight: 300, lineHeight: "26.33px", letterSpacing: "-0.01em" }, @@ -43,7 +43,7 @@ export const legacyTheme = createMuiTheme({ letterSpacing: "-0.01em" }, h3: { - fontSize: 35, + fontSize: 32, fontWeight: 500, fontFamily: "Roboto Mono" }, @@ -241,9 +241,7 @@ export const legacyTheme = createMuiTheme({ color: "#fff" }, root: { - "&::placeholder": { - fontWeight: 300 - } + fontWeight: 300 } }, MuiDivider: {