From 3eba845314b259e620b42a24f07c4715e2fab772 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Fri, 26 Jun 2020 18:57:17 -0300 Subject: [PATCH] WIP: add `Advanced` settings there's a lot of duplicated code, just copying functionalities and building basic structure for the section --- .../Advanced/ChangeThreshold/index.tsx | 128 ++++++++++++ .../Advanced/ChangeThreshold/style.ts | 43 ++++ .../Settings/Advanced/dataFetcher.ts | 40 ++++ .../components/Settings/Advanced/index.tsx | 196 ++++++++++++++++++ .../components/Settings/Advanced/style.ts | 57 +++++ src/routes/safe/components/Settings/index.tsx | 17 +- src/routes/safe/components/Settings/style.ts | 14 +- 7 files changed, 485 insertions(+), 10 deletions(-) create mode 100644 src/routes/safe/components/Settings/Advanced/ChangeThreshold/index.tsx create mode 100644 src/routes/safe/components/Settings/Advanced/ChangeThreshold/style.ts create mode 100644 src/routes/safe/components/Settings/Advanced/dataFetcher.ts create mode 100644 src/routes/safe/components/Settings/Advanced/index.tsx create mode 100644 src/routes/safe/components/Settings/Advanced/style.ts diff --git a/src/routes/safe/components/Settings/Advanced/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/Advanced/ChangeThreshold/index.tsx new file mode 100644 index 00000000..d9f1f765 --- /dev/null +++ b/src/routes/safe/components/Settings/Advanced/ChangeThreshold/index.tsx @@ -0,0 +1,128 @@ +import IconButton from '@material-ui/core/IconButton' +import MenuItem from '@material-ui/core/MenuItem' +import { withStyles } from '@material-ui/core/styles' +import Close from '@material-ui/icons/Close' +import React, { useEffect, useState } from 'react' + +import { styles } from './style' + +import Field from 'src/components/forms/Field' +import GnoForm from 'src/components/forms/GnoForm' +import SelectField from 'src/components/forms/SelectField' +import { composeValidators, differentFrom, minValue, mustBeInteger, required } from 'src/components/forms/validator' +import Block from 'src/components/layout/Block' +import Button from 'src/components/layout/Button' +import Col from 'src/components/layout/Col' +import Hairline from 'src/components/layout/Hairline' +import Paragraph from 'src/components/layout/Paragraph' +import Row from 'src/components/layout/Row' +import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' +import { estimateTxGasCosts } from 'src/logic/safe/transactions/gasNew' +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { getWeb3 } from 'src/logic/wallets/getWeb3' + +const THRESHOLD_FIELD_NAME = 'threshold' + +const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddress, threshold }) => { + const [gasCosts, setGasCosts] = useState('< 0.001') + + useEffect(() => { + let isCurrent = true + const estimateGasCosts = async () => { + const web3 = getWeb3() + const { fromWei, toBN } = web3.utils + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + const txData = safeInstance.contract.methods.changeThreshold('1').encodeABI() + const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData) + const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') + const formattedGasCosts = formatAmount(gasCostsAsEth) + if (isCurrent) { + setGasCosts(formattedGasCosts) + } + } + + estimateGasCosts() + + return () => { + isCurrent = false + } + }, [safeAddress]) + + const handleSubmit = (values) => { + const newThreshold = values[THRESHOLD_FIELD_NAME] + + onClose() + onChangeThreshold(newThreshold) + } + + return ( + <> + + + Change required confirmations + + + + + + + + {() => ( + <> + + + Any transaction requires the confirmation of: + + + + ( + <> + + {[...Array(Number(owners.size))].map((x, index) => ( + + {index + 1} + + ))} + + {props.meta.error && props.meta.touched && ( + + {props.meta.error} + + )} + + )} + validate={composeValidators(required, mustBeInteger, minValue(1), differentFrom(threshold))} + /> + + + + {`out of ${owners.size} owner(s)`} + + + + + + {`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`} + + + + + + + + + + )} + + + ) +} + +export default withStyles(styles as any)(ChangeThreshold) diff --git a/src/routes/safe/components/Settings/Advanced/ChangeThreshold/style.ts b/src/routes/safe/components/Settings/Advanced/ChangeThreshold/style.ts new file mode 100644 index 00000000..e0f9c000 --- /dev/null +++ b/src/routes/safe/components/Settings/Advanced/ChangeThreshold/style.ts @@ -0,0 +1,43 @@ +import { lg, md, secondaryText, sm } from 'src/theme/variables' + +export const styles = () => ({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'space-between', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: secondaryText, + marginRight: 'auto', + marginLeft: '20px', + }, + headingText: { + fontSize: '20px', + }, + close: { + height: '35px', + width: '35px', + }, + modalContent: { + padding: `${md} ${lg}`, + }, + ownersText: { + marginLeft: sm, + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + position: 'absolute', + bottom: 0, + width: '100%', + }, + inputRow: { + position: 'relative', + }, + errorText: { + position: 'absolute', + bottom: '-25px', + }, +}) diff --git a/src/routes/safe/components/Settings/Advanced/dataFetcher.ts b/src/routes/safe/components/Settings/Advanced/dataFetcher.ts new file mode 100644 index 00000000..01117650 --- /dev/null +++ b/src/routes/safe/components/Settings/Advanced/dataFetcher.ts @@ -0,0 +1,40 @@ +import { List, Set } from 'immutable' + +export const MODULES_TABLE_ADDRESS_ID = 'address' +export const MODULES_TABLE_ACTIONS_ID = 'actions' + +export const getModuleData = (modules: Set): List<{ [MODULES_TABLE_ADDRESS_ID]: string }> => { + return modules.toList().map((module) => ({ + [MODULES_TABLE_ADDRESS_ID]: module, + })) +} + +interface TableColumn { + id: string + order: boolean + disablePadding: boolean + label: string + custom: boolean + align?: string +} + +export const generateColumns = (): List => { + const addressColumn: TableColumn = { + id: MODULES_TABLE_ADDRESS_ID, + order: false, + disablePadding: false, + label: 'Address', + custom: false, + align: 'left', + } + + const actionsColumn: TableColumn = { + id: MODULES_TABLE_ACTIONS_ID, + order: false, + disablePadding: false, + label: '', + custom: true, + } + + return List([addressColumn, actionsColumn]) +} diff --git a/src/routes/safe/components/Settings/Advanced/index.tsx b/src/routes/safe/components/Settings/Advanced/index.tsx new file mode 100644 index 00000000..13473375 --- /dev/null +++ b/src/routes/safe/components/Settings/Advanced/index.tsx @@ -0,0 +1,196 @@ +import { Set } from 'immutable' +import { makeStyles } from '@material-ui/core/styles' +// import { useSnackbar } from 'notistack' +import React, { useState } from 'react' +import { /*useDispatch, */ useSelector } from 'react-redux' +import cn from 'classnames' + +// import ChangeThreshold from './ChangeThreshold' +import { styles } from './style' + +import Modal from 'src/components/Modal' +import Block from 'src/components/layout/Block' +import Bold from 'src/components/layout/Bold' +// import Button from 'src/components/layout/Button' +import Heading from 'src/components/layout/Heading' +import Paragraph from 'src/components/layout/Paragraph' +// import Row from 'src/components/layout/Row' +// import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' +// import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' +// import { grantedSelector } from 'src/routes/safe/container/selector' +// import createTransaction from 'src/routes/safe/store/actions/createTransaction' +import { + // safeOwnersSelector, + // safeParamAddressFromStateSelector, + // safeThresholdSelector, + safeNonceSelector, + safeModulesSelector, +} from 'src/routes/safe/store/selectors' +import DividerLine from 'src/components/DividerLine' +import TableContainer from '@material-ui/core/TableContainer' +import Table from '../../../../../components/Table' +import TableRow from '@material-ui/core/TableRow' +import TableCell from '@material-ui/core/TableCell' +import { cellWidth } from '../../../../../components/Table/TableHead' +import OwnerAddressTableCell from '../ManageOwners/OwnerAddressTableCell' +import Row from '../../../../../components/layout/Row' +import Img from '../../../../../components/layout/Img' +// import RenameOwnerIcon from '../ManageOwners/assets/icons/rename-owner.svg' +// import ReplaceOwnerIcon from '../ManageOwners/assets/icons/replace-owner.svg' +import RemoveOwnerIcon from '../assets/icons/bin.svg' +import { generateColumns, MODULES_TABLE_ADDRESS_ID, getModuleData } from './dataFetcher' +import { grantedSelector } from '../../../container/selector' +// import RemoveOwnerModal from '../ManageOwners/RemoveOwnerModal' +// import { getOwnersWithNameFromAddressBook } from '../../../../../logic/addressBook/utils' +// import { getOwnerData } from '../ManageOwners/dataFetcher' + +export const REMOVE_MODULE_BTN_TEST_ID = 'remove-module-btn' +export const MODULES_ROW_TEST_ID = 'owners-row' + +const useStyles = makeStyles(styles) + +const useToggle = (initialOn = false) => { + const [on, setOn] = useState(initialOn) + const toggle = () => setOn(!on) + + return { on, toggle } +} + +const Advanced: React.FC = () => { + const classes = useStyles() + const columns = generateColumns() + const autoColumns = columns.filter(({ custom }) => !custom) + + // const { enqueueSnackbar, closeSnackbar } = useSnackbar() + // const dispatch = useDispatch() + + const { on, toggle } = useToggle() + + const nonce = useSelector(safeNonceSelector) + const granted = useSelector(grantedSelector) + const modules = useSelector(safeModulesSelector) + console.log(modules) + const moduleData = getModuleData(Set(modules)) + // const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook, owners) + // const ownerData = getOwnerData(ownersAdbk) + + // const safeAddress = useSelector(safeParamAddressFromStateSelector) + // const owners = useSelector(safeOwnersSelector) + + // const onChangeThreshold = async (newThreshold) => { + // const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + // const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI() + // + // dispatch( + // createTransaction({ + // safeAddress, + // to: safeAddress, + // valueInWei: 0, + // txData, + // notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, + // enqueueSnackbar, + // closeSnackbar, + // } as any), + // ) + // } + + return ( + <> + + Safe Nonce + + For security reasons, transactions made with the Safe need to be executed in order. The nonce shows you which + transaction was executed most recently. You can find the nonce for a transaction in the transaction details. + + + Current Nonce: {nonce} + + + + + Safe Modules + + Modules allow you to customize the access-control logic of your Safe. Modules are potentially risky, so make + sure to only use modules from trusted sources. Learn more about modules{' '} + + here + + . + + {moduleData.size === 0 ? ( + + No modules enabled + + ) : ( + + + {(sortedData) => + sortedData.map((row, index) => ( + = 3 && index === sortedData.size - 1 && classes.noBorderBottom)} + data-testid={MODULES_ROW_TEST_ID} + key={index} + tabIndex={-1} + > + {autoColumns.map((column: any) => ( + + {column.id === MODULES_TABLE_ADDRESS_ID ? ( + + ) : ( + row[column.id] + )} + + ))} + + + {granted && ( + Remove module + )} + + + + )) + } +
+
+ )} +
+ {/**/} + + {/**/} + + + ) +} + +export default Advanced diff --git a/src/routes/safe/components/Settings/Advanced/style.ts b/src/routes/safe/components/Settings/Advanced/style.ts new file mode 100644 index 00000000..887ff65b --- /dev/null +++ b/src/routes/safe/components/Settings/Advanced/style.ts @@ -0,0 +1,57 @@ +import { createStyles } from '@material-ui/core' +import { border, fontColor, lg, secondaryText, smallFontSize, xl } from 'src/theme/variables' + +export const styles = createStyles({ + title: { + padding: lg, + paddingBottom: 0, + }, + hide: { + '&:hover': { + backgroundColor: '#fff3e2', + }, + '&:hover $actions': { + visibility: 'initial', + }, + }, + actions: { + justifyContent: 'flex-end', + visibility: 'hidden', + minWidth: '100px', + }, + noBorderBottom: { + '& > td': { + borderBottom: 'none', + }, + }, + annotation: { + paddingLeft: lg, + }, + ownersText: { + color: secondaryText, + '& b': { + color: fontColor, + }, + }, + container: { + padding: lg, + }, + buttonRow: { + padding: lg, + position: 'absolute', + left: 0, + bottom: 0, + boxSizing: 'border-box', + width: '100%', + justifyContent: 'flex-end', + borderTop: `2px solid ${border}`, + }, + modifyBtn: { + height: xl, + fontSize: smallFontSize, + }, + removeModuleIcon: { + marginLeft: lg, + cursor: 'pointer', + }, +}) diff --git a/src/routes/safe/components/Settings/index.tsx b/src/routes/safe/components/Settings/index.tsx index 69cbfd67..1f7fcd63 100644 --- a/src/routes/safe/components/Settings/index.tsx +++ b/src/routes/safe/components/Settings/index.tsx @@ -1,10 +1,11 @@ import Badge from '@material-ui/core/Badge' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import cn from 'classnames' import * as React from 'react' import { useState } from 'react' import { useSelector } from 'react-redux' +import Advanced from './Advanced' import ManageOwners from './ManageOwners' import { RemoveSafeModal } from './RemoveSafeModal' import SafeDetails from './SafeDetails' @@ -36,7 +37,10 @@ const INITIAL_STATE = { menuOptionIndex: 1, } -const Settings = (props) => { +const useStyles = makeStyles(styles) + +const Settings: React.FC = () => { + const classes = useStyles() const [state, setState] = useState(INITIAL_STATE) const owners = useSelector(safeOwnersSelector) const needsUpdate = useSelector(safeNeedsUpdate) @@ -56,7 +60,6 @@ const Settings = (props) => { } const { menuOptionIndex, showRemoveSafe } = state - const { classes } = props return !owners ? ( @@ -102,6 +105,11 @@ const Settings = (props) => { Policies + + + Advanced + + @@ -109,6 +117,7 @@ const Settings = (props) => { {menuOptionIndex === 1 && } {menuOptionIndex === 2 && } {menuOptionIndex === 3 && } + {menuOptionIndex === 4 && } @@ -116,4 +125,4 @@ const Settings = (props) => { ) } -export default withStyles(styles as any)(Settings) +export default Settings diff --git a/src/routes/safe/components/Settings/style.ts b/src/routes/safe/components/Settings/style.ts index 7d6fb657..14019764 100644 --- a/src/routes/safe/components/Settings/style.ts +++ b/src/routes/safe/components/Settings/style.ts @@ -1,3 +1,5 @@ +import { createStyles } from '@material-ui/core' + import { background, bolderFont, @@ -11,7 +13,7 @@ import { xs, } from 'src/theme/variables' -export const styles = () => ({ +export const styles = createStyles({ root: { backgroundColor: 'white', borderRadius: sm, @@ -31,7 +33,7 @@ export const styles = () => ({ menuWrapper: { display: 'flex', flexDirection: 'row', - flexGrow: '0', + flexGrow: 0, maxWidth: '100%', [`@media (min-width: ${screenSm}px)`]: { @@ -43,7 +45,7 @@ export const styles = () => ({ borderBottom: `solid 2px ${border}`, display: 'flex', flexDirection: 'row', - flexGrow: '1', + flexGrow: 1, height: '100%', width: '100%', @@ -59,8 +61,8 @@ export const styles = () => ({ borderRight: `solid 1px ${border}`, boxSizing: 'border-box', cursor: 'pointer', - flexGrow: '1', - flexShrink: '1', + flexGrow: 1, + flexShrink: 1, fontSize: '13px', justifyContent: 'center', lineHeight: '1.2', @@ -113,7 +115,7 @@ export const styles = () => ({ }, }, container: { - flexGrow: '1', + flexGrow: 1, height: '100%', position: 'relative', },