Load route components #75
This commit is contained in:
parent
4ee3a05eab
commit
fcb996cd2a
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
// @flow
|
||||
export const FIELD_LOAD_NAME: string = 'name'
|
||||
export const FIELD_LOAD_ADDRESS: string = 'address'
|
||||
|
Loading…
Reference in New Issue