diff --git a/src/routes/load/components/DetailsForm/index.jsx b/src/routes/load/components/DetailsForm/index.jsx new file mode 100644 index 00000000..28e0ba04 --- /dev/null +++ b/src/routes/load/components/DetailsForm/index.jsx @@ -0,0 +1,131 @@ +// @flow +import * as React from 'react' +import contract from 'truffle-contract' +import { withStyles } from '@material-ui/core/styles' +import Field from '~/components/forms/Field' +import { composeValidators, required, noErrorsOn, mustBeEthereumAddress } from '~/components/forms/validator' +import TextField from '~/components/forms/TextField' +import InputAdornment from '@material-ui/core/InputAdornment' +import CheckCircle from '@material-ui/icons/CheckCircle' +import Block from '~/components/layout/Block' +import Paragraph from '~/components/layout/Paragraph' +import OpenPaper from '~/components/Stepper/OpenPaper' +import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields' +import { getWeb3 } from '~/logic/wallets/getWeb3' +import { promisify } from '~/utils/promisify' +import SafeProxy from '#/Proxy.json' +import { getGnosisSafeContract } from '~/logic/contracts/safeContracts' + +type Props = { + classes: Object, + errors: Object, +} + +const styles = () => ({ + root: { + display: 'flex', + maxWidth: '460px', + }, + check: { + color: '#03AE60', + height: '20px', + }, +}) + +export const SAFE_INSTANCE_ERROR = 'Address given is not a safe instance' +export const SAFE_MASTERCOPY_ERROR = 'Mastercopy used by this safe is not the same' + +export const safeFieldsValidation = async (values: Object) => { + const errors = {} + + const web3 = getWeb3() + const safeAddress = values[FIELD_LOAD_ADDRESS] + if (!safeAddress || mustBeEthereumAddress(safeAddress) !== undefined) { + return errors + } + + // https://solidity.readthedocs.io/en/latest/metadata.html#usage-for-source-code-verification + const metaData = 'a165' + + const code = await promisify(cb => web3.eth.getCode(safeAddress, cb)) + const codeWithoutMetadata = code.substring(0, code.lastIndexOf(metaData)) + + const proxyCode = SafeProxy.deployedBytecode + const proxyCodeWithoutMetadata = proxyCode.substring(0, proxyCode.lastIndexOf(metaData)) + + const safeInstance = codeWithoutMetadata === proxyCodeWithoutMetadata + if (!safeInstance) { + errors[FIELD_LOAD_ADDRESS] = SAFE_INSTANCE_ERROR + + return errors + } + + // check mastercopy + const proxy = contract(SafeProxy) + proxy.setProvider(web3.currentProvider) + const proxyInstance = proxy.at(safeAddress) + const proxyImplementation = await proxyInstance.implementation() + + const GnosisSafe = getGnosisSafeContract(web3) + const safeMaster = await GnosisSafe.deployed() + const masterCopy = safeMaster.address + + const sameMasterCopy = proxyImplementation === masterCopy + if (!sameMasterCopy) { + errors[FIELD_LOAD_ADDRESS] = SAFE_MASTERCOPY_ERROR + } + + return errors +} + +const Details = ({ classes, errors }: Props) => ( + + + + Adding an existing Safe only requires the Safe address. Optionally you can give it a name. + In case your connected client is not the owner of the Safe, the interface will essentially provide you a + read-only view. + + + + + + + + + + ), + }} + type="text" + validate={composeValidators(required, mustBeEthereumAddress)} + placeholder="Safe Address*" + text="Safe Address" + /> + + +) + +const DetailsForm = withStyles(styles)(Details) + +const DetailsPage = () => (controls: React$Node, { errors }: Object) => ( + + + + + +) + + +export default DetailsPage diff --git a/src/routes/load/components/Layout.jsx b/src/routes/load/components/Layout.jsx new file mode 100644 index 00000000..75c36269 --- /dev/null +++ b/src/routes/load/components/Layout.jsx @@ -0,0 +1,69 @@ +// @flow +import * as React from 'react' +import Stepper from '~/components/Stepper' +import Block from '~/components/layout/Block' +import Heading from '~/components/layout/Heading' +import Row from '~/components/layout/Row' +import IconButton from '@material-ui/core/IconButton' +import ReviewInformation from '~/routes/load/components/ReviewInformation' +import DetailsForm, { safeFieldsValidation } from '~/routes/load/components/DetailsForm' +import ChevronLeft from '@material-ui/icons/ChevronLeft' +import { history } from '~/store' +import { secondary } from '~/theme/variables' + +const getSteps = () => [ + 'Details', 'Review', +] + +type Props = { + provider: string, + network: string, + onLoadSafeSubmit: () => Promise, +} + +const iconStyle = { + color: secondary, + width: '32px', + height: '32px', +} + +const back = () => { + history.goBack() +} + +const Layout = ({ + provider, onLoadSafeSubmit, network, +}: Props) => { + const steps = getSteps() + + return ( + + { provider + ? ( + + + + + + Load existing Safe + + + + { DetailsForm } + + + { ReviewInformation } + + + + ) + :
No metamask detected
+ } +
+ ) +} + +export default Layout diff --git a/src/routes/load/components/ReviewInformation/index.jsx b/src/routes/load/components/ReviewInformation/index.jsx new file mode 100644 index 00000000..935016c1 --- /dev/null +++ b/src/routes/load/components/ReviewInformation/index.jsx @@ -0,0 +1,105 @@ +// @flow +import * as React from 'react' +import Block from '~/components/layout/Block' +import { withStyles } from '@material-ui/core/styles' +import OpenInNew from '@material-ui/icons/OpenInNew' +import Identicon from '~/components/Identicon' +import OpenPaper from '~/components/Stepper/OpenPaper' +import Row from '~/components/layout/Row' +import Paragraph from '~/components/layout/Paragraph' +import { xs, sm, lg, border, secondary } from '~/theme/variables' +import { openAddressInEtherScan } from '~/logic/wallets/getWeb3' +import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields' + +const openIconStyle = { + height: '16px', + color: secondary, +} + +const styles = () => ({ + details: { + padding: lg, + borderRight: `solid 1px ${border}`, + height: '100%', + }, + name: { + letterSpacing: '-0.6px', + }, + container: { + marginTop: xs, + alignItems: 'center', + }, + address: { + paddingLeft: '6px', + }, + open: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, +}) + +type LayoutProps = { + network: string, +} + +type Props = LayoutProps & { + values: Object, + classes: Object, +} + +const ReviewComponent = ({ values, classes, network }: Props) => { + const safeAddress = values[FIELD_LOAD_ADDRESS] + + return ( + + + + + Name of the Safe + + + {values[FIELD_LOAD_NAME]} + + + + + Safe address + + + + {safeAddress} + + + + + + Connected wallet client is owner? + + + No (read-only) + + + + + ) +} + +const ReviewPage = withStyles(styles)(ReviewComponent) + +const Review = ({ network }: LayoutProps) => (controls: React$Node, { values }: Object) => ( + + + + + +) + + +export default Review diff --git a/src/routes/load/components/fields.js b/src/routes/load/components/fields.js new file mode 100644 index 00000000..9e2693a5 --- /dev/null +++ b/src/routes/load/components/fields.js @@ -0,0 +1,4 @@ +// @flow +export const FIELD_LOAD_NAME: string = 'name' +export const FIELD_LOAD_ADDRESS: string = 'address' +