Load route components #75

This commit is contained in:
apanizo 2018-11-12 16:34:38 +01:00
parent 4ee3a05eab
commit fcb996cd2a
4 changed files with 309 additions and 0 deletions

View File

@ -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) => (
<React.Fragment>
<Block margin="sm">
<Paragraph noMargin size="md" color="primary">
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.
</Paragraph>
</Block>
<Block className={classes.root}>
<Field
name={FIELD_LOAD_NAME}
component={TextField}
type="text"
validate={required}
placeholder="Name of the Safe"
text="Safe name"
/>
</Block>
<Block margin="lg" className={classes.root}>
<Field
name={FIELD_LOAD_ADDRESS}
component={TextField}
inputAdornment={noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
endAdornment: (
<InputAdornment position="end">
<CheckCircle className={classes.check} />
</InputAdornment>
),
}}
type="text"
validate={composeValidators(required, mustBeEthereumAddress)}
placeholder="Safe Address*"
text="Safe Address"
/>
</Block>
</React.Fragment>
)
const DetailsForm = withStyles(styles)(Details)
const DetailsPage = () => (controls: React$Node, { errors }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} container={605}>
<DetailsForm errors={errors} />
</OpenPaper>
</React.Fragment>
)
export default DetailsPage

View File

@ -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<void>,
}
const iconStyle = {
color: secondary,
width: '32px',
height: '32px',
}
const back = () => {
history.goBack()
}
const Layout = ({
provider, onLoadSafeSubmit, network,
}: Props) => {
const steps = getSteps()
return (
<React.Fragment>
{ provider
? (
<Block>
<Row align="center">
<IconButton onClick={back} style={iconStyle} disableRipple>
<ChevronLeft />
</IconButton>
<Heading tag="h2">Load existing Safe</Heading>
</Row>
<Stepper
onSubmit={onLoadSafeSubmit}
steps={steps}
>
<Stepper.Page validate={safeFieldsValidation}>
{ DetailsForm }
</Stepper.Page>
<Stepper.Page network={network}>
{ ReviewInformation }
</Stepper.Page>
</Stepper>
</Block>
)
: <div>No metamask detected</div>
}
</React.Fragment>
)
}
export default Layout

View File

@ -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 (
<React.Fragment>
<Block className={classes.details}>
<Block margin="lg">
<Paragraph size="sm" color="disabled" noMargin>
Name of the Safe
</Paragraph>
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
{values[FIELD_LOAD_NAME]}
</Paragraph>
</Block>
<Block margin="lg">
<Paragraph size="sm" color="disabled" noMargin>
Safe address
</Paragraph>
<Row className={classes.container}>
<Identicon address={safeAddress} diameter={32} />
<Paragraph size="md" color="disabled" noMargin className={classes.address}>{safeAddress}</Paragraph>
<OpenInNew
className={classes.open}
style={openIconStyle}
onClick={openAddressInEtherScan(safeAddress, network)}
/>
</Row>
</Block>
<Block margin="lg">
<Paragraph size="sm" color="disabled" noMargin>
Connected wallet client is owner?
</Paragraph>
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
No (read-only)
</Paragraph>
</Block>
</Block>
</React.Fragment>
)
}
const ReviewPage = withStyles(styles)(ReviewComponent)
const Review = ({ network }: LayoutProps) => (controls: React$Node, { values }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} padding={false}>
<ReviewPage network={network} values={values} />
</OpenPaper>
</React.Fragment>
)
export default Review

View File

@ -0,0 +1,4 @@
// @flow
export const FIELD_LOAD_NAME: string = 'name'
export const FIELD_LOAD_ADDRESS: string = 'address'