WA-280 - Feature open created safes (#12)
* WA-280 Added redux logic for safe route * WA-280 Added tests including builders for safe's redux store classes * WA-280 Improving Flow coverage in actions and reducers * WA- 280 Mocking LocalStorage and Web3 in JEST * WA-280 Generating view of Safe route and its logic to store and retrieve info from localstorage * WA-280 Added run-with-testrpc for simulating a testnet in memory while executing tests
This commit is contained in:
parent
d2b5131c9e
commit
88bfca0a0d
|
@ -4,7 +4,7 @@ node_js:
|
|||
os:
|
||||
- linux
|
||||
before_script:
|
||||
- yarn global add truffle
|
||||
- yarn global add truffle@4.1.3
|
||||
- yarn global add surge
|
||||
- git clone https://github.com/gnosis/gnosis-safe-contracts.git
|
||||
- cd gnosis-safe-contracts
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
class LocalStorageMock {
|
||||
store: Object
|
||||
|
||||
constructor() {
|
||||
this.store = {}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.store = {}
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.store[key] || null
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.store[key] = value.toString()
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
delete this.store[key]
|
||||
}
|
||||
}
|
||||
|
||||
global.localStorage = new LocalStorageMock()
|
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
import Web3 from 'web3'
|
||||
|
||||
const window = global.window || {}
|
||||
window.web3 = window.web3 || {}
|
||||
window.web3.currentProvider = new Web3.providers.HttpProvider('http://localhost:8545')
|
||||
|
||||
global.window = window
|
|
@ -2991,18 +2991,6 @@
|
|||
"integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==",
|
||||
"dev": true
|
||||
},
|
||||
"babel-eslint": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz",
|
||||
"integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-code-frame": "6.26.0",
|
||||
"babel-traverse": "6.26.0",
|
||||
"babel-types": "6.26.0",
|
||||
"babylon": "6.18.0"
|
||||
}
|
||||
},
|
||||
"babel-generator": {
|
||||
"version": "6.26.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
|
||||
|
@ -9745,6 +9733,33 @@
|
|||
"integrity": "sha1-8ESOgGmFW/Kj5oPNwdMg5+KgfvQ=",
|
||||
"dev": true
|
||||
},
|
||||
"ganache-cli": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ganache-cli/-/ganache-cli-6.1.0.tgz",
|
||||
"integrity": "sha512-FdTeyk4uLRHGeFiMe+Qnh4Hc5KiTVqvRVVvLDFJEVVKC1P1yHhEgZeh9sp1KhuvxSrxToxgJS25UapYQwH4zHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-map-support": "0.5.4",
|
||||
"webpack-cli": "2.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.4.tgz",
|
||||
"integrity": "sha512-PETSPG6BjY1AHs2t64vS2aqAgu6dMIMXJULWFBGbh2Gr8nVLbCFDo6i/RMMvviIQ2h1Z8+5gQhVKSn2je9nmdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-map": "0.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
|
@ -18547,6 +18562,16 @@
|
|||
"aproba": "1.2.0"
|
||||
}
|
||||
},
|
||||
"run-with-testrpc": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/run-with-testrpc/-/run-with-testrpc-0.3.0.tgz",
|
||||
"integrity": "sha512-G4mvZz0O9AaYyESVEWzaYTagooKtWajd6t4zC7qxPs06pIyhYYzNytmxapCH/5tfWgsRSNfZ+XJhYPBvYpIlaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"colors": "1.1.2",
|
||||
"ganache-cli": "6.1.0"
|
||||
}
|
||||
},
|
||||
"rustbn.js": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rustbn.js/-/rustbn.js-0.1.2.tgz",
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
"scripts": {
|
||||
"start": "node scripts/start.js",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "node scripts/test.js --env=jsdom",
|
||||
"test": "run-with-testrpc -l 40000000 'node scripts/test.js --env=jsdom'",
|
||||
"test-local": "node scripts/test.js --env=jsdom",
|
||||
"precommit": "./precommit.sh",
|
||||
"flow": "flow",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
|
@ -82,6 +83,7 @@
|
|||
"redux-actions": "^2.3.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^3.0.1",
|
||||
"run-with-testrpc": "^0.3.0",
|
||||
"storybook-host": "^4.1.5",
|
||||
"storybook-router": "^0.3.3",
|
||||
"style-loader": "^0.20.2",
|
||||
|
@ -105,12 +107,15 @@
|
|||
"react-router-dom": "^4.2.2"
|
||||
},
|
||||
"jest": {
|
||||
"verbose": true,
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx}"
|
||||
],
|
||||
"setupFiles": [
|
||||
"<rootDir>/config/webpack.config.test.js",
|
||||
"<rootDir>/config/polyfills.js"
|
||||
"<rootDir>/config/polyfills.js",
|
||||
"<rootDir>/config/jest/LocalStorageMock.js",
|
||||
"<rootDir>/config/jest/Web3Mock.js"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/src/**/__tests__/**/*.js?(x)",
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
import React from 'react'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Link from '~/components/layout/Link'
|
||||
import { WELCOME_ADDRESS, SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
import styles from './index.scss'
|
||||
|
||||
const Footer = () => (
|
||||
<Block className={styles.footer}>
|
||||
<Link to="/welcome">
|
||||
<Link padding="md" to={WELCOME_ADDRESS}>
|
||||
Welcome
|
||||
</Link>
|
||||
<Link to={SAFELIST_ADDRESS}>
|
||||
Safe List
|
||||
</Link>
|
||||
</Block>
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
.footer {
|
||||
display: grid;
|
||||
grid-template-rows: 100%;
|
||||
grid-template-columns: auto;
|
||||
justify-content: end;
|
||||
grid-column-gap: $md;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $(screenXs)px) {
|
||||
.footer {
|
||||
grid-template-columns: none;
|
||||
grid-template-rows: auto auto;
|
||||
grid-row-gap: $sm;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.footer > a {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
import React from 'react'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Loader from '~/components/Loader'
|
||||
import Refresh from '~/components/Refresh'
|
||||
import Row from '~/components/layout/Row'
|
||||
|
||||
import Connected from './Connected'
|
||||
|
@ -22,7 +22,7 @@ const Header = ({ provider, reloadWallet }: Props) => (
|
|||
</Col>
|
||||
<Col xs={12} center="xs" sm={6} end="sm" margin="lg">
|
||||
{ provider ? <Connected provider={provider} /> : <NotConnected /> }
|
||||
<Loader callback={reloadWallet} />
|
||||
<Refresh callback={reloadWallet} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
|
|
|
@ -1,47 +1,7 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { CircularProgress } from 'material-ui/Progress'
|
||||
import RefreshIcon from 'material-ui-icons/Refresh'
|
||||
import { sm, secondary } from '~/theme/variables'
|
||||
|
||||
type Props = {
|
||||
callback: () => void,
|
||||
}
|
||||
|
||||
type State = {
|
||||
loading: boolean,
|
||||
}
|
||||
|
||||
const loaderStyle = {
|
||||
margin: `0px 0px 0px ${sm}`,
|
||||
color: secondary,
|
||||
}
|
||||
|
||||
class Loader extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
onReload = async () => {
|
||||
this.setState({ loading: true }, await this.props.callback())
|
||||
this.setState({ loading: false })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading } = this.state
|
||||
|
||||
return (
|
||||
<div style={loaderStyle}>
|
||||
{loading
|
||||
? <CircularProgress color="inherit" size={24} />
|
||||
: <RefreshIcon color="inherit" onClick={this.onReload} /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
const Loader = () => <CircularProgress size={50} />
|
||||
|
||||
export default Loader
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Link from '~/components/layout/Link'
|
||||
import Paragraph from '~/components/layout/Paragraph/index'
|
||||
import { OPEN_ADDRESS } from '~/routes/routes'
|
||||
|
||||
type Props = {
|
||||
text: string
|
||||
}
|
||||
|
||||
const NoSafe = ({ text }: Props) => (
|
||||
<Row>
|
||||
<Col xs={12} center="xs" sm={10} smOffset={2} start="sm" margin="md">
|
||||
<Paragraph size="lg">
|
||||
<Bold>{text}</Bold>
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col xs={12} center="xs" sm={10} smOffset={2} start="sm" margin="md">
|
||||
<Link to={OPEN_ADDRESS}>
|
||||
<Button variant="raised" size="small" color="primary">CREATE A NEW SAFE</Button>
|
||||
</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
|
||||
export default NoSafe
|
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { CircularProgress } from 'material-ui/Progress'
|
||||
import RefreshIcon from 'material-ui-icons/Refresh'
|
||||
import { sm, secondary } from '~/theme/variables'
|
||||
|
||||
type Props = {
|
||||
callback: () => void,
|
||||
}
|
||||
|
||||
type State = {
|
||||
loading: boolean,
|
||||
}
|
||||
|
||||
const loaderStyle = {
|
||||
margin: `0px 0px 0px ${sm}`,
|
||||
color: secondary,
|
||||
}
|
||||
|
||||
class Loader extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
onReload = async () => {
|
||||
this.setState({ loading: true }, await this.props.callback())
|
||||
this.setState({ loading: false })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading } = this.state
|
||||
|
||||
return (
|
||||
<div style={loaderStyle}>
|
||||
{loading
|
||||
? <CircularProgress color="inherit" size={24} />
|
||||
: <RefreshIcon color="inherit" onClick={this.onReload} /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Loader
|
|
@ -2,6 +2,7 @@
|
|||
import * as React from 'react'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Link from '~/components/layout/Link'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
|
||||
type NextButtonProps = {
|
||||
text: string,
|
||||
|
@ -20,8 +21,8 @@ const NextButton = ({ text, disabled }: NextButtonProps) => (
|
|||
)
|
||||
|
||||
const GoButton = () => (
|
||||
<Link to="/welcome">
|
||||
<NextButton text="GO" disabled={false} />
|
||||
<Link to={SAFELIST_ADDRESS}>
|
||||
<NextButton text="VISIT SAFES" disabled={false} />
|
||||
</Link>
|
||||
)
|
||||
|
||||
|
|
|
@ -1,17 +1,33 @@
|
|||
// @flow
|
||||
import classNames from 'classnames/bind'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { capitalize } from '~/utils/css'
|
||||
import styles from './index.scss'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
type Props = {
|
||||
padding?: 'xs' | 'sm' | 'md',
|
||||
to: string,
|
||||
children: React$Node,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
const GnosisLink = ({ to, children, ...props }: Props) => (
|
||||
<Link className={styles.link} to={to} {...props}>
|
||||
{ children }
|
||||
</Link>
|
||||
)
|
||||
const GnosisLink = ({
|
||||
to, children, className, padding, ...props
|
||||
}: Props) => {
|
||||
const classes = cx(
|
||||
styles.link,
|
||||
padding ? capitalize(padding, 'padding') : undefined,
|
||||
className,
|
||||
)
|
||||
|
||||
return (
|
||||
<Link className={classes} to={to} {...props}>
|
||||
{ children }
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default GnosisLink
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
.link {
|
||||
text-decoration: none;
|
||||
color: $secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.paddingXs {
|
||||
padding-right: $xs;
|
||||
}
|
||||
|
||||
.paddingSm {
|
||||
padding-right: $sm;
|
||||
}
|
||||
|
||||
.paddingMd {
|
||||
padding-right: $md;
|
||||
}
|
||||
|
|
|
@ -8,10 +8,11 @@ const cx = classNames.bind(styles)
|
|||
type Props = {
|
||||
children: React$Node,
|
||||
align?: 'center',
|
||||
overflow?: boolean
|
||||
}
|
||||
|
||||
const Page = ({ children, align }: Props) => (
|
||||
<main className={cx(styles.page, align)}>
|
||||
const Page = ({ children, align, overflow }: Props) => (
|
||||
<main className={cx(styles.page, align, { overflow })}>
|
||||
{children}
|
||||
</main>
|
||||
)
|
||||
|
|
|
@ -7,3 +7,7 @@
|
|||
.center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.overflow {
|
||||
overflow-x: scroll;
|
||||
}
|
|
@ -1,21 +1,17 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import Page from '~/components/layout/Page'
|
||||
import Footer from '~/components/Footer'
|
||||
import Header from '~/components/Header'
|
||||
import styles from './index.scss'
|
||||
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
align?: "center",
|
||||
}
|
||||
|
||||
const PageFrame = ({ children, align }: Props) => (
|
||||
const PageFrame = ({ children }: Props) => (
|
||||
<div className={styles.frame}>
|
||||
<Header />
|
||||
<Page align={align}>
|
||||
{children}
|
||||
</Page>
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ type Props = {
|
|||
center?: boolean,
|
||||
noMargin?: boolean,
|
||||
bold?: boolean,
|
||||
size?: 'sm' | 'md' | 'lg',
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl',
|
||||
color?: 'soft' | 'medium' | 'dark' | 'primary',
|
||||
children: React$Node
|
||||
}
|
||||
|
|
|
@ -39,6 +39,10 @@
|
|||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.lg {
|
||||
font-size: $extraLargeFontSize;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Table, { TableBody, TableCell, TableHead, TableRow } from 'material-ui/Table'
|
||||
|
||||
export { TableBody, TableCell, TableHead, TableRow }
|
||||
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
size?: number
|
||||
}
|
||||
|
||||
const buildWidthFrom = (size: number) => ({
|
||||
minWidth: `${size}px`,
|
||||
})
|
||||
|
||||
// see: https://css-tricks.com/responsive-data-tables/
|
||||
const GnoTable = ({ size, children }: Props) => {
|
||||
const style = size ? buildWidthFrom(size) : undefined
|
||||
|
||||
return (
|
||||
<Table style={style}>
|
||||
{children}
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
export default GnoTable
|
||||
|
|
@ -6,6 +6,7 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ConnectedRouter } from 'react-router-redux'
|
||||
import PageFrame from '~/components/layout/PageFrame'
|
||||
import { history, store } from '~/store'
|
||||
import theme from '~/theme/mui'
|
||||
import AppRoutes from '~/routes'
|
||||
|
@ -15,7 +16,9 @@ const Root = () => (
|
|||
<Provider store={store}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<ConnectedRouter history={history}>
|
||||
<AppRoutes />
|
||||
<PageFrame>
|
||||
<AppRoutes />
|
||||
</PageFrame>
|
||||
</ConnectedRouter>
|
||||
</MuiThemeProvider>
|
||||
</Provider>
|
||||
|
|
|
@ -1,28 +1,35 @@
|
|||
// @flow
|
||||
import { CircularProgress } from 'material-ui/Progress'
|
||||
import React from 'react'
|
||||
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 } from './routes'
|
||||
|
||||
const Loading = () => <CircularProgress size={50} />
|
||||
const Safe = Loadable({
|
||||
loader: () => import('./safe/container'),
|
||||
loading: Loader,
|
||||
})
|
||||
|
||||
const Transactions = Loadable({
|
||||
loader: () => import('./transactions/components/Layout'),
|
||||
loading: Loading,
|
||||
const SafeList = Loadable({
|
||||
loader: () => import('./safeList/container'),
|
||||
loading: Loader,
|
||||
})
|
||||
|
||||
const Open = Loadable({
|
||||
loader: () => import('./open/container/Open'),
|
||||
loading: Loading,
|
||||
loading: Loader,
|
||||
})
|
||||
|
||||
const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
|
||||
|
||||
const Routes = () => (
|
||||
<Switch>
|
||||
<Redirect exact from="/" to="/welcome" />
|
||||
<Route exact path="/welcome" component={Welcome} />
|
||||
<Route exact path="/open" component={Open} />
|
||||
<Route exact path="/transactions" component={Transactions} />
|
||||
<Redirect exact from="/" to={WELCOME_ADDRESS} />
|
||||
<Route exact path={WELCOME_ADDRESS} component={Welcome} />
|
||||
<Route exact path={OPEN_ADDRESS} component={Open} />
|
||||
<Route exact path={SAFELIST_ADDRESS} component={SafeList} />
|
||||
<Route exact path={SAFE_ADDRESS} component={Safe} />
|
||||
</Switch>
|
||||
)
|
||||
|
||||
|
|
|
@ -17,8 +17,10 @@ type Props = {
|
|||
tx: Object,
|
||||
}
|
||||
|
||||
export const DEPLOYED_COMPONENT_ID = 'deployedSafeComponent'
|
||||
|
||||
const Deployment = ({ address, tx }: Props) => (
|
||||
<Block>
|
||||
<Block className={DEPLOYED_COMPONENT_ID}>
|
||||
<Paragraph><Bold>Deployed safe to: </Bold>{address}</Paragraph>
|
||||
<Pre>
|
||||
{JSON.stringify(tx, null, 2) }
|
||||
|
|
|
@ -20,7 +20,7 @@ const store = new Store({
|
|||
})
|
||||
|
||||
|
||||
storiesOf('Routes', module)
|
||||
storiesOf('Routes /open', module)
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Open safe with all props set', () => {
|
||||
getProviderInfo()
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import Open from '~/routes/open/container/Open'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ConnectedRouter } from 'react-router-redux'
|
||||
import { FIELD_NAME, FIELD_OWNERS, FIELD_CONFIRMATIONS, getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
|
||||
import { DEPLOYED_COMPONENT_ID } from '~/routes/open/components/FormConfirmation'
|
||||
import { history, store } from '~/store'
|
||||
import { sleep } from '~/utils/timer'
|
||||
import { getProviderInfo } from '~/wallets/getWeb3'
|
||||
import addProvider from '~/wallets/store/actions/addProvider'
|
||||
import { makeProvider } from '~/wallets/store/model/provider'
|
||||
|
||||
describe('React DOM TESTS > Create Safe form', () => {
|
||||
let open
|
||||
let provider
|
||||
beforeEach(async () => {
|
||||
// init app web3 instance
|
||||
provider = await getProviderInfo()
|
||||
const walletRecord = makeProvider(provider)
|
||||
store.dispatch(addProvider(walletRecord))
|
||||
|
||||
open = TestUtils.renderIntoDocument((
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<Open />
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
))
|
||||
})
|
||||
|
||||
it('should create a 1 owner safe after rendering correctly the form', async () => {
|
||||
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(open, 'input')
|
||||
const fieldName = inputs[0]
|
||||
expect(fieldName.name).toEqual(FIELD_NAME)
|
||||
|
||||
const fieldOwners = inputs[1]
|
||||
expect(fieldOwners.name).toEqual(FIELD_OWNERS)
|
||||
|
||||
const fieldConfirmations = inputs[2]
|
||||
expect(fieldConfirmations.name).toEqual(FIELD_CONFIRMATIONS)
|
||||
|
||||
TestUtils.Simulate.change(fieldOwners, { target: { value: '1' } })
|
||||
const inputsExpanded = TestUtils.scryRenderedDOMComponentsWithTag(open, 'input')
|
||||
|
||||
const ownerName = inputsExpanded[2]
|
||||
expect(ownerName.name).toEqual(getOwnerNameBy(0))
|
||||
const ownerAddress = inputsExpanded[3]
|
||||
expect(ownerAddress.name).toEqual(getOwnerAddressBy(0))
|
||||
expect(ownerAddress.value).toEqual(provider.account)
|
||||
|
||||
// WHEN
|
||||
TestUtils.Simulate.change(fieldName, { target: { value: 'Adolfo Safe' } })
|
||||
TestUtils.Simulate.change(fieldConfirmations, { target: { value: '1' } })
|
||||
TestUtils.Simulate.change(ownerName, { target: { value: 'Adolfo Eth Account' } })
|
||||
|
||||
const form = TestUtils.findRenderedDOMComponentWithTag(open, 'form')
|
||||
// One submit per step when creating a safe
|
||||
TestUtils.Simulate.submit(form) // fill the form
|
||||
TestUtils.Simulate.submit(form) // confirming data
|
||||
TestUtils.Simulate.submit(form) // Executing transaction
|
||||
|
||||
// giving some time to the component for updating its state with safe
|
||||
// before destroying its context
|
||||
await sleep(1500)
|
||||
|
||||
// THEN
|
||||
const Deployed = TestUtils.findRenderedDOMComponentWithClass(open, DEPLOYED_COMPONENT_ID)
|
||||
|
||||
const addressHtml = Deployed.getElementsByTagName('p')[0].innerHTML
|
||||
const contractAddress = addressHtml.slice(addressHtml.lastIndexOf('>') + 1)
|
||||
const transactionHash = JSON.parse(Deployed.getElementsByTagName('pre')[0].innerHTML)
|
||||
delete transactionHash.logsBloom
|
||||
// eslint-disable-next-line
|
||||
console.log('Deployed safe address is: ' + contractAddress)
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.log(transactionHash)
|
||||
})
|
||||
})
|
|
@ -4,11 +4,12 @@ import { Field } from 'react-final-form'
|
|||
import TextField from '~/components/forms/TextField'
|
||||
import { composeValidators, minValue, mustBeNumber, required } from '~/components/forms/validator'
|
||||
import Block from '~/components/layout/Block'
|
||||
import { FIELD_CONFIRMATIONS } from '~/routes/open/components/fields'
|
||||
|
||||
const Confirmations = () => (
|
||||
<Block margin="md">
|
||||
<Field
|
||||
name="confirmations"
|
||||
name={FIELD_CONFIRMATIONS}
|
||||
component={TextField}
|
||||
type="text"
|
||||
validate={composeValidators(
|
||||
|
|
|
@ -4,11 +4,12 @@ import Field from '~/components/forms/Field'
|
|||
import TextField from '~/components/forms/TextField'
|
||||
import { required } from '~/components/forms/validator'
|
||||
import Block from '~/components/layout/Block'
|
||||
import { FIELD_NAME } from '~/routes/open/components/fields'
|
||||
|
||||
const Name = () => (
|
||||
<Block margin="md">
|
||||
<Field
|
||||
name="name"
|
||||
name={FIELD_NAME}
|
||||
component={TextField}
|
||||
type="text"
|
||||
validate={required}
|
||||
|
|
|
@ -8,6 +8,7 @@ import Col from '~/components/layout/Col'
|
|||
import Heading from '~/components/layout/Heading'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import { FIELD_OWNERS, getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
|
||||
|
||||
type Props = {
|
||||
numOwners: number,
|
||||
|
@ -18,7 +19,7 @@ const Owners = ({ numOwners }: Props) => (
|
|||
<Heading tag="h3">Owners</Heading>
|
||||
<Block margin="sm">
|
||||
<Field
|
||||
name="owners"
|
||||
name={FIELD_OWNERS}
|
||||
component={TextField}
|
||||
type="text"
|
||||
validate={composeValidators(required, mustBeNumber, minValue(1))}
|
||||
|
@ -33,7 +34,7 @@ const Owners = ({ numOwners }: Props) => (
|
|||
<Paragraph bold>Owner Nº {index + 1}</Paragraph>
|
||||
<Block margin="sm">
|
||||
<Field
|
||||
name={`owner${index}Name`}
|
||||
name={getOwnerNameBy(index)}
|
||||
component={TextField}
|
||||
type="text"
|
||||
validate={required}
|
||||
|
@ -43,7 +44,7 @@ const Owners = ({ numOwners }: Props) => (
|
|||
</Block>
|
||||
<Block margin="sm">
|
||||
<Field
|
||||
name={`owner${index}Address`}
|
||||
name={getOwnerAddressBy(index)}
|
||||
component={TextField}
|
||||
type="text"
|
||||
validate={composeValidators(required, mustBeEthereumAddress)}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
export const FIELD_NAME: string = 'name'
|
||||
export const FIELD_CONFIRMATIONS: string = 'confirmations'
|
||||
export const FIELD_OWNERS: string = 'owners'
|
||||
|
||||
export const getOwnerNameBy = (index: number) => `owner${index}Name`
|
||||
export const getOwnerAddressBy = (index: number) => `owner${index}Address`
|
|
@ -1,17 +1,17 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import contract from 'truffle-contract'
|
||||
import PageFrame from '~/components/layout/PageFrame'
|
||||
import { getAccountsFrom, getThresholdFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||
import Page from '~/components/layout/Page'
|
||||
import { getAccountsFrom, getThresholdFrom, getNamesFrom, getSafeNameFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||
import { getWeb3 } from '~/wallets/getWeb3'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import Safe from '#/GnosisSafe.json'
|
||||
import selector from './selector'
|
||||
import actions, { type Actions } from './actions'
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
type Props = {
|
||||
type Props = Actions & {
|
||||
provider: string,
|
||||
userAccount: string,
|
||||
}
|
||||
|
@ -35,20 +35,21 @@ class Open extends React.Component<Props, State> {
|
|||
|
||||
onCallSafeContractSubmit = async (values) => {
|
||||
try {
|
||||
const { userAccount } = this.props
|
||||
const { userAccount, addSafe } = this.props
|
||||
const accounts = getAccountsFrom(values)
|
||||
const numConfirmations = getThresholdFrom(values)
|
||||
const name = getSafeNameFrom(values)
|
||||
const owners = getNamesFrom(values)
|
||||
|
||||
const web3 = getWeb3()
|
||||
this.safe.setProvider(web3.currentProvider)
|
||||
|
||||
const safeInstance = await this.safe.new(accounts, numConfirmations, 0, 0, { from: userAccount, gas: '5000000' })
|
||||
const { transactionHash } = safeInstance
|
||||
|
||||
const { address, transactionHash } = safeInstance
|
||||
const transactionReceipt = await promisify(cb => web3.eth.getTransactionReceipt(transactionHash, cb))
|
||||
// eslint-disable-next-line
|
||||
console.log(`Transaction Receipt${JSON.stringify(transactionReceipt)}`)
|
||||
this.setState({ safeAddress: safeInstance.address, safeTx: transactionReceipt })
|
||||
|
||||
addSafe(name, address, numConfirmations, owners, accounts)
|
||||
this.setState({ safeAddress: address, safeTx: transactionReceipt })
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log('Error while creating the Safe' + error)
|
||||
|
@ -62,7 +63,7 @@ class Open extends React.Component<Props, State> {
|
|||
const { provider, userAccount } = this.props
|
||||
|
||||
return (
|
||||
<PageFrame>
|
||||
<Page>
|
||||
<Layout
|
||||
provider={provider}
|
||||
userAccount={userAccount}
|
||||
|
@ -70,9 +71,9 @@ class Open extends React.Component<Props, State> {
|
|||
safeTx={safeTx}
|
||||
onCallSafeContractSubmit={this.onCallSafeContractSubmit}
|
||||
/>
|
||||
</PageFrame>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(selector)(Open)
|
||||
export default connect(selector, actions)(Open)
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import contract from 'truffle-contract'
|
||||
import PageFrame from '~/components/layout/PageFrame'
|
||||
import { getWeb3 } from '~/wallets/getWeb3'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import Safe from '#/GnosisSafe.json'
|
||||
import Layout from '../components/Layout'
|
||||
import selector from './selector'
|
||||
import { getAccountsFrom, getThresholdFrom } from './safe'
|
||||
|
||||
type Props = {
|
||||
provider: string,
|
||||
userAccount: string,
|
||||
}
|
||||
|
||||
type State = {
|
||||
safeAddress: string,
|
||||
funds: number,
|
||||
}
|
||||
|
||||
class Open extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
safeAddress: '',
|
||||
funds: 0,
|
||||
}
|
||||
|
||||
this.safe = contract(Safe)
|
||||
}
|
||||
|
||||
onAddFunds = async (values: Object) => {
|
||||
const { fundsToAdd } = values
|
||||
const { safeAddress } = this.state
|
||||
try {
|
||||
const web3 = getWeb3()
|
||||
const accounts = await promisify(cb => web3.eth.getAccounts(cb))
|
||||
const txData = { from: accounts[0], to: safeAddress, value: web3.toWei(fundsToAdd, 'ether') }
|
||||
await promisify(cb => web3.eth.sendTransaction(txData, cb))
|
||||
const funds = await promisify(cb => web3.eth.getBalance(safeAddress, cb))
|
||||
const fundsInEther = funds ? web3.fromWei(funds.toNumber(), 'ether') : 0
|
||||
this.setState({ funds: fundsInEther })
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log(`Errog adding funds to safe${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onCallSafeContractSubmit = async (values) => {
|
||||
try {
|
||||
const { userAccount } = this.props
|
||||
const accounts = getAccountsFrom(values)
|
||||
const numConfirmations = getThresholdFrom(values)
|
||||
|
||||
const web3 = getWeb3()
|
||||
this.safe.setProvider(web3.currentProvider)
|
||||
|
||||
const safeInstance = await this.safe.new(accounts, numConfirmations, 0, 0, { from: userAccount, gas: '5000000' })
|
||||
const { transactionHash } = safeInstance
|
||||
|
||||
const transactionReceipt = await promisify(cb => web3.eth.getTransactionReceipt(transactionHash, cb))
|
||||
// eslint-disable-next-line
|
||||
console.log(`Transaction Receipt${JSON.stringify(transactionReceipt)}`)
|
||||
this.setState({ safeAddress: safeInstance.address })
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log('Error while creating the Safe' + error)
|
||||
}
|
||||
}
|
||||
|
||||
safe: any
|
||||
|
||||
render() {
|
||||
const { provider, userAccount } = this.props
|
||||
const { safeAddress, funds } = this.state
|
||||
return (
|
||||
<PageFrame>
|
||||
{ provider
|
||||
? <Layout
|
||||
userAccount={userAccount}
|
||||
safeAddress={safeAddress}
|
||||
onAddFunds={this.onAddFunds}
|
||||
funds={funds}
|
||||
onCallSafeContractSubmit={this.onCallSafeContractSubmit}
|
||||
/>
|
||||
: <div>No metamask detected</div>
|
||||
}
|
||||
</PageFrame>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(selector)(Open)
|
|
@ -0,0 +1,10 @@
|
|||
// @flow
|
||||
import addSafe from '~/routes/safe/store/actions/addSafe'
|
||||
|
||||
export type Actions = {
|
||||
addSafe: typeof addSafe,
|
||||
}
|
||||
|
||||
export default {
|
||||
addSafe,
|
||||
}
|
|
@ -12,3 +12,5 @@ export const getNamesFrom = (values: Object): string[] => {
|
|||
}
|
||||
|
||||
export const getThresholdFrom = (values: Object): number => Number(values.confirmations)
|
||||
|
||||
export const getSafeNameFrom = (values: Object): string => values.name
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
export const SAFE_PARAM_ADDRESS = 'address'
|
||||
export const SAFELIST_ADDRESS = '/safes'
|
||||
export const OPEN_ADDRESS = '/open'
|
||||
export const WELCOME_ADDRESS = '/welcome'
|
|
@ -0,0 +1,18 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import NoSafe from '~/components/NoSafe'
|
||||
import { type SelectorProps } from '~/routes/safe/container/selector'
|
||||
import GnoSafe from './Safe'
|
||||
|
||||
type Props = SelectorProps
|
||||
|
||||
const Layout = ({ safe }: Props) => (
|
||||
<React.Fragment>
|
||||
{ safe
|
||||
? <GnoSafe safe={safe} />
|
||||
: <NoSafe text="Not found safe" />
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
export default Layout
|
|
@ -0,0 +1,24 @@
|
|||
// @flow
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import * as React from 'react'
|
||||
import styles from '~/components/layout/PageFrame/index.scss'
|
||||
import { SafeFactory } from '~/routes/safe/store/test/builder/index.builder'
|
||||
import Component from './Layout'
|
||||
|
||||
|
||||
const FrameDecorator = story => (
|
||||
<div className={styles.frame}>
|
||||
{ story() }
|
||||
</div>
|
||||
)
|
||||
|
||||
storiesOf('Routes /safe:address', module)
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Safe undefined', () => <Component safe={undefined} />)
|
||||
.add('Safe with 2 owners', () => {
|
||||
const safe = SafeFactory.twoOwnersSafe
|
||||
|
||||
return (
|
||||
<Component safe={safe} />
|
||||
)
|
||||
})
|
|
@ -0,0 +1,73 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Table, { TableBody, TableCell, TableHead, TableRow } from '~/components/layout/Table'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
|
||||
type SafeProps = {
|
||||
safe: Safe,
|
||||
}
|
||||
|
||||
const GnoSafe = ({ safe }: SafeProps) => (
|
||||
<React.Fragment>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<Paragraph size="lg">
|
||||
<Bold>{safe.name.toUpperCase()}</Bold>
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph size="lg">
|
||||
<Bold>Address</Bold>
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<Block>
|
||||
<Paragraph>
|
||||
{safe.address}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph size="lg">
|
||||
<Bold>Number of required confirmations per transaction</Bold>
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{safe.get('confirmations')}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph size="lg">
|
||||
<Bold>Owners</Bold>
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row margin="lg">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Adress</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{safe.owners.map(owner => (
|
||||
<TableRow key={safe.address}>
|
||||
<TableCell>{owner.name}</TableCell>
|
||||
<TableCell>{owner.address}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div />
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
export default GnoSafe
|
|
@ -0,0 +1,22 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import Page from '~/components/layout/Page'
|
||||
import Layout from '~/routes/safe/component/Layout'
|
||||
import selector, { type SelectorProps } from './selector'
|
||||
|
||||
type Props = SelectorProps
|
||||
|
||||
class SafeView extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { safe } = this.props
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Layout safe={safe} />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(selector)(SafeView)
|
|
@ -0,0 +1,11 @@
|
|||
// @flow
|
||||
import { createStructuredSelector } from 'reselect'
|
||||
import { safeSelector, type SafeSelectorProps } from '~/routes/safe/store/selectors'
|
||||
|
||||
export type SelectorProps = {
|
||||
safe: SafeSelectorProps,
|
||||
}
|
||||
|
||||
export default createStructuredSelector({
|
||||
safe: safeSelector,
|
||||
})
|
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import { List } from 'immutable'
|
||||
import { createAction } from 'redux-actions'
|
||||
import { type SafeProps } from '~/routes/safe/store/model/safe'
|
||||
import { makeOwner, type Owner } from '~/routes/safe/store/model/owner'
|
||||
|
||||
export const ADD_SAFE = 'ADD_SAFE'
|
||||
|
||||
export const buildOwnersFrom = (names: string[], addresses: string[]) => {
|
||||
const owners = names.map((name: string, index: number) => makeOwner({ name, address: addresses[index] }))
|
||||
|
||||
return List(owners)
|
||||
}
|
||||
|
||||
const addSafe = createAction(
|
||||
ADD_SAFE,
|
||||
(
|
||||
name: string, address: string, confirmations: number,
|
||||
ownersName: string[], ownersAddress: string[],
|
||||
): SafeProps => {
|
||||
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
|
||||
|
||||
return ({
|
||||
address, name, confirmations, owners,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export default addSafe
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
export * from './addSafe'
|
||||
export { default as addSafe } from './addSafe'
|
|
@ -0,0 +1,17 @@
|
|||
// @flow
|
||||
import { Record } from 'immutable'
|
||||
import type { RecordFactory, RecordOf } from 'immutable'
|
||||
|
||||
export type OwnerProps = {
|
||||
name: string,
|
||||
address: string,
|
||||
}
|
||||
|
||||
export const makeOwner: RecordFactory<OwnerProps> = Record({
|
||||
name: '',
|
||||
address: '',
|
||||
})
|
||||
|
||||
export type Owner = RecordOf<OwnerProps>
|
||||
|
||||
// Useage const someRecord: Owner = makeOwner({ name: ... })
|
|
@ -0,0 +1,22 @@
|
|||
// @flow
|
||||
import { List, Record } from 'immutable'
|
||||
import type { RecordFactory, RecordOf } from 'immutable'
|
||||
import type { Owner } from '~/routes/safe/store/model/owner'
|
||||
|
||||
export type SafeProps = {
|
||||
name: string,
|
||||
address: string,
|
||||
confirmations: number,
|
||||
owners: List<Owner>,
|
||||
}
|
||||
|
||||
export const makeSafe: RecordFactory<SafeProps> = Record({
|
||||
name: '',
|
||||
address: '',
|
||||
confirmations: 0,
|
||||
owners: List([]),
|
||||
})
|
||||
|
||||
export type Safe = RecordOf<SafeProps>
|
||||
|
||||
// Useage const someRecord: Safe = makeSafe({ name: ... })
|
|
@ -0,0 +1,48 @@
|
|||
// @flow
|
||||
import { Map, List } from 'immutable'
|
||||
import { handleActions, type ActionType } from 'redux-actions'
|
||||
import addSafe, { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
||||
import { makeOwner } from '~/routes/safe/store/model/owner'
|
||||
import { type Safe, makeSafe } from '~/routes/safe/store/model/safe'
|
||||
import { loadSafes, saveSafes } from '~/utils/localStorage'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
|
||||
export type State = Map<string, Safe>
|
||||
|
||||
const buildSafesFrom = (loadedSafes: Object): State => {
|
||||
const safes: State = Map()
|
||||
|
||||
return safes.withMutations((map: State) => {
|
||||
Object.keys(loadedSafes).forEach((address) => {
|
||||
const safe = loadedSafes[address]
|
||||
safe.owners = List(safe.owners.map((owner => makeOwner(owner))))
|
||||
return map.set(address, makeSafe(safe))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const calculateInitialState = (): State => {
|
||||
const storedSafes = loadSafes()
|
||||
|
||||
return storedSafes ? buildSafesFrom(storedSafes) : Map()
|
||||
}
|
||||
|
||||
/*
|
||||
type Action<T> = {
|
||||
key: string,
|
||||
payload: T,
|
||||
};
|
||||
|
||||
type AddSafeType = Action<SafeProps>
|
||||
|
||||
action: AddSafeType
|
||||
*/
|
||||
|
||||
export default handleActions({
|
||||
[ADD_SAFE]: (state: State, action: ActionType<typeof addSafe>): State => {
|
||||
const safes = state.set(action.payload.address, makeSafe(action.payload))
|
||||
saveSafes(safes.toJSON())
|
||||
return safes
|
||||
},
|
||||
}, Map())
|
|
@ -0,0 +1,32 @@
|
|||
// @flow
|
||||
import { Map } from 'immutable'
|
||||
import { type Match } from 'react-router-dom'
|
||||
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { SAFE_PARAM_ADDRESS } from '~/routes/routes'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { safesMapSelector } from '~/routes/safeList/store/selectors'
|
||||
|
||||
type RouterProps = {
|
||||
match: Match,
|
||||
}
|
||||
|
||||
const safeAddessSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
|
||||
|
||||
export type SafeSelectorProps = Safe | typeof undefined
|
||||
|
||||
export const safeSelector: Selector<GlobalState, RouterProps, SafeSelectorProps> = createSelector(
|
||||
safesMapSelector,
|
||||
safeAddessSelector,
|
||||
(safes: Map<string, Safe>, address: string) => {
|
||||
if (!address) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return safes.get(address)
|
||||
},
|
||||
)
|
||||
|
||||
export default createStructuredSelector({
|
||||
safe: safeSelector,
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
import { makeSafe, type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { buildOwnersFrom } from '~/routes/safe/store/actions'
|
||||
|
||||
class SafeBuilder {
|
||||
safe: Safe
|
||||
|
||||
constructor() {
|
||||
this.safe = makeSafe()
|
||||
}
|
||||
|
||||
withAddress(address: string) {
|
||||
this.safe = this.safe.set('address', address)
|
||||
return this
|
||||
}
|
||||
|
||||
withName(name: string) {
|
||||
this.safe = this.safe.set('name', name)
|
||||
return this
|
||||
}
|
||||
|
||||
withConfirmations(confirmations: number) {
|
||||
this.safe = this.safe.set('confirmations', confirmations)
|
||||
return this
|
||||
}
|
||||
|
||||
withOwner(names: string[], adresses: string[]) {
|
||||
const owners = buildOwnersFrom(names, adresses)
|
||||
this.safe = this.safe.set('owners', owners)
|
||||
return this
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.safe
|
||||
}
|
||||
}
|
||||
|
||||
const aSafe = () => new SafeBuilder()
|
||||
|
||||
export class SafeFactory {
|
||||
static oneOwnerSafe = aSafe()
|
||||
.withAddress('0x03db1a8b26d08df23337e9276a36b474510f0025')
|
||||
.withName('Adol ICO Safe')
|
||||
.withConfirmations(1)
|
||||
.withOwner(['Adol Metamask'], ['0x03db1a8b26d08df23337e9276a36b474510f0023'])
|
||||
.get()
|
||||
|
||||
static twoOwnersSafe = aSafe()
|
||||
.withAddress('0x03db1a8b26d08df23337e9276a36b474510f0026')
|
||||
.withName('Adol & Tobias Safe')
|
||||
.withConfirmations(2)
|
||||
.withOwner(
|
||||
['Adol Metamask', 'Tobias Metamask'],
|
||||
['0x03db1a8b26d08df23337e9276a36b474510f0023', '0x03db1a8b26d08df23337e9276a36b474510f0024'],
|
||||
)
|
||||
.get()
|
||||
}
|
||||
|
||||
export default aSafe
|
|
@ -0,0 +1,86 @@
|
|||
// @flow
|
||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import safeReducer, { calculateInitialState, SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
||||
import addSafe from '~/routes/safe/store/actions/addSafe'
|
||||
import * as SafeFields from '~/routes/open/components/fields'
|
||||
import { getAccountsFrom, getNamesFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||
import { SafeFactory } from './builder/index.builder'
|
||||
|
||||
const aStore = (initState) => {
|
||||
const reducers = combineReducers({
|
||||
[SAFE_REDUCER_ID]: safeReducer,
|
||||
})
|
||||
const middlewares = [
|
||||
thunk,
|
||||
]
|
||||
const enhancers = [
|
||||
applyMiddleware(...middlewares),
|
||||
]
|
||||
return createStore(reducers, initState, compose(...enhancers))
|
||||
}
|
||||
|
||||
const providerReducerTests = () => {
|
||||
describe('Safe Actions[addSafe]', () => {
|
||||
let store
|
||||
beforeEach(() => {
|
||||
store = aStore()
|
||||
})
|
||||
|
||||
it('reducer should return SafeRecord from form values', () => {
|
||||
// GIVEN
|
||||
const address = '0x03db1a8b26d08df23337e9276a36b474510f0025'
|
||||
const formValues = {
|
||||
[SafeFields.FIELD_NAME]: 'Adol ICO Safe',
|
||||
[SafeFields.FIELD_CONFIRMATIONS]: 1,
|
||||
[SafeFields.FIELD_OWNERS]: 1,
|
||||
[SafeFields.getOwnerAddressBy(0)]: '0x03db1a8b26d08df23337e9276a36b474510f0023',
|
||||
[SafeFields.getOwnerNameBy(0)]: 'Adol Metamask',
|
||||
address,
|
||||
}
|
||||
|
||||
// WHEN
|
||||
store.dispatch(addSafe(
|
||||
formValues[SafeFields.FIELD_NAME],
|
||||
formValues.address,
|
||||
formValues[SafeFields.FIELD_CONFIRMATIONS],
|
||||
getNamesFrom(formValues),
|
||||
getAccountsFrom(formValues),
|
||||
))
|
||||
const safes = store.getState()[SAFE_REDUCER_ID]
|
||||
|
||||
// THEN
|
||||
expect(safes.get(address)).toEqual(SafeFactory.oneOwnerSafe)
|
||||
})
|
||||
|
||||
it('reducer loads information from localStorage', async () => {
|
||||
// GIVEN
|
||||
const address = '0x03db1a8b26d08df23337e9276a36b474510f0025'
|
||||
const formValues = {
|
||||
[SafeFields.FIELD_NAME]: 'Adol ICO Safe',
|
||||
[SafeFields.FIELD_CONFIRMATIONS]: 1,
|
||||
[SafeFields.FIELD_OWNERS]: 1,
|
||||
[SafeFields.getOwnerAddressBy(0)]: '0x03db1a8b26d08df23337e9276a36b474510f0023',
|
||||
[SafeFields.getOwnerNameBy(0)]: 'Adol Metamask',
|
||||
address,
|
||||
}
|
||||
|
||||
// WHEN
|
||||
store.dispatch(addSafe(
|
||||
formValues[SafeFields.FIELD_NAME],
|
||||
formValues.address,
|
||||
formValues[SafeFields.FIELD_CONFIRMATIONS],
|
||||
getNamesFrom(formValues),
|
||||
getAccountsFrom(formValues),
|
||||
))
|
||||
|
||||
const anotherStore = aStore({ [SAFE_REDUCER_ID]: calculateInitialState() })
|
||||
const safes = anotherStore.getState()[SAFE_REDUCER_ID]
|
||||
|
||||
// THEN
|
||||
expect(calculateInitialState()).toEqual(safes)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default providerReducerTests
|
|
@ -0,0 +1,54 @@
|
|||
// @flow
|
||||
import { Map } from 'immutable'
|
||||
import { type Match } from 'react-router-dom'
|
||||
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { SafeFactory } from '~/routes/safe/store/test/builder/index.builder'
|
||||
import { safeSelector } from '../selectors'
|
||||
|
||||
const buildMathPropsFrom = (address): Match => ({
|
||||
params: {
|
||||
address,
|
||||
},
|
||||
isExact: true,
|
||||
path: '',
|
||||
url: '',
|
||||
})
|
||||
|
||||
const safeSelectorTests = () => {
|
||||
describe('Safe Selector[safeSelector]', () => {
|
||||
it('should return empty list when no safes', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [SAFE_REDUCER_ID]: Map(), providers: undefined }
|
||||
const match: Match = buildMathPropsFrom('fooAddress')
|
||||
|
||||
// WHEN
|
||||
const safes = safeSelector(reduxStore, { match })
|
||||
|
||||
// THEN
|
||||
expect(safes).toBe(undefined)
|
||||
})
|
||||
|
||||
it('should return a list of size 2 when 2 safes are created', () => {
|
||||
// GIVEN
|
||||
let map: Map<string, Safe> = Map()
|
||||
map = map.set('fooAddress', SafeFactory.oneOwnerSafe)
|
||||
map = map.set('barAddress', SafeFactory.twoOwnersSafe)
|
||||
|
||||
const match: Match = buildMathPropsFrom('fooAddress')
|
||||
const undefMatch: Match = buildMathPropsFrom('inventedAddress')
|
||||
|
||||
const reduxStore = { [SAFE_REDUCER_ID]: map, providers: undefined }
|
||||
|
||||
// WHEN
|
||||
const oneOwnerSafe = safeSelector(reduxStore, { match })
|
||||
const undefinedSafe = safeSelector(reduxStore, { match: undefMatch })
|
||||
|
||||
// THEN
|
||||
expect(oneOwnerSafe).toEqual(SafeFactory.oneOwnerSafe)
|
||||
expect(undefinedSafe).toBe(undefined)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default safeSelectorTests
|
|
@ -0,0 +1,11 @@
|
|||
// @flow
|
||||
import safeReducerTests from './safe.reducer'
|
||||
import safeSelectorTests from './safe.selector'
|
||||
|
||||
describe('Safe Test suite', () => {
|
||||
// ACTIONS AND REDUCERS
|
||||
safeReducerTests()
|
||||
|
||||
// SAFE SELECTOR
|
||||
safeSelectorTests()
|
||||
})
|
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import { List } from 'immutable'
|
||||
import * as React from 'react'
|
||||
import NoSafe from '~/components/NoSafe'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
import SafeTable from '~/routes/safeList/components/SafeTable'
|
||||
|
||||
type Props = {
|
||||
safes: List<Safe>
|
||||
}
|
||||
|
||||
const SafeList = ({ safes }: Props) => {
|
||||
const safesAvailable = safes && safes.count() > 0
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ safesAvailable
|
||||
? <SafeTable safes={safes} />
|
||||
: <NoSafe text="No safes created, please create a new one" />
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeList
|
|
@ -0,0 +1,24 @@
|
|||
// @flow
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import { List } from 'immutable'
|
||||
import * as React from 'react'
|
||||
import styles from '~/components/layout/PageFrame/index.scss'
|
||||
import { SafeFactory } from '~/routes/safe/store/test/builder/index.builder'
|
||||
import Component from './Layout'
|
||||
|
||||
|
||||
const FrameDecorator = story => (
|
||||
<div className={styles.frame}>
|
||||
{ story() }
|
||||
</div>
|
||||
)
|
||||
|
||||
storiesOf('Routes /safes', module)
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Safe List whithout safes', () => <Component safes={List([])} />)
|
||||
.add('Safe List whith 2 safes', () => {
|
||||
const safes = List([SafeFactory.oneOwnerSafe, SafeFactory.twoOwnersSafe])
|
||||
return (
|
||||
<Component safes={safes} />
|
||||
)
|
||||
})
|
|
@ -0,0 +1,42 @@
|
|||
// @flow
|
||||
import { List } from 'immutable'
|
||||
import * as React from 'react'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Link from '~/components/layout/Link'
|
||||
import Table, { TableBody, TableCell, TableHead, TableRow } from '~/components/layout/Table'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
|
||||
type Props = {
|
||||
safes: List<Safe>
|
||||
}
|
||||
const SafeTable = ({ safes }: Props) => (
|
||||
<Table size={900}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Open</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Deployed Address</TableCell>
|
||||
<TableCell numeric>Confirmations</TableCell>
|
||||
<TableCell numeric>Number of owners</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{safes.map(safe => (
|
||||
<TableRow key={safe.address}>
|
||||
<TableCell>
|
||||
<Link to={`${SAFELIST_ADDRESS}/${safe.address}`}>
|
||||
<Button variant="raised" size="small" color="primary">Open</Button>
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell padding="none">{safe.name}</TableCell>
|
||||
<TableCell padding="none">{safe.address}</TableCell>
|
||||
<TableCell padding="none" numeric>{safe.confirmations}</TableCell>
|
||||
<TableCell padding="none" numeric>{safe.owners.count()}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
|
||||
export default SafeTable
|
|
@ -0,0 +1,20 @@
|
|||
// @flow
|
||||
import { List } from 'immutable'
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import Page from '~/components/layout/Page'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
import Layout from '../components/Layout'
|
||||
import selector from './selector'
|
||||
|
||||
type Props = {
|
||||
safes: List<Safe>
|
||||
}
|
||||
|
||||
const SafeList = ({ safes }: Props) => (
|
||||
<Page overflow>
|
||||
<Layout safes={safes} />
|
||||
</Page>
|
||||
)
|
||||
|
||||
export default connect(selector)(SafeList)
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
import { createStructuredSelector } from 'reselect'
|
||||
import { safesListSelector } from '~/routes/safeList/store/selectors'
|
||||
|
||||
export default createStructuredSelector({
|
||||
safes: safesListSelector,
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
|
||||
export const safesMapSelector = (state: GlobalState): Map<string, Safe> => state.safes
|
||||
export const safesListSelector = (state: GlobalState): List<Safe> => state.safes.toList()
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
import safesSelectorTests from './safes.selector'
|
||||
|
||||
describe('SafeList Test suite', () => {
|
||||
// safesSelector SELECTOR
|
||||
safesSelectorTests()
|
||||
})
|
|
@ -0,0 +1,40 @@
|
|||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { SafeFactory } from '~/routes/safe/store/test/builder/index.builder'
|
||||
import { safesListSelector } from '../selectors'
|
||||
|
||||
const safesListSelectorTests = () => {
|
||||
describe('Safes Selector[safesSelector]', () => {
|
||||
it('should return empty list when no safes', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [SAFE_REDUCER_ID]: Map(), providers: undefined }
|
||||
const emptyList = List([])
|
||||
|
||||
// WHEN
|
||||
const safes = safesListSelector(reduxStore)
|
||||
|
||||
// THEN
|
||||
expect(safes).toEqual(emptyList)
|
||||
})
|
||||
|
||||
it('should return a list of size 2 when 2 safes are created', () => {
|
||||
// GIVEN
|
||||
let map: Map<string, Safe> = Map()
|
||||
map = map.set('fooAddress', SafeFactory.oneOwnerSafe)
|
||||
map = map.set('barAddress', SafeFactory.twoOwnersSafe)
|
||||
|
||||
const reduxStore = { [SAFE_REDUCER_ID]: map, providers: undefined }
|
||||
|
||||
// WHEN
|
||||
const safes = safesListSelector(reduxStore)
|
||||
|
||||
// THEN
|
||||
expect(safes.count()).toEqual(2)
|
||||
expect(safes.get(0)).not.toEqual(safes.get(1))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default safesListSelectorTests
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
class Layout extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>I am transactions Layout</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Layout
|
|
@ -4,6 +4,7 @@ import Block from '~/components/layout/Block'
|
|||
import Img from '~/components/layout/Img'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Link from '~/components/layout/Link'
|
||||
import { OPEN_ADDRESS, SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
import styles from './Layout.scss'
|
||||
|
||||
const vault = require('../assets/vault.svg')
|
||||
|
@ -12,20 +13,26 @@ type Props = {
|
|||
provider: string
|
||||
}
|
||||
|
||||
type SafeProps = {
|
||||
size?: 'small' | 'medium',
|
||||
}
|
||||
|
||||
export const CreateSafe = ({ size }: SafeProps) => (
|
||||
<Link to={OPEN_ADDRESS}>
|
||||
<Button variant="raised" size={size || 'medium'} color="primary">
|
||||
Create a new Safe
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
|
||||
const Welcome = ({ provider }: Props) => (
|
||||
<Block className={styles.safe}>
|
||||
<Img alt="Safe Box" src={vault} height={330} />
|
||||
<Block className={styles.safeActions} margin="md">
|
||||
{ provider &&
|
||||
<Link to="/open">
|
||||
<Button variant="raised" color="primary">
|
||||
Create a new Safe
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
<Link to="/transactions">
|
||||
{ provider && <CreateSafe /> }
|
||||
<Link to={SAFELIST_ADDRESS}>
|
||||
<Button variant="raised" color="primary">
|
||||
Open a Safe
|
||||
See Safe list
|
||||
</Button>
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -12,7 +12,7 @@ const FrameDecorator = story => (
|
|||
)
|
||||
|
||||
|
||||
storiesOf('Routes', module)
|
||||
storiesOf('Routes /welcome', module)
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Welcome with Metamask connected', () => {
|
||||
const provider = select('Status by Provider', ['', 'UNKNOWN', 'METAMASK', 'PARITY'], 'METAMASK')
|
||||
|
@ -23,3 +23,21 @@ storiesOf('Routes', module)
|
|||
/>
|
||||
)
|
||||
})
|
||||
.add('Welcome with unknown wallet', () => {
|
||||
const provider = select('Status by Provider', ['', 'UNKNOWN', 'METAMASK', 'PARITY'], 'UNKNOWN')
|
||||
return (
|
||||
<Component
|
||||
provider={provider}
|
||||
fetchProvider={() => { }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
.add('Welcome without wallet connected', () => {
|
||||
const provider = select('Status by Provider', ['', 'UNKNOWN', 'METAMASK', 'PARITY'], '')
|
||||
return (
|
||||
<Component
|
||||
provider={provider}
|
||||
fetchProvider={() => { }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PageFrame from '~/components/layout/PageFrame'
|
||||
import Page from '~/components/layout/Page'
|
||||
import Layout from '../components/Layout'
|
||||
import selector from './selector'
|
||||
|
||||
|
@ -10,9 +10,9 @@ type Props = {
|
|||
}
|
||||
|
||||
const Welcome = ({ provider }: Props) => (
|
||||
<PageFrame align="center">
|
||||
<Page align="center">
|
||||
<Layout provider={provider} />
|
||||
</PageFrame>
|
||||
</Page>
|
||||
)
|
||||
|
||||
export default connect(selector)(Welcome)
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
// @flow
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { routerMiddleware, routerReducer } from 'react-router-redux'
|
||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
|
||||
import { combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import provider, { REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/wallets/store/reducer/provider'
|
||||
import safe, { SAFE_REDUCER_ID, calculateInitialState, type State as SafeState } from '~/routes/safe/store/reducer/safe'
|
||||
|
||||
export const history = createBrowserHistory()
|
||||
|
||||
// eslint-disable-next-line
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
||||
const finalCreateStore = composeEnhancers(applyMiddleware(
|
||||
|
@ -13,9 +15,17 @@ const finalCreateStore = composeEnhancers(applyMiddleware(
|
|||
routerMiddleware(history),
|
||||
))
|
||||
|
||||
const reducers = combineReducers({
|
||||
export type GlobalState = {
|
||||
providers: ProviderState,
|
||||
safes: SafeState,
|
||||
}
|
||||
|
||||
const reducers: Reducer<GlobalState> = combineReducers({
|
||||
routing: routerReducer,
|
||||
[REDUCER_ID]: provider,
|
||||
[PROVIDER_REDUCER_ID]: provider,
|
||||
[SAFE_REDUCER_ID]: safe,
|
||||
})
|
||||
|
||||
export const store = createStore(reducers, finalCreateStore)
|
||||
const initialState = { [SAFE_REDUCER_ID]: calculateInitialState() }
|
||||
|
||||
export const store: Store<GlobalState> = createStore(reducers, initialState, finalCreateStore)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
const primary = '#1798cc'
|
||||
const secondary = '#13222b'
|
||||
const tertiary = '#f6f9fc'
|
||||
const xs = '4px'
|
||||
const sm = '8px'
|
||||
const md = '16px'
|
||||
const lg = '24px'
|
||||
|
@ -11,6 +12,7 @@ module.exports = Object.assign({}, {
|
|||
primary,
|
||||
secondary,
|
||||
tertiary,
|
||||
xs,
|
||||
sm,
|
||||
md,
|
||||
lg,
|
||||
|
@ -24,6 +26,7 @@ module.exports = Object.assign({}, {
|
|||
smallFontSize: '12px',
|
||||
mediumFontSize: '14px',
|
||||
largeFontSize: '18px',
|
||||
extraLargeFontSize: '24px',
|
||||
screenXs: 480,
|
||||
screenXsMax: 767,
|
||||
screenSm: 768,
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// @flow
|
||||
const SAFES_KEY = 'SAFES'
|
||||
|
||||
export const loadSafes = () => {
|
||||
try {
|
||||
const serializedState = localStorage.getItem(SAFES_KEY)
|
||||
if (serializedState === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (serializedState === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return JSON.parse(serializedState)
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const saveSafes = (safes: Object) => {
|
||||
try {
|
||||
const serializedState = JSON.stringify(safes)
|
||||
localStorage.setItem(SAFES_KEY, serializedState)
|
||||
} catch (err) {
|
||||
// Ignore write errors
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
import { type Provider } from '~/wallets/store/model/provider'
|
||||
|
||||
export const ADD_PROVIDER = 'ADD_PROVIDER'
|
||||
|
||||
export default createAction(ADD_PROVIDER)
|
||||
const addProvider = createAction(ADD_PROVIDER, (provider: Provider) => provider)
|
||||
|
||||
export default addProvider
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
// @flow
|
||||
import { handleActions } from 'redux-actions'
|
||||
import { makeProvider } from '~/wallets/store/model/provider'
|
||||
import { ADD_PROVIDER } from '~/wallets/store/actions/addProvider'
|
||||
import { handleActions, type ActionType } from 'redux-actions'
|
||||
import { makeProvider, type Provider } from '~/wallets/store/model/provider'
|
||||
import addProvider, { ADD_PROVIDER } from '~/wallets/store/actions/addProvider'
|
||||
|
||||
export const REDUCER_ID = 'providers'
|
||||
export const PROVIDER_REDUCER_ID = 'providers'
|
||||
|
||||
export type State = Provider
|
||||
|
||||
export default handleActions({
|
||||
[ADD_PROVIDER]: (state, { payload }) =>
|
||||
[ADD_PROVIDER]: (state: State, { payload }: ActionType<typeof addProvider>) =>
|
||||
makeProvider(payload),
|
||||
}, makeProvider())
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
import { createSelector } from 'reselect'
|
||||
import type { Provider } from '~/wallets/store/model/provider'
|
||||
import { REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import { PROVIDER_REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
|
||||
const providerSelector = (state: any): Provider => state[REDUCER_ID]
|
||||
const providerSelector = (state: any): Provider => state[PROVIDER_REDUCER_ID]
|
||||
|
||||
export const userAccountSelector = createSelector(
|
||||
providerSelector,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import { PROVIDER_REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import { userAccountSelector } from '../selectors'
|
||||
import { ProviderFactory } from './builder/index.builder'
|
||||
|
||||
|
@ -7,7 +7,7 @@ const providerReducerTests = () => {
|
|||
describe('Provider Name Selector[userAccountSelector]', () => {
|
||||
it('should return empty when no provider is loaded', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [REDUCER_ID]: ProviderFactory.noProvider }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider }
|
||||
|
||||
// WHEN
|
||||
const providerName = userAccountSelector(reduxStore)
|
||||
|
@ -18,7 +18,7 @@ const providerReducerTests = () => {
|
|||
|
||||
it('should return empty when Metamask is loaded but not available', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [REDUCER_ID]: ProviderFactory.metamaskLoaded }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded }
|
||||
|
||||
// WHEN
|
||||
const providerName = userAccountSelector(reduxStore)
|
||||
|
@ -29,7 +29,7 @@ const providerReducerTests = () => {
|
|||
|
||||
it('should return account when Metamask is loaded and available', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [REDUCER_ID]: ProviderFactory.metamaskAvailable }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable }
|
||||
|
||||
// WHEN
|
||||
const providerName = userAccountSelector(reduxStore)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import { PROVIDER_REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import { providerNameSelector } from '../selectors'
|
||||
import { ProviderFactory } from './builder/index.builder'
|
||||
|
||||
|
@ -7,7 +7,7 @@ const providerReducerTests = () => {
|
|||
describe('Provider Name Selector[providerNameSelector]', () => {
|
||||
it('should return undefined when no provider is loaded', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [REDUCER_ID]: ProviderFactory.noProvider }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider }
|
||||
|
||||
// WHEN
|
||||
const providerName = providerNameSelector(reduxStore)
|
||||
|
@ -18,7 +18,7 @@ const providerReducerTests = () => {
|
|||
|
||||
it('should return undefined when Metamask is loaded but not available', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [REDUCER_ID]: ProviderFactory.metamaskLoaded }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded }
|
||||
|
||||
// WHEN
|
||||
const providerName = providerNameSelector(reduxStore)
|
||||
|
@ -29,7 +29,7 @@ const providerReducerTests = () => {
|
|||
|
||||
it('should return METAMASK when Metamask is loaded and available', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [REDUCER_ID]: ProviderFactory.metamaskAvailable }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable }
|
||||
|
||||
// WHEN
|
||||
const providerName = providerNameSelector(reduxStore)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import providerReducer, { REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import providerReducer, { PROVIDER_REDUCER_ID } from '~/wallets/store/reducer/provider'
|
||||
import type { ProviderProps } from '~/wallets/store/model/provider'
|
||||
import { makeProvider } from '~/wallets/store/model/provider'
|
||||
import { processProviderResponse } from '../actions/fetchProvider'
|
||||
|
@ -11,7 +11,7 @@ const providerReducerTests = () => {
|
|||
let store
|
||||
beforeEach(() => {
|
||||
const reducers = combineReducers({
|
||||
[REDUCER_ID]: providerReducer,
|
||||
[PROVIDER_REDUCER_ID]: providerReducer,
|
||||
})
|
||||
const middlewares = [
|
||||
thunk,
|
||||
|
@ -30,7 +30,7 @@ const providerReducerTests = () => {
|
|||
|
||||
// WHEN
|
||||
processProviderResponse(store.dispatch, emptyResponse)
|
||||
const provider = store.getState()[REDUCER_ID]
|
||||
const provider = store.getState()[PROVIDER_REDUCER_ID]
|
||||
|
||||
// THEN
|
||||
expect(makeProvider(emptyResponse)).toEqual(provider)
|
||||
|
@ -44,7 +44,7 @@ const providerReducerTests = () => {
|
|||
|
||||
// WHEN
|
||||
processProviderResponse(store.dispatch, metamaskLoaded)
|
||||
const provider = store.getState()[REDUCER_ID]
|
||||
const provider = store.getState()[PROVIDER_REDUCER_ID]
|
||||
|
||||
// THEN
|
||||
expect(makeProvider(metamaskLoaded)).toEqual(provider)
|
||||
|
@ -58,7 +58,7 @@ const providerReducerTests = () => {
|
|||
|
||||
// WHEN
|
||||
processProviderResponse(store.dispatch, metamask)
|
||||
const provider = store.getState()[REDUCER_ID]
|
||||
const provider = store.getState()[PROVIDER_REDUCER_ID]
|
||||
|
||||
// THEN
|
||||
expect(makeProvider(metamask)).toEqual(provider)
|
||||
|
|
72
yarn.lock
72
yarn.lock
|
@ -4177,6 +4177,10 @@ entities@~1.1.1:
|
|||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
|
||||
|
||||
envinfo@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-4.4.2.tgz#472c49f3a8b9bca73962641ce7cb692bf623cd1c"
|
||||
|
||||
errno@^0.1.3, errno@~0.1.1, errno@~0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
|
||||
|
@ -5175,6 +5179,13 @@ fuse.js@^3.0.1, fuse.js@^3.2.0:
|
|||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.0.tgz#f0448e8069855bf2a3e683cdc1d320e7e2a07ef4"
|
||||
|
||||
ganache-cli@^6.0.3:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.1.0.tgz#486c846497204b644166b5f0f74c9b41d02bdc25"
|
||||
dependencies:
|
||||
source-map-support "^0.5.3"
|
||||
webpack-cli "^2.0.9"
|
||||
|
||||
gauge@~2.7.3:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||
|
@ -9942,6 +9953,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
|||
dependencies:
|
||||
aproba "^1.1.1"
|
||||
|
||||
run-with-testrpc@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/run-with-testrpc/-/run-with-testrpc-0.3.0.tgz#77205fc63e44e62202e0e5de51596038d82ba519"
|
||||
dependencies:
|
||||
colors "^1.1.2"
|
||||
ganache-cli "^6.0.3"
|
||||
|
||||
rustbn.js@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.1.2.tgz#979fa0f9562216dd667c9d2cd179ae5d13830eff"
|
||||
|
@ -10435,6 +10453,12 @@ source-map-support@^0.5.0:
|
|||
dependencies:
|
||||
source-map "^0.6.0"
|
||||
|
||||
source-map-support@^0.5.3:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.4.tgz#54456efa89caa9270af7cd624cc2f123e51fbae8"
|
||||
dependencies:
|
||||
source-map "^0.6.0"
|
||||
|
||||
source-map-url@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
|
||||
|
@ -11698,6 +11722,37 @@ webpack-cli@^2.0.8:
|
|||
yeoman-environment "^2.0.0"
|
||||
yeoman-generator "^2.0.3"
|
||||
|
||||
webpack-cli@^2.0.9:
|
||||
version "2.0.14"
|
||||
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-2.0.14.tgz#71d03d8c10547c1dfd674f71ff3b0457c33a74cd"
|
||||
dependencies:
|
||||
chalk "^2.3.2"
|
||||
cross-spawn "^6.0.5"
|
||||
diff "^3.5.0"
|
||||
enhanced-resolve "^4.0.0"
|
||||
envinfo "^4.4.2"
|
||||
glob-all "^3.1.0"
|
||||
global-modules "^1.0.0"
|
||||
got "^8.2.0"
|
||||
import-local "^1.0.0"
|
||||
inquirer "^5.1.0"
|
||||
interpret "^1.0.4"
|
||||
jscodeshift "^0.5.0"
|
||||
listr "^0.13.0"
|
||||
loader-utils "^1.1.0"
|
||||
lodash "^4.17.5"
|
||||
log-symbols "^2.2.0"
|
||||
mkdirp "^0.5.1"
|
||||
p-each-series "^1.0.0"
|
||||
p-lazy "^1.0.0"
|
||||
prettier "^1.5.3"
|
||||
supports-color "^5.3.0"
|
||||
v8-compile-cache "^1.1.2"
|
||||
webpack-addons "^1.1.5"
|
||||
yargs "^11.1.0"
|
||||
yeoman-environment "^2.0.0"
|
||||
yeoman-generator "^2.0.3"
|
||||
|
||||
webpack-dev-middleware@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.0.1.tgz#7ffd6d0192883c83d3f262e8d7dec822493c6166"
|
||||
|
@ -12099,6 +12154,23 @@ yargs@^11.0.0:
|
|||
y18n "^3.2.1"
|
||||
yargs-parser "^9.0.2"
|
||||
|
||||
yargs@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77"
|
||||
dependencies:
|
||||
cliui "^4.0.0"
|
||||
decamelize "^1.1.1"
|
||||
find-up "^2.1.0"
|
||||
get-caller-file "^1.0.1"
|
||||
os-locale "^2.0.0"
|
||||
require-directory "^2.1.1"
|
||||
require-main-filename "^1.0.1"
|
||||
set-blocking "^2.0.0"
|
||||
string-width "^2.0.0"
|
||||
which-module "^2.0.0"
|
||||
y18n "^3.2.1"
|
||||
yargs-parser "^9.0.2"
|
||||
|
||||
yargs@^3.27.0, yargs@^3.29.0:
|
||||
version "3.32.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"
|
||||
|
|
Loading…
Reference in New Issue