Merge pull request #87 from gnosis/feature/#75-load-existing-safe
Feature #75 - Load existing safe
This commit is contained in:
commit
3e305d3267
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = () => (
|
|||
<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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
// @flow
|
||||
export const FIELD_LOAD_NAME: string = 'name'
|
||||
export const FIELD_LOAD_ADDRESS: string = 'address'
|
||||
|
|
@ -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<Props> {
|
||||
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 (
|
||||
<Page>
|
||||
<Layout
|
||||
network={network}
|
||||
provider={provider}
|
||||
onLoadSafeSubmit={this.onLoadSafeSubmit}
|
||||
userAddress={userAddress}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(selector, actions)(Load)
|
|
@ -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,
|
||||
}
|
|
@ -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<GlobalState, any, any> = createStructuredSelector({
|
||||
provider: providerNameSelector,
|
||||
network: networkSelector,
|
||||
userAddress: userAccountSelector,
|
||||
})
|
||||
|
||||
export default structuredSelector
|
||||
|
|
@ -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) => {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -71,6 +71,7 @@ export default handleActions({
|
|||
|
||||
const safes = state.set(action.payload.address, safe)
|
||||
saveSafes(safes.toJSON())
|
||||
|
||||
return safes
|
||||
},
|
||||
}, Map())
|
||||
|
|
|
@ -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) => (
|
||||
<Button
|
||||
component={Link}
|
||||
to={OPEN_ADDRESS}
|
||||
to={LOAD_ADDRESS}
|
||||
variant="outlined"
|
||||
size={size || 'medium'}
|
||||
color="primary"
|
||||
|
|
|
@ -115,3 +115,23 @@ export const travelToTokens = (store: Store, address: string): React$Component<{
|
|||
|
||||
return createDom(store)
|
||||
}
|
||||
|
||||
const INTERVAL = 500
|
||||
const MAX_TIMES_EXECUTED = 30
|
||||
export const whenSafeDeployed = (): Promise<string> => new Promise((resolve, reject) => {
|
||||
let times = 0
|
||||
const interval = setInterval(() => {
|
||||
if (times >= MAX_TIMES_EXECUTED) {
|
||||
clearInterval(interval)
|
||||
reject()
|
||||
}
|
||||
|
||||
const url = `${window.location}`
|
||||
const regex = /.*safes\/(0x[a-f0-9A-F]*)/
|
||||
const safeAddress = url.match(regex)
|
||||
if (safeAddress) {
|
||||
resolve(safeAddress[1])
|
||||
}
|
||||
times += 1
|
||||
}, INTERVAL)
|
||||
})
|
||||
|
|
|
@ -14,6 +14,7 @@ import addProvider from '~/logic/wallets/store/actions/addProvider'
|
|||
import { makeProvider } from '~/logic/wallets/store/model/provider'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
import { whenSafeDeployed } from './builder/safe.dom.utils'
|
||||
|
||||
const fillOpenSafeForm = async (localStore: Store<GlobalState>) => {
|
||||
const provider = await getProviderInfo()
|
||||
|
@ -31,26 +32,6 @@ const fillOpenSafeForm = async (localStore: Store<GlobalState>) => {
|
|||
)
|
||||
}
|
||||
|
||||
const INTERVAL = 500
|
||||
const MAX_TIMES_EXECUTED = 30
|
||||
const whenSafeDeployed = () => new Promise((resolve, reject) => {
|
||||
let times = 0
|
||||
const interval = setInterval(() => {
|
||||
if (times >= MAX_TIMES_EXECUTED) {
|
||||
clearInterval(interval)
|
||||
reject()
|
||||
}
|
||||
|
||||
const url = `${window.location}`
|
||||
const regex = /.*safes\/(0x[a-f0-9A-F]*)/
|
||||
const safeAddress = url.match(regex)
|
||||
if (safeAddress) {
|
||||
resolve(safeAddress[1])
|
||||
}
|
||||
times += 1
|
||||
}, INTERVAL)
|
||||
})
|
||||
|
||||
const deploySafe = async (safe: React$Component<{}>, threshold: number, numOwners: number) => {
|
||||
const web3 = getWeb3()
|
||||
const accounts = await promisify(cb => web3.eth.getAccounts(cb))
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { type Store } from 'redux'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ConnectedRouter } from 'react-router-redux'
|
||||
import Load from '~/routes/load/container/Load'
|
||||
import { aNewStore, history, type GlobalState } from '~/store'
|
||||
import { sleep } from '~/utils/timer'
|
||||
import { getProviderInfo } from '~/logic/wallets/getWeb3'
|
||||
import addProvider from '~/logic/wallets/store/actions/addProvider'
|
||||
import { makeProvider } from '~/logic/wallets/store/model/provider'
|
||||
import { aMinedSafe } from './builder/safe.redux.builder'
|
||||
import { whenSafeDeployed } from './builder/safe.dom.utils'
|
||||
|
||||
const travelToLoadRoute = async (localStore: Store<GlobalState>) => {
|
||||
const provider = await getProviderInfo()
|
||||
const walletRecord = makeProvider(provider)
|
||||
localStore.dispatch(addProvider(walletRecord))
|
||||
|
||||
return (
|
||||
TestUtils.renderIntoDocument((
|
||||
<Provider store={localStore}>
|
||||
<ConnectedRouter history={history}>
|
||||
<Load />
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
describe('DOM > Feature > LOAD a safe', () => {
|
||||
it('load correctly a created safe', async () => {
|
||||
const store = aNewStore()
|
||||
const address = await aMinedSafe(store)
|
||||
const LoadDom = await travelToLoadRoute(store)
|
||||
|
||||
const form = TestUtils.findRenderedDOMComponentWithTag(LoadDom, 'form')
|
||||
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(LoadDom, 'input')
|
||||
|
||||
// Fill Safe's name
|
||||
const fieldName = inputs[0]
|
||||
TestUtils.Simulate.change(fieldName, { target: { value: 'Adolfo Safe' } })
|
||||
const fieldAddress = inputs[1]
|
||||
TestUtils.Simulate.change(fieldAddress, { target: { value: address } })
|
||||
await sleep(400)
|
||||
|
||||
// Click next
|
||||
TestUtils.Simulate.submit(form)
|
||||
await sleep(400)
|
||||
|
||||
// Submit
|
||||
TestUtils.Simulate.submit(form)
|
||||
await sleep(400)
|
||||
|
||||
|
||||
const deployedAddress = await whenSafeDeployed()
|
||||
expect(deployedAddress).toBe(address)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,113 @@
|
|||
// @flow
|
||||
import { Map, List } from 'immutable'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { aNewStore } from '~/store'
|
||||
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
||||
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||
import { loadSafe } from '~/routes/load/container/Load'
|
||||
import { safesMapSelector } from '~/routes/safeList/store/selectors'
|
||||
import { makeOwner, type Owner } from '~/routes/safe/store/model/owner'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import { safesInitialState } from '~/routes/safe/store/reducer/safe'
|
||||
import { setOwners, OWNERS_KEY } from '~/utils/localStorage'
|
||||
|
||||
describe('Safe - redux load safe', () => {
|
||||
let store
|
||||
let address: string
|
||||
let accounts
|
||||
beforeEach(async () => {
|
||||
store = aNewStore()
|
||||
address = await aMinedSafe(store)
|
||||
localStorage.clear()
|
||||
accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||
})
|
||||
|
||||
it('if safe is not present, store and persist it with default names', async () => {
|
||||
const safeName = 'Loaded Safe'
|
||||
const safeAddress = address
|
||||
const updateSafeFn: any = (...args) => store.dispatch(updateSafe(...args))
|
||||
|
||||
await loadSafe(safeName, safeAddress, updateSafeFn)
|
||||
|
||||
const safes: Map<string, Safe> = safesMapSelector(store.getState())
|
||||
expect(safes.size).toBe(1)
|
||||
if (!safes) throw new Error()
|
||||
const safe = safes.get(safeAddress)
|
||||
if (!safe) throw new Error()
|
||||
|
||||
expect(safe.get('name')).toBe(safeName)
|
||||
expect(safe.get('threshold')).toBe(1)
|
||||
expect(safe.get('address')).toBe(safeAddress)
|
||||
expect(safe.get('owners')).toEqual(List([makeOwner({ name: 'UNKNOWN', address: accounts[0] })]))
|
||||
|
||||
expect(safesInitialState()).toEqual(safes)
|
||||
})
|
||||
|
||||
it('if safe is not present but owners, store and persist it with stored names', async () => {
|
||||
const safeName = 'Loaded Safe'
|
||||
const safeAddress = address
|
||||
const ownerName = 'Foo Bar Restores'
|
||||
const updateSafeFn: any = (...args) => store.dispatch(updateSafe(...args))
|
||||
const owner: Owner = makeOwner({ name: ownerName, address: accounts[0] })
|
||||
setOwners(safeAddress, List([owner]))
|
||||
|
||||
await loadSafe(safeName, safeAddress, updateSafeFn)
|
||||
|
||||
const safes: Map<string, Safe> = safesMapSelector(store.getState())
|
||||
expect(safes.size).toBe(1)
|
||||
if (!safes) throw new Error()
|
||||
const safe = safes.get(safeAddress)
|
||||
if (!safe) throw new Error()
|
||||
|
||||
expect(safe.get('name')).toBe(safeName)
|
||||
expect(safe.get('threshold')).toBe(1)
|
||||
expect(safe.get('address')).toBe(safeAddress)
|
||||
expect(safe.get('owners')).toEqual(List([makeOwner({ name: ownerName, address: accounts[0] })]))
|
||||
|
||||
expect(safesInitialState()).toEqual(safes)
|
||||
})
|
||||
|
||||
it('if safe is present but no owners, store and persist it with default names', async () => {
|
||||
const safeAddress = await aMinedSafe(store)
|
||||
localStorage.removeItem(`${OWNERS_KEY}-${safeAddress}`)
|
||||
|
||||
const safeName = 'Loaded Safe'
|
||||
const updateSafeFn: any = (...args) => store.dispatch(updateSafe(...args))
|
||||
await loadSafe(safeName, safeAddress, updateSafeFn)
|
||||
|
||||
const safes: Map<string, Safe> = safesMapSelector(store.getState())
|
||||
expect(safes.size).toBe(2)
|
||||
if (!safes) throw new Error()
|
||||
const safe = safes.get(safeAddress)
|
||||
if (!safe) throw new Error()
|
||||
|
||||
expect(safe.get('name')).toBe(safeName)
|
||||
expect(safe.get('threshold')).toBe(1)
|
||||
expect(safe.get('address')).toBe(safeAddress)
|
||||
expect(safe.get('owners')).toEqual(List([makeOwner({ name: 'UNKNOWN', address: accounts[0] })]))
|
||||
|
||||
expect(safesInitialState()).toEqual(safes)
|
||||
})
|
||||
|
||||
it('if safe is present but owners, store and persist it with stored names', async () => {
|
||||
const safeAddress = await aMinedSafe(store)
|
||||
|
||||
const safeName = 'Loaded Safe'
|
||||
const updateSafeFn: any = (...args) => store.dispatch(updateSafe(...args))
|
||||
await loadSafe(safeName, safeAddress, updateSafeFn)
|
||||
|
||||
const safes: Map<string, Safe> = safesMapSelector(store.getState())
|
||||
expect(safes.size).toBe(2)
|
||||
if (!safes) throw new Error()
|
||||
const safe = safes.get(safeAddress)
|
||||
if (!safe) throw new Error()
|
||||
|
||||
expect(safe.get('name')).toBe(safeName)
|
||||
expect(safe.get('threshold')).toBe(1)
|
||||
expect(safe.get('address')).toBe(safeAddress)
|
||||
expect(safe.get('owners')).toEqual(List([makeOwner({ name: 'Adol 1 Eth Account', address: accounts[0] })]))
|
||||
|
||||
expect(safesInitialState()).toEqual(safes)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue