Merge pull request #88 from gnosis/development

Feature #75 - Load existing safe
This commit is contained in:
Adolfo Panizo 2018-11-14 17:37:25 +01:00 committed by GitHub
commit 5360e75669
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 655 additions and 26 deletions

View File

@ -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

View File

@ -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,

View File

@ -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>
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
// @flow
export const FIELD_LOAD_NAME: string = 'name'
export const FIELD_LOAD_ADDRESS: string = 'address'

View File

@ -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)

View File

@ -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,
}

View File

@ -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

View File

@ -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) => {

View File

@ -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'

View File

@ -71,6 +71,7 @@ export default handleActions({
const safes = state.set(action.payload.address, safe)
saveSafes(safes.toJSON())
return safes
},
}, Map())

View File

@ -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"

View File

@ -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)
})

View File

@ -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))

View File

@ -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)
})
})

View File

@ -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)
})
})