diff --git a/src/components/forms/validator.js b/src/components/forms/validator.js
index 0d0e595659..38e654626a 100644
--- a/src/components/forms/validator.js
+++ b/src/components/forms/validator.js
@@ -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
diff --git a/src/logic/contracts/safeContracts.js b/src/logic/contracts/safeContracts.js
index c1cd1c9c9a..af120c89f5 100644
--- a/src/logic/contracts/safeContracts.js
+++ b/src/logic/contracts/safeContracts.js
@@ -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,
diff --git a/src/routes/index.js b/src/routes/index.js
index e02b1dbf87..fc2c8be32a 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -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'),
@@ -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}`
@@ -45,6 +50,7 @@ const Routes = () => (
+
)
diff --git a/src/routes/load/components/DetailsForm/index.jsx b/src/routes/load/components/DetailsForm/index.jsx
new file mode 100644
index 0000000000..a8f849d390
--- /dev/null
+++ b/src/routes/load/components/DetailsForm/index.jsx
@@ -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) => (
+
+
+
+ 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 0000000000..95beb9c966
--- /dev/null
+++ b/src/routes/load/components/Layout.jsx
@@ -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,
+}
+
+const iconStyle = {
+ color: secondary,
+ width: '32px',
+ height: '32px',
+}
+
+const back = () => {
+ history.goBack()
+}
+
+const Layout = ({
+ provider, onLoadSafeSubmit, network, userAddress,
+}: 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 0000000000..1790e910a1
--- /dev/null
+++ b/src/routes/load/components/ReviewInformation/index.jsx
@@ -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 {
+ 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 (
+
+
+
+
+ Name of the Safe
+
+
+ {values[FIELD_LOAD_NAME]}
+
+
+
+
+ Safe address
+
+
+
+ {safeAddress}
+
+
+
+
+
+ Connected wallet client is owner?
+
+
+ { isOwner ? 'Yes' : 'No (read-only)' }
+
+
+
+
+ )
+ }
+}
+
+const ReviewPage = withStyles(styles)(ReviewComponent)
+
+const Review = ({ network, userAddress }: 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 0000000000..9e2693a554
--- /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'
+
diff --git a/src/routes/load/container/Load.jsx b/src/routes/load/container/Load.jsx
new file mode 100644
index 0000000000..9fe4192163
--- /dev/null
+++ b/src/routes/load/container/Load.jsx
@@ -0,0 +1,61 @@
+// @flow
+import * as React from 'react'
+import { connect } from 'react-redux'
+import Page from '~/components/layout/Page'
+import { buildSafe } from '~/routes/safe/store/actions/fetchSafe'
+import { SAFES_KEY, load, saveSafes } from '~/utils/localStorage'
+import { SAFELIST_ADDRESS } from '~/routes/routes'
+import { history } from '~/store'
+import selector, { type SelectorProps } from './selector'
+import actions, { type Actions, type UpdateSafe } from './actions'
+import Layout from '../components/Layout'
+import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '../components/fields'
+
+type Props = SelectorProps & Actions
+
+export const loadSafe = async (safeName: string, safeAddress: string, updateSafe: UpdateSafe) => {
+ const safeRecord = await buildSafe(safeAddress, safeName)
+
+ await updateSafe(safeRecord)
+
+ const storedSafes = load(SAFES_KEY) || {}
+ storedSafes[safeAddress] = safeRecord.toJSON()
+
+ saveSafes(storedSafes)
+}
+
+class Load extends React.Component {
+ onLoadSafeSubmit = async (values: Object) => {
+ try {
+ const { updateSafe } = this.props
+ const safeName = values[FIELD_LOAD_NAME]
+ const safeAddress = values[FIELD_LOAD_ADDRESS]
+
+ await loadSafe(safeName, safeAddress, updateSafe)
+ const url = `${SAFELIST_ADDRESS}/${safeAddress}`
+ history.push(url)
+ } catch (error) {
+ // eslint-disable-next-line
+ console.log('Error while loading the Safe' + error)
+ }
+ }
+
+ render() {
+ const {
+ provider, network, userAddress,
+ } = this.props
+
+ return (
+
+
+
+ )
+ }
+}
+
+export default connect(selector, actions)(Load)
diff --git a/src/routes/load/container/actions.js b/src/routes/load/container/actions.js
new file mode 100644
index 0000000000..08a56d7eea
--- /dev/null
+++ b/src/routes/load/container/actions.js
@@ -0,0 +1,12 @@
+// @flow
+import updateSafe from '~/routes/safe/store/actions/updateSafe'
+
+export type UpdateSafe = typeof updateSafe
+
+export type Actions = {
+ updateSafe: typeof updateSafe,
+}
+
+export default {
+ updateSafe,
+}
diff --git a/src/routes/load/container/selector.js b/src/routes/load/container/selector.js
new file mode 100644
index 0000000000..28f673b95f
--- /dev/null
+++ b/src/routes/load/container/selector.js
@@ -0,0 +1,19 @@
+// @flow
+import { createStructuredSelector, type Selector } from 'reselect'
+import { providerNameSelector, networkSelector, userAccountSelector } from '~/logic/wallets/store/selectors'
+import { type GlobalState } from '~/store'
+
+export type SelectorProps = {
+ provider: string,
+ network: string,
+ userAddress: string,
+}
+
+const structuredSelector: Selector = createStructuredSelector({
+ provider: providerNameSelector,
+ network: networkSelector,
+ userAddress: userAccountSelector,
+})
+
+export default structuredSelector
+
diff --git a/src/routes/open/components/SafeOwnersForm/index.jsx b/src/routes/open/components/SafeOwnersForm/index.jsx
index 297327e447..4ef98bab8b 100644
--- a/src/routes/open/components/SafeOwnersForm/index.jsx
+++ b/src/routes/open/components/SafeOwnersForm/index.jsx
@@ -3,7 +3,7 @@ import * as React from 'react'
import { withStyles } from '@material-ui/core/styles'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
-import { required, composeValidators, uniqueAddress, mustBeEthereumAddress } from '~/components/forms/validator'
+import { required, composeValidators, uniqueAddress, mustBeEthereumAddress, noErrorsOn } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Row from '~/components/layout/Row'
@@ -75,8 +75,6 @@ const getAddressValidators = (addresses: string[], position: number) => {
return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy))
}
-const noErrorsOn = (name: string, errors: Object) => errors[name] === undefined
-
export const ADD_OWNER_BUTTON = '+ ADD ANOTHER OWNER'
export const calculateValuesAfterRemoving = (index: number, notRemovedOwners: number, values: Object) => {
diff --git a/src/routes/routes.js b/src/routes/routes.js
index ca32621818..a3f95255d8 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -4,6 +4,7 @@ import { history } from '~/store'
export const SAFE_PARAM_ADDRESS = 'address'
export const SAFELIST_ADDRESS = '/safes'
export const OPEN_ADDRESS = '/open'
+export const LOAD_ADDRESS = '/load'
export const WELCOME_ADDRESS = '/welcome'
export const SETTINS_ADDRESS = '/settings'
export const OPENING_ADDRESS = '/opening'
diff --git a/src/routes/safe/store/reducer/safe.js b/src/routes/safe/store/reducer/safe.js
index 95091ae101..48f7a3bc6a 100644
--- a/src/routes/safe/store/reducer/safe.js
+++ b/src/routes/safe/store/reducer/safe.js
@@ -71,6 +71,7 @@ export default handleActions({
const safes = state.set(action.payload.address, safe)
saveSafes(safes.toJSON())
+
return safes
},
}, Map())
diff --git a/src/routes/welcome/components/Layout.jsx b/src/routes/welcome/components/Layout.jsx
index 10985fb905..dc152c08f4 100644
--- a/src/routes/welcome/components/Layout.jsx
+++ b/src/routes/welcome/components/Layout.jsx
@@ -5,7 +5,7 @@ import Heading from '~/components/layout/Heading'
import Img from '~/components/layout/Img'
import Button from '~/components/layout/Button'
import Link from '~/components/layout/Link'
-import { OPEN_ADDRESS } from '~/routes/routes'
+import { OPEN_ADDRESS, LOAD_ADDRESS } from '~/routes/routes'
import { marginButtonImg } from '~/theme/variables'
import styles from './Layout.scss'
@@ -43,7 +43,7 @@ export const CreateSafe = ({ size, provider }: SafeProps) => (
export const LoadSafe = ({ size, provider }: SafeProps) => (