This repository has been archived by the owner on Nov 10, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 362
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #88 from gnosis/development
Feature #75 - Load existing safe
- Loading branch information
Showing
18 changed files
with
655 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.