Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
Merge pull request #88 from gnosis/development
Browse files Browse the repository at this point in the history
Feature #75 - Load existing safe
  • Loading branch information
apanizo authored Nov 14, 2018
2 parents aa219be + 3e305d3 commit 5360e75
Show file tree
Hide file tree
Showing 18 changed files with 655 additions and 26 deletions.
2 changes: 2 additions & 0 deletions src/components/forms/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ export const inLimit = (limit: number, base: number, baseText: string, symbol: s

return `Should not exceed ${max} ${symbol} (amount to reach ${baseText})`
}

export const noErrorsOn = (name: string, errors: Object) => errors[name] === undefined
6 changes: 6 additions & 0 deletions src/logic/contracts/safeContracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ const createMasterCopies = async () => {

export const initContracts = ensureOnce(process.env.NODE_ENV === 'test' ? createMasterCopies : instanciateMasterCopies)

export const getSafeMasterContract = async () => {
await initContracts()

return safeMaster
}

export const deploySafeContract = async (
safeAccounts: string[],
numConfirmations: number,
Expand Down
8 changes: 7 additions & 1 deletion src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Loadable from 'react-loadable'
import { Switch, Redirect, Route } from 'react-router-dom'
import Loader from '~/components/Loader'
import Welcome from './welcome/container'
import { SAFELIST_ADDRESS, OPEN_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS, SETTINS_ADDRESS, OPENING_ADDRESS } from './routes'
import { SAFELIST_ADDRESS, OPEN_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS, SETTINS_ADDRESS, OPENING_ADDRESS, LOAD_ADDRESS } from './routes'

const Safe = Loadable({
loader: () => import('./safe/container'),
Expand All @@ -31,6 +31,11 @@ const Opening = Loadable({
loading: Loader,
})

const Load = Loadable({
loader: () => import('./load/container/Load'),
loading: Loader,
})

const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
const SAFE_SETTINGS = `${SAFE_ADDRESS}${SETTINS_ADDRESS}`

Expand All @@ -45,6 +50,7 @@ const Routes = () => (
<Route exact path={SAFE_ADDRESS} component={Safe} />
<Route exact path={SAFE_SETTINGS} component={Settings} />
<Route exact path={OPENING_ADDRESS} component={Opening} />
<Route exact path={LOAD_ADDRESS} component={Load} />
</Switch>
)

Expand Down
130 changes: 130 additions & 0 deletions src/routes/load/components/DetailsForm/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// @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 { getSafeMasterContract } 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 safeMaster = await getSafeMasterContract()
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
68 changes: 68 additions & 0 deletions src/routes/load/components/Layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// @flow
import * as React from 'react'
import ChevronLeft from '@material-ui/icons/ChevronLeft'
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 { history } from '~/store'
import { secondary } from '~/theme/variables'
import { type SelectorProps } from '~/routes/load/container/selector'

const getSteps = () => [
'Details', 'Review',
]

type Props = SelectorProps & {
onLoadSafeSubmit: (values: Object) => Promise<void>,
}

const iconStyle = {
color: secondary,
width: '32px',
height: '32px',
}

const back = () => {
history.goBack()
}

const Layout = ({
provider, onLoadSafeSubmit, network, userAddress,
}: 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} userAddress={userAddress}>
{ ReviewInformation }
</Stepper.Page>
</Stepper>
</Block>
)
: <div>No metamask detected</div>
}
</React.Fragment>
)
}

export default Layout
147 changes: 147 additions & 0 deletions src/routes/load/components/ReviewInformation/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// @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, getWeb3 } from '~/logic/wallets/getWeb3'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields'
import { sameAddress } from '~/logic/wallets/ethAddresses'
import { getGnosisSafeContract } from '~/logic/contracts/safeContracts'

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,
userAddress: string,
}

type Props = LayoutProps & {
values: Object,
classes: Object,
}

type State = {
isOwner: boolean,
}

class ReviewComponent extends React.PureComponent<Props, State> {
state = {
isOwner: false,
}

componentDidMount = async () => {
this.mounted = true

const { values, userAddress } = this.props
const safeAddress = values[FIELD_LOAD_ADDRESS]
const web3 = getWeb3()

const GnosisSafe = getGnosisSafeContract(web3)
const gnosisSafe = GnosisSafe.at(safeAddress)
const owners = await gnosisSafe.getOwners()
if (!owners) {
return
}

const isOwner = owners.find((owner: string) => sameAddress(owner, userAddress)) !== undefined
if (this.mounted) {
this.setState(() => ({ isOwner }))
}
}

componentWillUnmount() {
this.mounted = false
}

mounted = false

render() {
const { values, classes, network } = this.props
const { isOwner } = this.state

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}>
{ isOwner ? 'Yes' : 'No (read-only)' }
</Paragraph>
</Block>
</Block>
</React.Fragment>
)
}
}

const ReviewPage = withStyles(styles)(ReviewComponent)

const Review = ({ network, userAddress }: LayoutProps) => (controls: React$Node, { values }: Object) => (
<React.Fragment>
<OpenPaper controls={controls} padding={false}>
<ReviewPage network={network} values={values} userAddress={userAddress} />
</OpenPaper>
</React.Fragment>
)


export default Review
Loading

0 comments on commit 5360e75

Please sign in to comment.