Pull from dev, conflict fixes

This commit is contained in:
Mikhail Mikheev 2019-09-30 16:05:08 +04:00
commit 99ddb5989f
82 changed files with 2017 additions and 1329 deletions

View File

@ -9,11 +9,20 @@ node_js:
os:
- linux
env:
- DOCKER_COMPOSE_VERSION=1.22.0
global:
- DOCKER_COMPOSE_VERSION=1.22.0
matrix:
include:
- env:
- REACT_APP_NETWORK='mainnet'
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
if: (branch = master AND NOT type = pull_request) OR tag IS present
- env:
- REACT_APP_NETWORK='rinkeby'
before_install:
# Install custom docker-compose version
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- curl -Ls https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
# Shut down postgres because it blocks our db container's port map to :5432
@ -50,6 +59,7 @@ after_success:
else
export NODE_ENV=development;
fi
- if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi
- yarn build-storybook
- yarn build
# Pull Request - Deploy it to a review environment
@ -91,3 +101,18 @@ deploy:
upload-dir: current
on:
branch: master
# Prepare production deployment
- provider: s3
bucket: $STAGING_BUCKET_NAME
secret_access_key: $AWS_SECRET_ACCESS_KEY
access_key_id: $AWS_ACCESS_KEY_ID
skip_cleanup: true
local_dir: build_webpack
upload-dir: releases/$TRAVIS_TAG
on:
tags: true
- provider: script
script: bash config/travis/prepare_production_deployment.sh
on:
tags: true

View File

@ -1,28 +1,29 @@
// @flow
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
var REACT_APP = /^REACT_APP_/i;
const REACT_APP = /^REACT_APP_/i
function getClientEnvironment(publicUrl) {
var processEnv = Object
.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce((env, key) => {
env[key] = JSON.stringify(process.env[key]);
return env;
}, {
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
'NODE_ENV': JSON.stringify(
process.env.NODE_ENV || 'development'
),
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
'PUBLIC_URL': JSON.stringify(publicUrl)
});
return {'process.env': processEnv};
const processEnv = Object.keys(process.env)
.filter((key) => REACT_APP.test(key))
.reduce(
(env, key) => {
env[key] = JSON.stringify(process.env[key])
return env
},
{
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: JSON.stringify(publicUrl),
},
)
return { 'process.env': processEnv }
}
module.exports = getClientEnvironment;
module.exports = getClientEnvironment

View File

@ -0,0 +1,17 @@
#!/bin/bash
set -ev
# Only:
# - Tagged commits
# - Security env variables are available.
if [ -n "$TRAVIS_TAG" ] && [ -n "$PROD_DEPLOYMENT_HOOK_TOKEN" ] && [ -n "$PROD_DEPLOYMENT_HOOK_URL" ]
then
curl --silent --output /dev/null --write-out "%{http_code}" -X POST \
-F token="$PROD_DEPLOYMENT_HOOK_TOKEN" \
-F ref=master \
-F "variables[TRIGGER_RELEASE_COMMIT_TAG]=$TRAVIS_TAG" \
$PROD_DEPLOYMENT_HOOK_URL
else
echo "[ERROR] Production deployment could not be prepared"
fi

View File

@ -17,10 +17,12 @@
},
"scripts": {
"build": "node scripts/build.js",
"build-mainnet": "REACT_APP_NETWORK=mainnet yarn build",
"build-storybook": "build-storybook -o build_storybook",
"flow": "flow",
"precommit": "./precommit.sh",
"start": "node scripts/start.js",
"start-mainnet": "REACT_APP_NETWORK=mainnet yarn start",
"storybook": "start-storybook -p 6006",
"test": "NODE_ENV=test && node scripts/test.js --env=jsdom",
"format": "prettier-eslint \"src/**/*.js\" --write"
@ -38,28 +40,29 @@
"axios": "0.19.0",
"bignumber.js": "9.0.0",
"connected-react-router": "6.5.2",
"date-fns": "2.3.0",
"date-fns": "2.4.1",
"ethereum-ens": "0.7.8",
"final-form": "4.18.5",
"history": "4.10.1",
"immortal-db": "^1.0.2",
"immutable": "^4.0.0-rc.9",
"material-ui-search-bar": "^1.0.0-beta.13",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
"optimize-css-assets-webpack-plugin": "5.0.3",
"qrcode.react": "^0.9.3",
"react": "16.9.0",
"react-dom": "16.9.0",
"react": "16.10.1",
"react-dom": "16.10.1",
"react-final-form": "6.3.0",
"react-final-form-listeners": "^1.0.2",
"react-hot-loader": "4.12.14",
"react-infinite-scroll-component": "4.5.3",
"react-qr-reader": "^2.2.1",
"react-redux": "7.1.1",
"react-router-dom": "5.1.0",
"react-router-dom": "5.1.1",
"recompose": "^0.30.0",
"redux": "4.0.4",
"redux-actions": "^2.3.0",
"redux-thunk": "^2.2.0",
"redux-actions": "^2.6.5",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"web3": "1.2.1"
},
@ -71,24 +74,24 @@
"@babel/plugin-proposal-do-expressions": "7.6.0",
"@babel/plugin-proposal-export-default-from": "7.5.2",
"@babel/plugin-proposal-export-namespace-from": "7.5.2",
"@babel/plugin-proposal-function-bind": "^7.0.0",
"@babel/plugin-proposal-function-bind": "^7.2.0",
"@babel/plugin-proposal-function-sent": "7.5.0",
"@babel/plugin-proposal-json-strings": "^7.0.0",
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
"@babel/plugin-proposal-json-strings": "^7.2.0",
"@babel/plugin-proposal-logical-assignment-operators": "^7.2.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
"@babel/plugin-proposal-numeric-separator": "^7.2.0",
"@babel/plugin-proposal-optional-chaining": "7.6.0",
"@babel/plugin-proposal-pipeline-operator": "7.5.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/plugin-transform-member-expression-literals": "^7.2.0",
"@babel/plugin-transform-property-literals": "^7.2.0",
"@babel/polyfill": "7.6.0",
"@babel/preset-env": "7.6.2",
"@babel/preset-flow": "^7.0.0-beta.40",
"@babel/preset-react": "^7.0.0-beta.40",
"@sambego/storybook-state": "^1.0.7",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@sambego/storybook-state": "^1.3.6",
"@storybook/addon-actions": "5.2.1",
"@storybook/addon-knobs": "5.2.1",
"@storybook/addon-links": "5.2.1",
@ -99,13 +102,13 @@
"babel-eslint": "10.0.3",
"babel-jest": "24.9.0",
"babel-loader": "8.0.6",
"babel-plugin-dynamic-import-node": "^2.2.0",
"babel-plugin-dynamic-import-node": "^2.3.0",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.22.0",
"classnames": "^2.2.5",
"classnames": "^2.2.6",
"css-loader": "3.2.0",
"detect-port": "^1.2.2",
"eslint": "5.16.0",
"detect-port": "^1.3.0",
"eslint": "6.4.0",
"eslint-config-airbnb": "18.0.1",
"eslint-plugin-flowtype": "4.3.0",
"eslint-plugin-import": "2.18.2",
@ -118,7 +121,7 @@
"flow-bin": "0.108.0",
"fs-extra": "8.1.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.0.4",
"html-webpack-plugin": "^3.2.0",
"jest": "24.9.0",
"jest-dom": "4.0.0",
"json-loader": "^0.5.7",
@ -130,7 +133,7 @@
"prettier-eslint-cli": "5.0.0",
"run-with-testrpc": "0.3.1",
"storybook-host": "5.1.0",
"storybook-router": "^0.3.3",
"storybook-router": "^0.3.4",
"style-loader": "1.0.0",
"truffle": "5.0.38",
"truffle-contract": "4.0.31",

View File

@ -18,7 +18,7 @@ yarn add flow-type // recommended usage of -g flag
We use [yarn](https://yarnpkg.com) in our infrastacture, so we decided to go with yarn in the README
### Installing
### Installing and running
A step by step series of examples that tell you have to get a development env running
@ -30,8 +30,28 @@ ganache-cli -b 3
Start the project in the other one
```
yarn install
```
For Rinkeby:
```
yarn start
```
For Mainnet:
```
yarn start-mainnet
```
### Building
For Rinkeby:
```
yarn build
```
For Mainnet:
```
yarn build-mainnet
```
## Running the tests

View File

@ -2,11 +2,9 @@
import React from 'react'
import Tooltip from '@material-ui/core/Tooltip'
import { withStyles } from '@material-ui/core/styles'
import { connect } from 'react-redux'
import Img from '~/components/layout/Img'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import { xs } from '~/theme/variables'
import { networkSelector } from '~/logic/wallets/store/selectors'
import SearchIcon from './search.svg'
const styles = () => ({
@ -26,17 +24,16 @@ const styles = () => ({
type EtherscanBtnProps = {
type: 'tx' | 'address',
value: string,
currentNetwork: string,
classes: Object,
}
const EtherscanBtn = ({
type, value, currentNetwork, classes,
type, value, classes,
}: EtherscanBtnProps) => (
<Tooltip title="Show details on Etherscan" placement="top">
<a
className={classes.container}
href={getEtherScanLink(type, value, currentNetwork)}
href={getEtherScanLink(type, value)}
target="_blank"
rel="noopener noreferrer"
aria-label="Show details on Etherscan"
@ -48,7 +45,4 @@ const EtherscanBtn = ({
const EtherscanBtnWithStyles = withStyles(styles)(EtherscanBtn)
export default connect<Object, Object, ?Function, ?Object>(
(state) => ({ currentNetwork: networkSelector(state) }),
null,
)(EtherscanBtnWithStyles)
export default EtherscanBtnWithStyles

View File

@ -1,11 +1,9 @@
// @flow
import React from 'react'
import { connect } from 'react-redux'
import OpenInNew from '@material-ui/icons/OpenInNew'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
import { secondary } from '~/theme/variables'
import { networkSelector } from '~/logic/wallets/store/selectors'
const openIconStyle = {
height: '13px',
@ -15,17 +13,13 @@ const openIconStyle = {
type EtherscanLinkProps = {
type: 'tx' | 'address',
value: string,
currentNetwork: string,
}
const EtherscanLink = ({ type, value, currentNetwork }: EtherscanLinkProps) => (
<a href={getEtherScanLink(type, value, currentNetwork)} target="_blank" rel="noopener noreferrer">
const EtherscanLink = ({ type, value }: EtherscanLinkProps) => (
<a href={getEtherScanLink(type, value)} target="_blank" rel="noopener noreferrer">
{shortVersionOf(value, 4)}
<OpenInNew style={openIconStyle} />
</a>
)
export default connect<Object, Object, ?Function, ?Object>(
(state) => ({ currentNetwork: networkSelector(state) }),
null,
)(EtherscanLink)
export default EtherscanLink

View File

@ -32,7 +32,8 @@ const styles = () => ({
padding: 0,
boxShadow: '0 0 10px 0 rgba(33, 48, 77, 0.1)',
minWidth: '280px',
left: '4px',
borderRadius: '8px',
marginTop: '11px',
},
summary: {
borderBottom: `solid 2px ${border}`,
@ -47,6 +48,9 @@ const styles = () => ({
flexBasis: '95px',
flexGrow: 0,
},
popper: {
zIndex: 2000,
},
})
const Layout = openHoc(({
@ -63,17 +67,18 @@ const Layout = openHoc(({
<SafeListHeader />
<Divider />
<Spacer />
<Divider />
<Provider open={open} toggle={toggle} info={providerInfo}>
{(providerRef) => (
<Popper open={open} anchorEl={providerRef.current} placement="bottom-end">
<Popper open={open} anchorEl={providerRef.current} placement="bottom" className={classes.popper}>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<ClickAwayListener onClickAway={clickAway} mouseEvent="onClick" touchEvent={false}>
<List className={classes.root} component="div">
{providerDetails}
</List>
</ClickAwayListener>
<>
<ClickAwayListener onClickAway={clickAway} mouseEvent="onClick" touchEvent={false}>
<List className={classes.root} component="div">
{providerDetails}
</List>
</ClickAwayListener>
</>
</Grow>
)}
</Popper>

View File

@ -5,6 +5,7 @@ import IconButton from '@material-ui/core/IconButton'
import ExpandLess from '@material-ui/icons/ExpandLess'
import ExpandMore from '@material-ui/icons/ExpandMore'
import Col from '~/components/layout/Col'
import Divider from '~/components/layout/Divider'
import { type Open } from '~/components/hoc/OpenHoc'
import { sm, md } from '~/theme/variables'
@ -20,7 +21,8 @@ const styles = () => ({
height: '100%',
display: 'flex',
alignItems: 'center',
flexBasis: '250px',
flexBasis: '284px',
marginRight: '20px',
},
provider: {
padding: `${sm} ${md}`,
@ -54,12 +56,14 @@ class Provider extends React.Component<Props> {
return (
<>
<div ref={this.myRef} className={classes.root}>
<Divider />
<Col end="sm" middle="xs" className={classes.provider} onClick={toggle}>
{info}
<IconButton disableRipple className={classes.expand}>
{open ? <ExpandLess /> : <ExpandMore />}
</IconButton>
</Col>
<Divider />
</div>
{children(this.myRef)}
</>

View File

@ -123,7 +123,7 @@ const UserDetails = ({
{address}
</Paragraph>
{userAddress && (
<Link className={classes.open} to={getEtherScanLink('address', userAddress, network)} target="_blank">
<Link className={classes.open} to={getEtherScanLink('address', userAddress)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
)}
@ -143,7 +143,7 @@ const UserDetails = ({
<Hairline margin="xs" />
<Row className={classes.details}>
<Paragraph noMargin align="right" className={classes.labels}>
Client
Wallet
</Paragraph>
<Spacer />
{provider === 'safe'

View File

@ -1,11 +1,11 @@
// @flow
import * as React from 'react'
import { connect } from 'react-redux'
import { withSnackbar } from 'notistack'
import { logComponentStack, type Info } from '~/utils/logBoundaries'
import { SharedSnackbarConsumer, type Variant } from '~/components/SharedSnackBar'
import { WALLET_ERROR_MSG } from '~/logic/wallets/store/actions'
import { getProviderInfo } from '~/logic/wallets/getWeb3'
import type { ProviderProps } from '~/logic/wallets/store/model/provider'
import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications'
import ProviderAccesible from './component/ProviderInfo/ProviderAccesible'
import UserDetails from './component/ProviderDetails/UserDetails'
import ProviderDisconnected from './component/ProviderInfo/ProviderDisconnected'
@ -16,7 +16,7 @@ import selector, { type SelectorProps } from './selector'
type Props = Actions &
SelectorProps & {
openSnackbar: (message: string, variant: Variant) => void,
enqueueSnackbar: Function,
}
type State = {
@ -39,31 +39,33 @@ class HeaderComponent extends React.PureComponent<Props, State> {
}
componentDidCatch(error: Error, info: Info) {
const { openSnackbar } = this.props
const { enqueueSnackbar, closeSnackbar } = this.props
this.setState({ hasError: true })
openSnackbar(WALLET_ERROR_MSG, 'error')
showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar)
logComponentStack(error, info)
}
onDisconnect = () => {
const { removeProvider, openSnackbar } = this.props
const { removeProvider, enqueueSnackbar, closeSnackbar } = this.props
clearInterval(this.providerListener)
removeProvider(openSnackbar)
removeProvider(enqueueSnackbar, closeSnackbar)
}
onConnect = async () => {
const { fetchProvider, openSnackbar } = this.props
const { fetchProvider, enqueueSnackbar, closeSnackbar } = this.props
clearInterval(this.providerListener)
let currentProvider: ProviderProps = await getProviderInfo()
fetchProvider(currentProvider, openSnackbar)
fetchProvider(currentProvider, enqueueSnackbar, closeSnackbar)
this.providerListener = setInterval(async () => {
const newProvider: ProviderProps = await getProviderInfo()
if (JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) {
fetchProvider(newProvider, openSnackbar)
if (currentProvider && JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) {
fetchProvider(newProvider, enqueueSnackbar, closeSnackbar)
}
currentProvider = newProvider
}, 2000)
@ -111,13 +113,7 @@ class HeaderComponent extends React.PureComponent<Props, State> {
}
}
const Header = connect(
export default connect(
selector,
actions,
)(HeaderComponent)
const HeaderSnack = () => (
<SharedSnackbarConsumer>{({ openSnackbar }) => <Header openSnackbar={openSnackbar} />}</SharedSnackbarConsumer>
)
export default HeaderSnack
)(withSnackbar(HeaderComponent))

View File

@ -0,0 +1,16 @@
// @flow
import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbar from '~/logic/notifications/store/actions/closeSnackbar'
import removeSnackbar from '~/logic/notifications/store/actions/removeSnackbar'
export type Actions = {
enqueueSnackbar: typeof enqueueSnackbar,
closeSnackbar: typeof closeSnackbar,
removeSnackbar: typeof removeSnackbar,
}
export default {
enqueueSnackbar,
closeSnackbar,
removeSnackbar,
}

View File

@ -0,0 +1,73 @@
// @flow
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withSnackbar } from 'notistack'
import actions from './actions'
import selector from './selector'
class Notifier extends Component {
displayed = []
shouldComponentUpdate({ notifications: newSnacks = [] }) {
const { notifications: currentSnacks, closeSnackbar, removeSnackbar } = this.props
if (!newSnacks.size) {
this.displayed = []
return false
}
let notExists = false
for (let i = 0; i < newSnacks.size; i += 1) {
const newSnack = newSnacks.get(i)
if (newSnack.dismissed) {
closeSnackbar(newSnack.key)
removeSnackbar(newSnack.key)
}
if (notExists) {
continue
}
notExists = notExists || !currentSnacks.filter(({ key }) => newSnack.key === key).length
}
return notExists
}
componentDidUpdate() {
const { notifications = [], enqueueSnackbar, removeSnackbar } = this.props
notifications.forEach((notification) => {
// Do nothing if snackbar is already displayed
if (this.displayed.includes(notification.key)) {
return
}
// Display snackbar using notistack
enqueueSnackbar(notification.message, {
...notification.options,
onClose: (event, reason, key) => {
if (notification.options.onClose) {
notification.options.onClose(event, reason, key)
}
// Dispatch action to remove snackbar from redux store
removeSnackbar(key)
},
})
// Keep track of snackbars that we've displayed
this.storeDisplayed(notification.key)
})
}
storeDisplayed = (id) => {
this.displayed = [...this.displayed, id]
}
render() {
return null
}
}
export default withSnackbar(
connect(
selector,
actions,
)(Notifier),
)

View File

@ -0,0 +1,7 @@
// @flow
import { createStructuredSelector } from 'reselect'
import { notificationsListSelector } from '~/logic/notifications/store/selectors'
export default createStructuredSelector<Object, *>({
notifications: notificationsListSelector,
})

View File

@ -1,105 +0,0 @@
// @flow
import * as React from 'react'
import { Snackbar } from '@material-ui/core'
import SnackbarContent from '~/components/SnackbarContent'
export const SharedSnackbar = () => (
<SharedSnackbarConsumer>
{(value) => {
const {
snackbarIsOpen, message, closeSnackbar, variant,
} = value
return (
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
open={snackbarIsOpen}
autoHideDuration={4000}
onClose={closeSnackbar}
>
<SnackbarContent onClose={closeSnackbar} message={message} variant={variant} />
</Snackbar>
)
}}
</SharedSnackbarConsumer>
)
type SnackbarContext = {
openSnackbar: Function,
closeSnackbar: Function,
snackbarIsOpen: boolean,
message: string,
variant: string,
}
const SharedSnackbarContext = React.createContext<SnackbarContext>({
openSnackbar: undefined,
closeSnackbar: undefined,
snackbarIsOpen: false,
message: '',
variant: 'info',
})
type Props = {
children: React.Node,
}
export type Variant = 'success' | 'error' | 'warning' | 'info'
type State = {
isOpen: boolean,
message: string,
variant: Variant,
}
export class SharedSnackbarProvider extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isOpen: false,
message: '',
variant: 'info',
}
}
openSnackbar = (message: string, variant: Variant) => {
this.setState({
message,
variant,
isOpen: true,
})
}
closeSnackbar = () => {
this.setState({
message: '',
isOpen: false,
})
}
render() {
const { children } = this.props
const { message, variant, isOpen } = this.state
return (
<SharedSnackbarContext.Provider
value={{
openSnackbar: this.openSnackbar,
closeSnackbar: this.closeSnackbar,
snackbarIsOpen: isOpen,
message,
variant,
}}
>
<SharedSnackbar />
{children}
</SharedSnackbarContext.Provider>
)
}
}
export const SharedSnackbarConsumer = SharedSnackbarContext.Consumer

View File

@ -1,110 +0,0 @@
// @flow
import SnackbarContent from '@material-ui/core/SnackbarContent'
import classNames from 'classnames/bind'
import * as React from 'react'
import CloseIcon from '@material-ui/icons/Close'
import CheckCircleIcon from '@material-ui/icons/CheckCircle'
import ErrorIcon from '@material-ui/icons/Error'
import InfoIcon from '@material-ui/icons/Info'
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import WarningIcon from '@material-ui/icons/Warning'
import { type WithStyles } from '~/theme/mui'
import {
secondary, warning, connected, error,
} from '~/theme/variables'
type Variant = 'success' | 'error' | 'warning' | 'info'
type MessageProps = WithStyles & {
variant: Variant,
message: string,
}
type Props = MessageProps & {
onClose?: () => void,
}
type CloseProps = WithStyles & {
onClose: () => void,
}
const variantIcon = {
success: CheckCircleIcon,
warning: WarningIcon,
error: ErrorIcon,
info: InfoIcon,
}
const styles = (theme) => ({
success: {
backgroundColor: '#ffffff',
},
successIcon: {
color: connected,
},
warning: {
backgroundColor: '#fff3e2',
},
warningIcon: {
color: warning,
},
error: {
backgroundColor: '#ffe6ea',
},
errorIcon: {
color: error,
},
info: {
backgroundColor: '#ffffff',
},
infoIcon: {
color: secondary,
},
icon: {
fontSize: 20,
},
iconVariant: {
opacity: 0.9,
marginRight: theme.spacing(1),
},
message: {
display: 'flex',
alignItems: 'center',
},
})
const Close = ({ classes, onClose }: CloseProps) => (
<IconButton key="close" aria-label="Close" color="inherit" className={classes.close} onClick={onClose}>
<CloseIcon className={classes.icon} />
</IconButton>
)
const Message = ({ classes, message, variant }: MessageProps) => {
const Icon = variantIcon[variant]
return (
<span id="client-snackbar" className={classes.message}>
<Icon className={classNames(classes.icon, classes.iconVariant, classes[`${variant}Icon`])} />
{message}
</span>
)
}
const GnoSnackbarContent = ({
variant, classes, message, onClose,
}: Props) => {
const action = onClose ? [<Close key="close" onClose={onClose} classes={classes} />] : undefined
const messageComponent = <Message classes={classes} message={message} variant={variant} />
return (
<SnackbarContent
className={classNames(classes[variant])}
aria-describedby="client-snackbar"
message={messageComponent}
action={action}
/>
)
}
export default withStyles(styles)(GnoSnackbarContent)

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="18" viewBox="0 0 22 18">
<g fill="none" fill-rule="nonzero">
<path fill="#FFC05F" d="M1.734 18h18.532a1 1 0 0 0 .865-1.501L11.865.495a1 1 0 0 0-1.73 0L.869 16.499A1 1 0 0 0 1.734 18z"/>
<path fill="#FFF" d="M12 12h-2V6h2zM12 15h-2v-2h2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="20" viewBox="0 0 22 20">
<g fill="none" fill-rule="evenodd">
<path fill="#008C73" d="M11 0C5.489 0 1 4.489 1 10s4.489 10 10 10 10-4.489 10-10S16.511 0 11 0z"/>
<path fill="#FFF" d="M10.124 13.75L6 9.406l1.245-1.312 2.88 3.034 4.63-4.878L16 7.561z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<g fill="none" fill-rule="evenodd">
<path fill="#F02525" fill-rule="nonzero" d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0z"/>
<path fill="#FFF" d="M11 15H9v-2h2zM11 11H9V5h2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@ -1,23 +1,85 @@
// @flow
import * as React from 'react'
import Header from '~/components/Header'
import { SnackbarProvider } from 'notistack'
import { withStyles } from '@material-ui/core/styles'
import SidebarProvider from '~/components/Sidebar'
import { SharedSnackbarProvider } from '~/components/SharedSnackBar'
import Header from '~/components/Header'
import Img from '~/components/layout/Img'
import Notifier from '~/components/Notifier'
import AlertLogo from './assets/alert.svg'
import CheckLogo from './assets/check.svg'
import ErrorLogo from './assets/error.svg'
import styles from './index.scss'
const notificationStyles = {
success: {
background: '#ffffff',
fontFamily: 'Averta',
fontSize: '14px',
lineHeight: 1.43,
color: '#001428',
minHeight: '58px',
boxShadow: '0 0 10px 0 rgba(212, 212, 211, 0.59)',
},
error: {
background: '#ffe6ea',
fontFamily: 'Averta',
fontSize: '14px',
lineHeight: 1.43,
color: '#001428',
minHeight: '58px',
boxShadow: '0 0 10px 0 rgba(212, 212, 211, 0.59)',
},
warning: {
background: '#fff3e2',
fontFamily: 'Averta',
fontSize: '14px',
lineHeight: 1.43,
color: '#001428',
minHeight: '58px',
boxShadow: '0 0 10px 0 rgba(212, 212, 211, 0.59)',
},
info: {
background: '#e8673c',
fontFamily: 'Averta',
fontSize: '14px',
lineHeight: 1.43,
color: '#ffffff',
minHeight: '58px',
boxShadow: '0 0 10px 0 rgba(212, 212, 211, 0.59)',
},
}
type Props = {
children: React.Node,
classes: Object,
}
const PageFrame = ({ children }: Props) => (
<SharedSnackbarProvider>
<div className={styles.frame}>
const PageFrame = ({ children, classes }: Props) => (
<div className={styles.frame}>
<SnackbarProvider
maxSnack={5}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
classes={{
variantSuccess: classes.success,
variantError: classes.error,
variantWarning: classes.warning,
variantInfo: classes.info,
}}
iconVariant={{
success: <Img src={CheckLogo} alt="Success" />,
error: <Img src={ErrorLogo} alt="Error" />,
warning: <Img src={AlertLogo} alt="Warning" />,
info: '',
}}
>
<Notifier />
<SidebarProvider>
<Header />
{children}
</SidebarProvider>
</div>
</SharedSnackbarProvider>
</SnackbarProvider>
</div>
)
export default PageFrame
export default withStyles(notificationStyles)(PageFrame)

View File

@ -0,0 +1,11 @@
// @flow
import devConfig from './development'
import { TX_SERVICE_HOST, RELAY_API_URL } from '~/config/names'
const devMainnetConfig = {
...devConfig,
[TX_SERVICE_HOST]: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1',
[RELAY_API_URL]: 'https://safe-relay.gnosis.io/api/v1/',
}
export default devMainnetConfig

View File

@ -1,9 +1,14 @@
// @flow
import { ensureOnce } from '~/utils/singleton'
import { ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
import devConfig from './development'
import testConfig from './testing'
import stagingConfig from './staging'
import prodConfig from './production'
import mainnetDevConfig from './development-mainnet'
import mainnetProdConfig from './production-mainnet'
import mainnetStagingConfig from './staging-mainnet'
const configuration = () => {
if (process.env.NODE_ENV === 'test') {
@ -11,12 +16,18 @@ const configuration = () => {
}
if (process.env.NODE_ENV === 'production') {
return prodConfig
if (process.env.REACT_APP_NETWORK === 'mainnet') {
return process.env.REACT_APP_ENV === 'production' ? mainnetProdConfig : mainnetStagingConfig
}
return process.env.REACT_APP_ENV === 'production' ? prodConfig : stagingConfig
}
return devConfig
return process.env.REACT_APP_NETWORK === 'mainnet' ? mainnetDevConfig : devConfig
}
export const getNetwork = () => (process.env.REACT_APP_NETWORK === 'mainnet' ? ETHEREUM_NETWORK.MAINNET : ETHEREUM_NETWORK.RINKEBY)
const getConfig = ensureOnce(configuration)
export const getTxServiceHost = () => {

View File

@ -0,0 +1,11 @@
// @flow
import prodConfig from './production'
import { TX_SERVICE_HOST, RELAY_API_URL } from '~/config/names'
const prodMainnetConfig = {
...prodConfig,
[TX_SERVICE_HOST]: 'https://safe-transaction.mainnet.gnosis.io/api/v1',
[RELAY_API_URL]: 'https://safe-relay.gnosis.io/api/v1/',
}
export default prodMainnetConfig

View File

@ -2,7 +2,7 @@
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
const prodConfig = {
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',
[TX_SERVICE_HOST]: 'https://safe-transaction.rinkeby.gnosis.io/api/v1/',
[SIGNATURES_VIA_METAMASK]: false,
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
}

View File

@ -0,0 +1,11 @@
// @flow
import stagingConfig from './staging'
import { TX_SERVICE_HOST, RELAY_API_URL } from '~/config/names'
const stagingMainnetConfig = {
...stagingConfig,
[TX_SERVICE_HOST]: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1',
[RELAY_API_URL]: 'https://safe-relay.staging.gnosis.io/api/v1/',
}
export default stagingMainnetConfig

10
src/config/staging.js Normal file
View File

@ -0,0 +1,10 @@
// @flow
import { TX_SERVICE_HOST, SIGNATURES_VIA_METAMASK, RELAY_API_URL } from '~/config/names'
const stagingConfig = {
[TX_SERVICE_HOST]: 'https://safe-transaction.staging.gnosisdev.com/api/v1/',
[SIGNATURES_VIA_METAMASK]: false,
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
}
export default stagingConfig

View File

@ -0,0 +1,3 @@
// @flow
export * from './notificationTypes'
export * from './notificationBuilder'

View File

@ -0,0 +1,159 @@
// @flow
import * as React from 'react'
import { IconButton } from '@material-ui/core'
import { Close as IconClose } from '@material-ui/icons'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { type Notification, NOTIFICATIONS } from './notificationTypes'
type NotificationsQueue = {
beforeExecution: Notification,
pendingExecution: {
noMoreConfirmationsNeeded: Notification,
moreConfirmationsNeeded: Notification,
},
afterExecution: Notification,
afterExecutionError: Notification,
afterRejection: Notification,
}
const standardTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: NOTIFICATIONS.TX_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
}
const confirmationTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG,
moreConfirmationsNeeded: null,
},
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: NOTIFICATIONS.TX_CONFIRMATION_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.TX_CONFIRMATION_FAILED_MSG,
}
const cancellationTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: NOTIFICATIONS.TX_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
}
const ownerChangeTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_OWNER_CHANGE_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.ONWER_CHANGE_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.ONWER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.ONWER_CHANGE_REJECTED_MSG,
afterExecution: NOTIFICATIONS.OWNER_CHANGE_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.ONWER_CHANGE_FAILED_MSG,
}
const safeNameChangeNotificationsQueue: NotificationsQueue = {
beforeExecution: null,
pendingExecution: {
noMoreConfirmationsNeeded: null,
moreConfirmationsNeeded: null,
},
afterRejection: null,
afterExecution: NOTIFICATIONS.SAFE_NAME_CHANGE_EXECUTED_MSG,
afterExecutionError: null,
}
const ownerNameChangeNotificationsQueue: NotificationsQueue = {
beforeExecution: null,
pendingExecution: {
noMoreConfirmationsNeeded: null,
moreConfirmationsNeeded: null,
},
afterRejection: null,
afterExecution: NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG,
afterExecutionError: null,
}
const thresholdChangeTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_THRESHOLD_CHANGE_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.THRESHOLD_CHANGE_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.THRESHOLD_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.THRESHOLD_CHANGE_REJECTED_MSG,
afterExecution: NOTIFICATIONS.THRESHOLD_CHANGE_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.THRESHOLD_CHANGE_FAILED_MSG,
}
const defaultNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.TX_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
afterExecution: NOTIFICATIONS.TX_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
}
export const getNofiticationsFromTxType = (txType: string) => {
let notificationsQueue: NotificationsQueue
switch (txType) {
case TX_NOTIFICATION_TYPES.STANDARD_TX: {
notificationsQueue = standardTxNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.CONFIRMATION_TX: {
notificationsQueue = confirmationTxNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.CANCELLATION_TX: {
notificationsQueue = cancellationTxNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.OWNER_CHANGE_TX: {
notificationsQueue = ownerChangeTxNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX: {
notificationsQueue = safeNameChangeNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.OWNER_NAME_CHANGE_TX: {
notificationsQueue = ownerNameChangeNotificationsQueue
break
}
case TX_NOTIFICATION_TYPES.THRESHOLD_CHANGE_TX: {
notificationsQueue = thresholdChangeTxNotificationsQueue
break
}
default: {
notificationsQueue = defaultNotificationsQueue
break
}
}
return notificationsQueue
}
export const showSnackbar = (
notification: Notification,
enqueueSnackbar: Function,
closeSnackbar: Function,
) => enqueueSnackbar(notification.message, {
...notification.options,
action: (key) => (
<IconButton onClick={() => closeSnackbar(key)}>
<IconClose />
</IconButton>
),
})

View File

@ -0,0 +1,222 @@
// @flow
import { getNetwork } from '~/config'
import { capitalize } from '~/utils/css'
export const SUCCESS = 'success'
export const ERROR = 'error'
export const WARNING = 'warning'
export const INFO = 'info'
const shortDuration = 5000
const longDuration = 10000
export type Variant = SUCCESS | ERROR | WARNING | INFO
export type Notification = {
message: string,
options: {
variant: Variant,
persist: boolean,
autoHideDuration?: shortDuration | longDuration,
preventDuplicate: boolean,
},
}
export type Notifications = {
// Wallet Connection
CONNECT_WALLET_MSG: Notification,
CONNECT_WALLET_READ_MODE_MSG: Notification,
WALLET_CONNECTED_MSG: Notification,
WALLET_DISCONNECTED_MSG: Notification,
UNLOCK_WALLET_MSG: Notification,
CONNECT_WALLET_ERROR_MSG: Notification,
// Regular/Custom Transactions
SIGN_TX_MSG: Notification,
TX_PENDING_MSG: Notification,
TX_PENDING_MORE_CONFIRMATIONS_MSG: Notification,
TX_REJECTED_MSG: Notification,
TX_EXECUTED_MSG: Notification,
TX_FAILED_MSG: Notification,
// Approval Transactions
TX_CONFIRMATION_PENDING_MSG: Notification,
TX_CONFIRMATION_EXECUTED_MSG: Notification,
TX_CONFIRMATION_FAILED_MSG: Notification,
// Safe Name
SAFE_NAME_CHANGED_MSG: Notification,
// Owner Name
OWNER_NAME_CHANGE_EXECUTED_MSG: Notification,
// Owners
SIGN_OWNER_CHANGE_MSG: Notification,
ONWER_CHANGE_PENDING_MSG: Notification,
ONWER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: Notification,
ONWER_CHANGE_REJECTED_MSG: Notification,
ONWER_CHANGE_EXECUTED_MSG: Notification,
ONWER_CHANGE_FAILED_MSG: Notification,
// Threshold
SIGN_THRESHOLD_CHANGE_MSG: Notification,
THRESHOLD_CHANGE_PENDING_MSG: Notification,
THRESHOLD_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: Notification,
THRESHOLD_CHANGE_REJECTED_MSG: Notification,
THRESHOLD_CHANGE_EXECUTED_MSG: Notification,
THRESHOLD_CHANGE_FAILED_MSG: Notification,
// Rinkeby version
RINKEBY_VERSION_MSG: Notification,
WRONG_NETWORK_MSG: Notification,
}
export const NOTIFICATIONS: Notifications = {
// Wallet Connection
CONNECT_WALLET_MSG: {
message: 'Please connect wallet to continue',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
CONNECT_WALLET_READ_MODE_MSG: {
message: 'You are in read-only mode: Please connect wallet',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
WALLET_CONNECTED_MSG: {
message: 'Wallet connected',
options: {
variant: SUCCESS,
persist: false,
autoHideDuration: shortDuration,
},
},
WALLET_DISCONNECTED_MSG: {
message: 'Wallet disconnected',
options: {
variant: SUCCESS,
persist: false,
autoHideDuration: shortDuration,
},
},
UNLOCK_WALLET_MSG: {
message: 'Unlock your wallet to connect',
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
CONNECT_WALLET_ERROR_MSG: {
message: 'Error connecting to your wallet',
options: { variant: ERROR, persist: true },
},
// Regular/Custom Transactions
SIGN_TX_MSG: {
message: 'Please sign the transaction',
options: { variant: SUCCESS, persist: true },
},
TX_PENDING_MSG: {
message: 'Transaction pending',
options: { variant: SUCCESS, persist: true },
},
TX_PENDING_MORE_CONFIRMATIONS_MSG: {
message: 'Transaction pending: More confirmations required to execute',
options: { variant: SUCCESS, persist: true },
},
TX_REJECTED_MSG: {
message: 'Transaction rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
TX_EXECUTED_MSG: {
message: 'Transaction successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
TX_FAILED_MSG: {
message: 'Transaction failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
// Approval Transactions
TX_CONFIRMATION_PENDING_MSG: {
message: 'Confirmation transaction pending',
options: { variant: SUCCESS, persist: true },
},
TX_CONFIRMATION_EXECUTED_MSG: {
message: 'Confirmation transaction succesful',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
TX_CONFIRMATION_FAILED_MSG: {
message: 'Confirmation transaction failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
// Safe Name
SAFE_NAME_CHANGE_EXECUTED_MSG: {
message: 'Safe name changed',
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
// Owner Name
OWNER_NAME_CHANGE_EXECUTED_MSG: {
message: 'Owner name changed',
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
// Owners
SIGN_OWNER_CHANGE_MSG: {
message: 'Please sign the owner change',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_PENDING_MSG: {
message: 'Owner change pending',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: {
message: 'Owner change pending: More confirmations required to execute',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_REJECTED_MSG: {
message: 'Owner change rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
OWNER_CHANGE_EXECUTED_MSG: {
message: 'Owner change successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
ONWER_CHANGE_FAILED_MSG: {
message: 'Owner change failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
// Threshold
SIGN_THRESHOLD_CHANGE_MSG: {
message: 'Please sign the required confirmations change',
options: { variant: SUCCESS, persist: true },
},
THRESHOLD_CHANGE_PENDING_MSG: {
message: 'Required confirmations change pending',
options: { variant: SUCCESS, persist: true },
},
THRESHOLD_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: {
message: 'Required confirmations change pending: More confirmations required to execute',
options: { variant: SUCCESS, persist: true },
},
THRESHOLD_CHANGE_REJECTED_MSG: {
message: 'Required confirmations change rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
THRESHOLD_CHANGE_EXECUTED_MSG: {
message: 'Required confirmations change successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
THRESHOLD_CHANGE_FAILED_MSG: {
message: 'Required confirmations change failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
// Network
RINKEBY_VERSION_MSG: {
message: "Rinkeby Version: Don't send Mainnet assets to this Safe",
options: { variant: INFO, persist: true, preventDuplicate: true },
},
WRONG_NETWORK_MSG: {
message: `Wrong network: Please use ${capitalize(getNetwork())}`,
options: { variant: WARNING, persist: true, preventDuplicate: true },
},
}

View File

@ -0,0 +1,8 @@
// @flow
import { createAction } from 'redux-actions'
export const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR'
const closeSnackbar = createAction<string, *>(CLOSE_SNACKBAR)
export default closeSnackbar

View File

@ -0,0 +1,22 @@
// @flow
import { createAction } from 'redux-actions'
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
import { type GlobalState } from '~/store'
import { type NotificationProps } from '~/logic/notifications/store/models/notification'
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
const addSnackbar = createAction<string, *>(ENQUEUE_SNACKBAR)
const enqueueSnackbar = (notification: NotificationProps) => (
dispatch: ReduxDispatch<GlobalState>,
getState: GetState<GlobalState>,
) => {
const newNotification = {
...notification,
key: new Date().getTime(),
}
dispatch(addSnackbar(newNotification))
}
export default enqueueSnackbar

View File

@ -0,0 +1,8 @@
// @flow
import { createAction } from 'redux-actions'
export const REMOVE_SNACKBAR = 'REMOVE_SNACKBAR'
const removeSnackbar = createAction<string, *>(REMOVE_SNACKBAR)
export default removeSnackbar

View File

@ -0,0 +1,19 @@
// @flow
import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
export type NotificationProps = {
key?: number,
message: string,
options: Object,
dismissed: boolean,
}
export const makeNotification: RecordFactory<NotificationProps> = Record({
key: 0,
message: '',
options: {},
dismissed: false,
})
export type Notification = RecordOf<NotificationProps>

View File

@ -0,0 +1,35 @@
// @flow
import { Map } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions'
import { makeNotification, type NotificationProps } from '~/logic/notifications/store/models/notification'
import { ENQUEUE_SNACKBAR } from '../actions/enqueueSnackbar'
import { CLOSE_SNACKBAR } from '../actions/closeSnackbar'
import { REMOVE_SNACKBAR } from '../actions/removeSnackbar'
export const NOTIFICATIONS_REDUCER_ID = 'notifications'
export type NotificationReducerState = Map<string, *>
export default handleActions<NotificationReducerState, *>(
{
[ENQUEUE_SNACKBAR]: (state: NotificationReducerState, action: ActionType<Function>): NotificationReducerState => {
const notification: NotificationProps = action.payload
return state.set(notification.key, makeNotification(notification))
},
[CLOSE_SNACKBAR]: (state: NotificationReducerState, action: ActionType<Function>): NotificationReducerState => {
const { notification }: { notification: NotificationProps } = action.payload
notification.dismissed = true
return state.update(notification.key, (prev) => prev.merge(notification))
},
[REMOVE_SNACKBAR]: (state: NotificationReducerState, action: ActionType<Function>): NotificationReducerState => {
const key = action.payload
return state.delete(key)
},
},
Map({
notifications: Map(),
}),
)

View File

@ -0,0 +1,15 @@
// @flow
import { List, Map } from 'immutable'
import { createSelector, type Selector } from 'reselect'
import { type GlobalState } from '~/store'
import { NOTIFICATIONS_REDUCER_ID } from '~/logic/notifications/store/reducer/notifications'
import { type Notification } from '~/logic/notifications/store/models/notification'
export const notificationsMapSelector = (
state: GlobalState,
): Map<string, Notification> => state[NOTIFICATIONS_REDUCER_ID]
export const notificationsListSelector: Selector<GlobalState, {}, List<Notification>> = createSelector(
notificationsMapSelector,
(notifications: Map<string, Notification>): List<Notification> => notifications.toList(),
)

View File

@ -82,8 +82,7 @@ export const generateTxGasEstimateFrom = async (
// Add 10k else we will fail in case of nested calls
return txGasEstimate.toNumber() + 10000
} catch (error) {
// eslint-disable-next-line
console.log('Error calculating tx gas estimation ' + error)
console.error('Error calculating tx gas estimation', error)
return 0
}
}
@ -128,8 +127,7 @@ export const calculateTxFee = async (
return estimate
} catch (error) {
// eslint-disable-next-line
console.log('Error calculating tx gas estimation ' + error)
console.error('Error calculating tx gas estimation', error)
return 0
}
}

View File

@ -1,7 +1,6 @@
// @flow
export * from './gas'
export * from './send'
export * from './safeBlockchainOperations'
export * from './safeTxSignerEIP712'
export * from './txHistory'
export * from './notifications'
export * from './notifiedTransactions'

View File

@ -1,14 +0,0 @@
// @flow
export type Notifications = {
BEFORE_EXECUTION_OR_CREATION: string,
AFTER_EXECUTION: string,
CREATED_MORE_CONFIRMATIONS_NEEDED: string,
ERROR: string,
}
export const DEFAULT_NOTIFICATIONS: Notifications = {
BEFORE_EXECUTION_OR_CREATION: 'Transaction in progress',
AFTER_EXECUTION: 'Transaction successfully executed',
CREATED_MORE_CONFIRMATIONS_NEEDED: 'Transaction in progress: More confirmations required to execute',
ERROR: 'Transaction failed',
}

View File

@ -0,0 +1,21 @@
// @flow
export type NotifiedTransaction = {
STANDARD_TX: string,
CONFIRMATION_TX: string,
CANCELLATION_TX: string,
OWNER_CHANGE_TX: string,
SAFE_NAME_CHANGE_TX: string,
OWNER_NAME_CHANGE_TX: string,
THRESHOLD_CHANGE_TX: string,
}
export const TX_NOTIFICATION_TYPES: NotifiedTransaction = {
STANDARD_TX: 'STANDARD_TX',
CONFIRMATION_TX: 'CONFIRMATION_TX',
CANCELLATION_TX: 'CANCELLATION_TX',
OWNER_CHANGE_TX: 'OWNER_CHANGE_TX',
SAFE_NAME_CHANGE_TX: 'SAFE_NAME_CHANGE_TX',
OWNER_NAME_CHANGE_TX: 'OWNER_NAME_CHANGE_TX',
THRESHOLD_CHANGE_TX: 'THRESHOLD_CHANGE_TX',
}

View File

@ -1,142 +0,0 @@
// // @flow
// import { List } from 'immutable'
// import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/wallets/ethTransactions'
// import { type Operation, saveTxToHistory } from '~/logic/safe/transactions'
// import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
// import { buildSignaturesFrom } from '~/logic/safe/safeTxSigner'
// import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/transactions'
// import { storeSignature, getSignaturesFrom } from '~/utils/storage/signatures'
// import { signaturesViaMetamask } from '~/config'
// export const approveTransaction = async (
// safeAddress: string,
// to: string,
// valueInWei: number,
// data: string,
// operation: Operation,
// nonce: number,
// sender: string,
// ) => {
// const gasPrice = await calculateGasPrice()
// if (signaturesViaMetamask()) {
// // return executeTransaction(safeAddress, to, valueInWei, data, operation, nonce, sender)
// const safe = await getGnosisSafeInstanceAt(safeAddress)
// const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
// const signature = await generateMetamaskSignature(
// safe,
// safeAddress,
// // sender
// to,
// valueInWei,
// nonce,
// data,
// operation,
// txGasEstimate,
// )
// storeSignature(safeAddress, nonce, signature)
// return undefined
// }
// const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
// const contractTxHash = await gnosisSafe.getTransactionHash(to, valueInWei, data, operation, 0, 0, 0, 0, 0, nonce)
// const approveData = gnosisSafe.contract.methods.approveHash(contractTxHash).encodeABI()
// const gas = await calculateGasOf(approveData, sender, safeAddress)
// const txReceipt = await gnosisSafe.approveHash(contractTxHash, { from: sender, gas, gasPrice })
// const txHash = txReceipt.tx
// await checkReceiptStatus(txHash)
// await saveTxToHistory(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'confirmation')
// return txHash
// }
// // export const executeTransaction = async (
// // safeAddress: string,
// // to: string,
// // valueInWei: number,
// // data: string,
// // operation: Operation,
// // nonce: number,
// // sender: string,
// // ownersWhoHasSigned: List<string>,
// // ) => {
// // const gasPrice = await calculateGasPrice()
// // if (signaturesViaMetamask()) {
// // const safe = await getSafeEthereumInstance(safeAddress)
// // const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
// // const signature = await generateMetamaskSignature(
// // safe,
// // safeAddress,
// // sender,
// // to,
// // valueInWei,
// // nonce,
// // data,
// // operation,
// // txGasEstimate,
// // )
// // storeSignature(safeAddress, nonce, signature)
// // const sigs = getSignaturesFrom(safeAddress, nonce)
// // const threshold = await safe.getThreshold()
// // const gas = await estimateDataGas(
// // safe,
// // to,
// // valueInWei,
// // data,
// // operation,
// // txGasEstimate,
// // 0,
// // nonce,
// // Number(threshold),
// // 0,
// // )
// // const numOwners = await safe.getOwners()
// // const gasIncludingRemovingStoreUpfront = gas + txGasEstimate + numOwners.length * 15000
// // const txReceipt = await safe.execTransaction(
// // to,
// // valueInWei,
// // data,
// // operation,
// // txGasEstimate,
// // 0, // dataGasEstimate
// // 0, // gasPrice
// // 0, // txGasToken
// // 0, // refundReceiver
// // sigs,
// // { from: sender, gas: gasIncludingRemovingStoreUpfront, gasPrice },
// // )
// // const txHash = txReceipt.tx
// // await checkReceiptStatus(txHash)
// // // await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
// // return txHash
// // }
// // const gnosisSafe = await getSafeEthereumInstance(safeAddress)
// // const signatures = buildSignaturesFrom(ownersWhoHasSigned, sender)
// // const txExecutionData = gnosisSafe.contract.methods
// // .execTransaction(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures)
// // .encodeABI()
// // const gas = await calculateGasOf(txExecutionData, sender, safeAddress)
// // const numOwners = await gnosisSafe.getOwners()
// // const gasIncludingRemovingStoreUpfront = gas + numOwners.length * 15000
// // const txReceipt = await gnosisSafe.execTransaction(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures, {
// // from: sender,
// // gas: gasIncludingRemovingStoreUpfront,
// // gasPrice,
// // })
// // const txHash = txReceipt.tx
// // await checkReceiptStatus(txHash)
// // await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
// // return txHash
// // }

View File

@ -1,14 +1,8 @@
// @flow
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
import { type Token } from '~/logic/tokens/store/model/token'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { type Operation } from '~/logic/safe/transactions'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
import { getErrorMessage } from '~/test/utils/ethereumErrors'
export const CALL = 0
export const TX_TYPE_EXECUTION = 'execution'
@ -44,13 +38,10 @@ export const getApprovalTransaction = async (
const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address)
return contract.methods.approveHash(txHash)
} catch (error) {
/* eslint-disable */
const executeData = safeInstance.contract.methods.approveHash(txHash).encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeData, sender)
console.log(`Error executing the TX: ${errMsg}`)
} catch (err) {
console.error(`Error while approving transaction: ${err}`)
throw error
throw err
}
}
@ -62,58 +53,16 @@ export const getExecutionTransaction = async (
operation: Operation,
nonce: string | number,
sender: string,
signatures?: string,
sigs: string,
) => {
let sigs = signatures
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
if (!sigs) {
sigs = `0x000000000000000000000000${sender.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}
try {
const web3 = getWeb3()
const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address)
return contract.methods.execTransaction(to, valueInWei, data, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
} catch (error) {
/* eslint-disable */
const executeDataUsedSignatures = safeInstance.contract.methods
.execTransaction(to, valueInWei, data, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, sender)
console.log(`Error executing the TX: ${errMsg}`)
} catch (err) {
console.error(`Error while creating transaction: ${err}`)
throw error
throw err
}
}
export const createTransaction = async (safeAddress: string, to: string, valueInEth: string, token: Token) => {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const web3 = getWeb3()
const from = web3.currentProvider.selectedAddress
const threshold = await safeInstance.getThreshold()
const nonce = (await safeInstance.nonce()).toString()
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
const isExecution = threshold.toNumber() === 1
let txData = EMPTY_DATA
if (!isEther(token.symbol)) {
const StandardToken = await getStandardTokenContract()
const sendToken = await StandardToken.at(token.address)
txData = sendToken.contract.transfer(to, valueInWei).encodeABI()
}
let txHash
if (isExecution) {
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
} else {
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
}
return txHash
}

View File

@ -22,8 +22,7 @@ export const saveSafes = async (safes: Object) => {
try {
await saveToStorage(SAFES_KEY, safes)
} catch (err) {
// eslint-disable-next-line
console.log('Error storing safe info in localstorage')
console.error('Error storing safe info in localstorage', err)
}
}
@ -32,8 +31,7 @@ export const setOwners = async (safeAddress: string, owners: List<Owner>) => {
const ownersAsMap = Map(owners.map((owner: Owner) => [owner.address.toLowerCase(), owner.name]))
await saveToStorage(`${OWNERS_KEY}-${safeAddress}`, ownersAsMap)
} catch (err) {
// eslint-disable-next-line
console.log('Error storing owners in localstorage')
console.error('Error storing owners in localstorage', err)
}
}

View File

@ -40,8 +40,7 @@ export const fetchTokens = () => async (dispatch: ReduxDispatch<GlobalState>) =>
dispatch(saveTokens(tokens))
} catch (err) {
// eslint-disable-next-line
console.log('Error fetching token list ' + err)
console.error('Error fetching token list', err)
return Promise.resolve()
}

View File

@ -14,8 +14,7 @@ export const saveActiveTokens = async (tokens: Map<string, Token>) => {
try {
await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS())
} catch (err) {
// eslint-disable-next-line
console.log('Error storing tokens in localstorage')
console.error('Error storing tokens in localstorage', err)
}
}
@ -38,8 +37,7 @@ export const removeTokenFromStorage = async (safeAddress: string, token: Token)
const index = data.indexOf(token)
await saveToStorage(CUSTOM_TOKENS_KEY, data.remove(index))
} catch (err) {
// eslint-disable-next-line
console.log('Error removing token in localstorage')
console.error('Error removing token in localstorage', err)
}
}

View File

@ -2,12 +2,14 @@
import Web3 from 'web3'
import ENS from 'ethereum-ens'
import type { ProviderProps } from '~/logic/wallets/store/model/provider'
import { getNetwork } from '~/config/index'
export const ETHEREUM_NETWORK = {
MAIN: 'MAIN',
MAINNET: 'MAINNET',
MORDEN: 'MORDEN',
ROPSTEN: 'ROPSTEN',
RINKEBY: 'RINKEBY',
GOERLI: 'GOERLI',
KOVAN: 'KOVAN',
UNKNOWN: 'UNKNOWN',
}
@ -22,7 +24,7 @@ export const WALLET_PROVIDER = {
export const ETHEREUM_NETWORK_IDS = {
// $FlowFixMe
1: ETHEREUM_NETWORK.MAIN,
1: ETHEREUM_NETWORK.MAINNET,
// $FlowFixMe
2: ETHEREUM_NETWORK.MORDEN,
// $FlowFixMe
@ -30,10 +32,17 @@ export const ETHEREUM_NETWORK_IDS = {
// $FlowFixMe
4: ETHEREUM_NETWORK.RINKEBY,
// $FlowFixMe
5: ETHEREUM_NETWORK.GOERLI,
// $FlowFixMe
42: ETHEREUM_NETWORK.KOVAN,
}
export const getEtherScanLink = (type: 'address' | 'tx', value: string, network: string) => `https://${network === 'mainnet' ? '' : `${network}.`}etherscan.io/${type}/${value}`
export const getEtherScanLink = (type: 'address' | 'tx', value: string) => {
const network = getNetwork()
return `https://${
network.toLowerCase() === 'mainnet' ? '' : `${network.toLowerCase()}.`
}etherscan.io/${type}/${value}`
}
let web3
export const getWeb3 = () => web3 || (window.web3 && new Web3(window.web3.currentProvider)) || (window.ethereum && new Web3(window.ethereum))
@ -76,7 +85,14 @@ export const getProviderInfo: Function = async (): Promise<ProviderProps> => {
if (window.ethereum) {
web3Provider = window.ethereum
await web3Provider.enable()
try {
const accounts = await web3Provider.enable()
if (!accounts) {
throw new Error()
}
} catch (error) {
console.error('Error when enabling web3 provider', error)
}
} else if (window.web3) {
web3Provider = window.web3.currentProvider
} else {
@ -114,6 +130,10 @@ export const getAddressFromENS = async (name: string) => {
}
export const getBalanceInEtherOf = async (safeAddress: string) => {
if (!web3) {
return '0'
}
const funds: String = await web3.eth.getBalance(safeAddress)
if (!funds) {

View File

@ -1,8 +1,10 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
import { getNetwork } from '~/config'
import type { ProviderProps } from '~/logic/wallets/store/model/provider'
import { makeProvider } from '~/logic/wallets/store/model/provider'
import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications'
import addProvider from './addProvider'
export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: ProviderProps) => {
@ -21,30 +23,41 @@ export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: Pr
dispatch(addProvider(walletRecord))
}
const SUCCESS_MSG = 'Wallet connected sucessfully'
const UNLOCK_MSG = 'Unlock your wallet to connect'
const WRONG_NETWORK = 'You are connected to wrong network. Please use RINKEBY'
export const WALLET_ERROR_MSG = 'Error connecting to your wallet'
const handleProviderNotification = (openSnackbar: Function, provider: ProviderProps) => {
const handleProviderNotification = (
provider: ProviderProps,
enqueueSnackbar: Function,
closeSnackbar: Function,
) => {
const { loaded, available, network } = provider
if (!loaded) {
openSnackbar(WALLET_ERROR_MSG, 'error')
showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar)
return
}
if (ETHEREUM_NETWORK_IDS[network] !== ETHEREUM_NETWORK.RINKEBY) {
openSnackbar(WRONG_NETWORK, 'error')
if (ETHEREUM_NETWORK_IDS[network] !== getNetwork()) {
showSnackbar(NOTIFICATIONS.WRONG_NETWORK_MSG, enqueueSnackbar, closeSnackbar)
return
}
if (ETHEREUM_NETWORK.RINKEBY === getNetwork()) {
showSnackbar(NOTIFICATIONS.RINKEBY_VERSION_MSG, enqueueSnackbar, closeSnackbar)
}
const msg = available ? SUCCESS_MSG : UNLOCK_MSG
const variant = available ? 'success' : 'warning'
openSnackbar(msg, variant)
if (available) {
// NOTE:
// if you want to be able to dispatch a `closeSnackbar` action later on,
// you SHOULD pass your own `key` in the options. `key` can be any sequence
// of number or characters, but it has to be unique to a given snackbar.
showSnackbar(NOTIFICATIONS.WALLET_CONNECTED_MSG, enqueueSnackbar, closeSnackbar)
} else {
showSnackbar(NOTIFICATIONS.UNLOCK_WALLET_MSG, enqueueSnackbar, closeSnackbar)
}
}
export default (provider: ProviderProps, openSnackbar: Function) => (dispatch: ReduxDispatch<*>) => {
handleProviderNotification(openSnackbar, provider)
export default (provider: ProviderProps, enqueueSnackbar: Function, closeSnackbar: Function) => (
dispatch: ReduxDispatch<*>,
) => {
handleProviderNotification(provider, enqueueSnackbar, closeSnackbar)
processProviderResponse(dispatch, provider)
}

View File

@ -1,9 +1,10 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { makeProvider, type ProviderProps, type Provider } from '~/logic/wallets/store/model/provider'
import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications'
import addProvider from './addProvider'
export default (openSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => {
export default (enqueueSnackbar: Function, closeSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => {
const providerProps: ProviderProps = {
name: '',
available: false,
@ -13,7 +14,7 @@ export default (openSnackbar: Function) => async (dispatch: ReduxDispatch<*>) =>
}
const provider: Provider = makeProvider(providerProps)
openSnackbar('Wallet disconnected succesfully', 'info')
showSnackbar(NOTIFICATIONS.WALLET_DISCONNECTED_MSG, enqueueSnackbar, closeSnackbar)
dispatch(addProvider(provider))
}

View File

@ -50,8 +50,7 @@ class Load extends React.Component<Props> {
const url = `${SAFELIST_ADDRESS}/${safeAddress}`
history.push(url)
} catch (error) {
// eslint-disable-next-line
console.log('Error while loading the Safe' + error)
console.error('Error while loading the Safe', error)
}
}

View File

@ -39,7 +39,7 @@ const styles = {
}
const Opening = ({
classes, name = 'Safe creation process', tx, network,
classes, name = 'Safe creation process', tx,
}: Props) => (
<Page align="center">
<Paragraph color="primary" size="xxl" weight="bold" align="center">
@ -70,7 +70,7 @@ const Opening = ({
Follow progress on
{' '}
<a
href={getEtherScanLink('tx', tx, network)}
href={getEtherScanLink('tx', tx)}
target="_blank"
rel="noopener noreferrer"
className={classes.etherscan}

View File

@ -1,6 +1,7 @@
// @flow
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, Suspense } from 'react'
import { List } from 'immutable'
import CircularProgress from '@material-ui/core/CircularProgress'
import cn from 'classnames'
import { withStyles } from '@material-ui/core/styles'
import { type Token } from '~/logic/tokens/store/model/token'
@ -29,7 +30,7 @@ type Props = {
tokens: List<Token>,
selectedToken: string,
createTransaction: Function,
activeScreenType: ActiveScreen
activeScreenType: ActiveScreen,
}
type TxStateType =
@ -51,6 +52,14 @@ const styles = () => ({
},
})
const loaderStyle = {
height: '500px',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}
const Send = ({
onClose,
isOpen,
@ -90,11 +99,15 @@ const Send = ({
description="Send Tokens Form"
handleClose={onClose}
open={isOpen}
paperClassName={cn(
scalableModalSize ? classes.scalableStaticModalWindow : classes.scalableModalWindow,
)}
paperClassName={cn(scalableModalSize ? classes.scalableStaticModalWindow : classes.scalableModalWindow)}
>
<>
<Suspense
fallback={(
<div style={loaderStyle}>
<CircularProgress size={40} />
</div>
)}
>
{activeScreen === 'chooseTxType' && <ChooseTxType onClose={onClose} setActiveScreen={setActiveScreen} />}
{activeScreen === 'sendFunds' && (
<SendFunds
@ -144,7 +157,7 @@ const Send = ({
createTransaction={createTransaction}
/>
)}
</>
</Suspense>
</Modal>
)
}

View File

@ -4,7 +4,7 @@ import OpenInNew from '@material-ui/icons/OpenInNew'
import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import IconButton from '@material-ui/core/IconButton'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { withSnackbar } from 'notistack'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link'
@ -13,11 +13,12 @@ import Button from '~/components/layout/Button'
import Img from '~/components/layout/Img'
import Block from '~/components/layout/Block'
import Identicon from '~/components/Identicon'
import { copyToClipboard } from '~/utils/clipboard'
import Hairline from '~/components/layout/Hairline'
import { copyToClipboard } from '~/utils/clipboard'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
import ArrowDown from '../assets/arrow-down.svg'
import { secondary } from '~/theme/variables'
@ -33,6 +34,8 @@ type Props = {
ethBalance: string,
tx: Object,
createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
const openIconStyle = {
@ -50,110 +53,117 @@ const ReviewCustomTx = ({
ethBalance,
tx,
createTransaction,
}: Props) => (
<SharedSnackbarConsumer>
{({ openSnackbar }) => {
const submitTx = async () => {
const web3 = getWeb3()
const txRecipient = tx.recipientAddress
const txData = tx.data
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const submitTx = async () => {
const web3 = getWeb3()
const txRecipient = tx.recipientAddress
const txData = tx.data
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0
createTransaction(safeAddress, txRecipient, txValue, txData, openSnackbar)
onClose()
}
createTransaction(
safeAddress,
txRecipient,
txValue,
txData,
TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar,
closeSnackbar,
)
onClose()
}
return (
<>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
Send Funds
return (
<>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
Send Funds
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<SafeInfo
safeAddress={safeAddress}
etherScanLink={etherScanLink}
safeName={safeName}
ethBalance={ethBalance}
/>
<Row margin="md">
<Col xs={1}>
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
</Col>
<Col xs={11} center="xs" layout="column">
<Hairline />
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Recipient
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col xs={1}>
<Identicon address={tx.recipientAddress} diameter={32} />
</Col>
<Col xs={11} layout="column">
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
{tx.recipientAddress}
<Link to={etherScanLink} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<SafeInfo
safeAddress={safeAddress}
etherScanLink={etherScanLink}
safeName={safeName}
ethBalance={ethBalance}
/>
<Row margin="md">
<Col xs={1}>
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
</Col>
<Col xs={11} center="xs" layout="column">
<Hairline />
</Col>
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Value
</Paragraph>
</Row>
<Row margin="md" align="center">
<Img src={getEthAsToken().logoUri} height={28} alt="Ether" onError={setImageToPlaceholder} />
<Paragraph size="md" noMargin className={classes.value}>
{tx.value || 0}
{' ETH'}
</Paragraph>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Data (hex encoded)
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col className={classes.outerData}>
<Row size="md" className={classes.data}>
{tx.data}
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Recipient
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col xs={1}>
<Identicon address={tx.recipientAddress} diameter={32} />
</Col>
<Col xs={11} layout="column">
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
{tx.recipientAddress}
<Link to={etherScanLink} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Paragraph>
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Value
</Paragraph>
</Row>
<Row margin="md" align="center">
<Img src={getEthAsToken().logoUri} height={28} alt="Ether" onError={setImageToPlaceholder} />
<Paragraph size="md" noMargin className={classes.value}>
{tx.value || 0}
{' ETH'}
</Paragraph>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Data (hex encoded)
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col className={classes.outerData}>
<Row size="md" className={classes.data}>
{tx.data}
</Row>
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={() => setActiveScreen('sendCustomTx')}>
Back
</Button>
<Button
type="submit"
onClick={submitTx}
variant="contained"
minWidth={140}
color="primary"
data-testid="submit-tx-btn"
className={classes.submitButton}
>
Submit
</Button>
</Row>
</>
)
}}
</SharedSnackbarConsumer>
)
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={() => setActiveScreen('sendCustomTx')}>
Back
</Button>
<Button
type="submit"
onClick={submitTx}
variant="contained"
minWidth={140}
color="primary"
data-testid="submit-tx-btn"
className={classes.submitButton}
>
Submit
</Button>
</Row>
</>
)
}
export default withStyles(styles)(ReviewCustomTx)
export default withStyles(styles)(withSnackbar(ReviewCustomTx))

View File

@ -4,7 +4,7 @@ import OpenInNew from '@material-ui/icons/OpenInNew'
import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import IconButton from '@material-ui/core/IconButton'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { withSnackbar } from 'notistack'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import Link from '~/components/layout/Link'
@ -20,6 +20,7 @@ import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import ArrowDown from '../assets/arrow-down.svg'
import { secondary } from '~/theme/variables'
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
@ -35,6 +36,8 @@ type Props = {
ethBalance: string,
tx: Object,
createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
const openIconStyle = {
@ -52,111 +55,117 @@ const ReviewTx = ({
ethBalance,
tx,
createTransaction,
}: Props) => (
<SharedSnackbarConsumer>
{({ openSnackbar }) => {
const submitTx = async () => {
const web3 = getWeb3()
const isSendingETH = isEther(tx.token.symbol)
const txRecipient = isSendingETH ? tx.recipientAddress : tx.token.address
let txData = EMPTY_DATA
let txAmount = web3.utils.toWei(tx.amount, 'ether')
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const submitTx = async () => {
const web3 = getWeb3()
const isSendingETH = isEther(tx.token.symbol)
const txRecipient = isSendingETH ? tx.recipientAddress : tx.token.address
let txData = EMPTY_DATA
let txAmount = web3.utils.toWei(tx.amount, 'ether')
if (!isSendingETH) {
const StandardToken = await getStandardTokenContract()
const tokenInstance = await StandardToken.at(tx.token.address)
if (!isSendingETH) {
const StandardToken = await getStandardTokenContract()
const tokenInstance = await StandardToken.at(tx.token.address)
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI()
// txAmount should be 0 if we send tokens
// the real value is encoded in txData and will be used by the contract
// if txAmount > 0 it would send ETH from the safe
txAmount = 0
}
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI()
// txAmount should be 0 if we send tokens
// the real value is encoded in txData and will be used by the contract
// if txAmount > 0 it would send ETH from the safe
txAmount = 0
}
createTransaction(safeAddress, txRecipient, txAmount, txData, openSnackbar)
onClose()
}
createTransaction(
safeAddress,
txRecipient,
txAmount,
txData,
TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar,
closeSnackbar,
)
onClose()
}
return (
<>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
Send Funds
return (
<>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
Send Funds
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<SafeInfo
safeAddress={safeAddress}
etherScanLink={etherScanLink}
safeName={safeName}
ethBalance={ethBalance}
/>
<Row margin="md">
<Col xs={1}>
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
</Col>
<Col xs={11} center="xs" layout="column">
<Hairline />
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Recipient
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col xs={1}>
<Identicon address={tx.recipientAddress} diameter={32} />
</Col>
<Col xs={11} layout="column">
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
{tx.recipientAddress}
<Link to={etherScanLink} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Paragraph>
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<SafeInfo
safeAddress={safeAddress}
etherScanLink={etherScanLink}
safeName={safeName}
ethBalance={ethBalance}
/>
<Row margin="md">
<Col xs={1}>
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
</Col>
<Col xs={11} center="xs" layout="column">
<Hairline />
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Recipient
</Paragraph>
</Row>
<Row margin="md" align="center">
<Col xs={1}>
<Identicon address={tx.recipientAddress} diameter={32} />
</Col>
<Col xs={11} layout="column">
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
{tx.recipientAddress}
<Link to={etherScanLink} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Paragraph>
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Amount
</Paragraph>
</Row>
<Row margin="md" align="center">
<Img src={tx.token.logoUri} height={28} alt={tx.token.name} onError={setImageToPlaceholder} />
<Paragraph size="md" noMargin className={classes.amount}>
{tx.amount}
{' '}
{tx.token.symbol}
</Paragraph>
</Row>
</Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={() => setActiveScreen('sendFunds')}>
Back
</Button>
<Button
type="submit"
onClick={submitTx}
variant="contained"
minWidth={140}
color="primary"
data-testid="submit-tx-btn"
className={classes.submitButton}
>
Submit
</Button>
</Row>
</>
)
}}
</SharedSnackbarConsumer>
)
</Col>
</Row>
<Row margin="xs">
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
Amount
</Paragraph>
</Row>
<Row margin="md" align="center">
<Img src={tx.token.logoUri} height={28} alt={tx.token.name} onError={setImageToPlaceholder} />
<Paragraph size="md" noMargin className={classes.amount}>
{tx.amount}
{' '}
{tx.token.symbol}
</Paragraph>
</Row>
</Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} />
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={() => setActiveScreen('sendFunds')}>
Back
</Button>
<Button
type="submit"
onClick={submitTx}
variant="contained"
minWidth={140}
color="primary"
data-testid="submit-tx-btn"
className={classes.submitButton}
>
Submit
</Button>
</Row>
</>
)
}
export default withStyles(styles)(ReviewTx)
export default withStyles(styles)(withSnackbar(ReviewTx))

View File

@ -106,7 +106,7 @@ class Layout extends React.Component<Props, State> {
}
const { address, ethBalance, name } = safe
const etherScanLink = getEtherScanLink('address', address, network)
const etherScanLink = getEtherScanLink('address', address)
return (
<>
@ -182,6 +182,7 @@ class Layout extends React.Component<Props, State> {
fetchTransactions={fetchTransactions}
safeAddress={address}
userAddress={userAddress}
currentNetwork={network}
granted={granted}
createTransaction={createTransaction}
processTransaction={processTransaction}

View File

@ -1,17 +1,19 @@
// @flow
import React from 'react'
import { withStyles } from '@material-ui/core/styles'
import { withSnackbar } from 'notistack'
import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col'
import Field from '~/components/forms/Field'
import Heading from '~/components/layout/Heading'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { composeValidators, required, minMaxLength } from '~/components/forms/validator'
import TextField from '~/components/forms/TextField'
import GnoForm from '~/components/forms/GnoForm'
import Row from '~/components/layout/Row'
import Paragraph from '~/components/layout/Paragraph'
import Button from '~/components/layout/Button'
import { getNofiticationsFromTxType, showSnackbar } from '~/logic/notifications'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { styles } from './style'
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
@ -22,17 +24,20 @@ type Props = {
safeAddress: string,
safeName: string,
updateSafe: Function,
openSnackbar: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
const ChangeSafeName = (props: Props) => {
const {
classes, safeAddress, safeName, updateSafe, openSnackbar,
classes, safeAddress, safeName, updateSafe, enqueueSnackbar, closeSnackbar,
} = props
const handleSubmit = (values) => {
updateSafe({ address: safeAddress, name: values.safeName })
openSnackbar('Safe name changed', 'success')
const notification = getNofiticationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX)
showSnackbar(notification.afterExecution, enqueueSnackbar, closeSnackbar)
}
return (
@ -80,10 +85,4 @@ const ChangeSafeName = (props: Props) => {
)
}
const withSnackbar = (props) => (
<SharedSnackbarConsumer>
{({ openSnackbar }) => <ChangeSafeName {...props} openSnackbar={openSnackbar} />}
</SharedSnackbarConsumer>
)
export default withStyles(styles)(withSnackbar)
export default withStyles(styles)(withSnackbar(ChangeSafeName))

View File

@ -2,10 +2,11 @@
import React, { useState, useEffect } from 'react'
import { List } from 'immutable'
import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal'
import { type Owner } from '~/routes/safe/store/models/owner'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import OwnerForm from './screens/OwnerForm'
import ThresholdForm from './screens/ThresholdForm'
import ReviewAddOwner from './screens/Review'
@ -26,9 +27,10 @@ type Props = {
safeName: string,
owners: List<Owner>,
threshold: number,
network: string,
addSafeOwner: Function,
createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
type ActiveScreen = 'selectOwner' | 'selectThreshold' | 'reviewAddOwner'
@ -36,14 +38,23 @@ export const sendAddOwner = async (
values: Object,
safeAddress: string,
ownersOld: List<Owner>,
openSnackbar: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
createTransaction: Function,
addSafeOwner: Function,
) => {
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const txData = gnosisSafe.contract.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar)
const txHash = await createTransaction(
safeAddress,
safeAddress,
0,
txData,
TX_NOTIFICATION_TYPES.OWNER_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
)
if (txHash) {
addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress })
@ -58,9 +69,10 @@ const AddOwner = ({
safeName,
owners,
threshold,
network,
createTransaction,
addSafeOwner,
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('selectOwner')
const [values, setValues] = useState<Object>({})
@ -98,59 +110,49 @@ const AddOwner = ({
setActiveScreen('reviewAddOwner')
}
return (
<>
<SharedSnackbarConsumer>
{({ openSnackbar }) => {
const onAddOwner = async () => {
onClose()
try {
sendAddOwner(values, safeAddress, owners, openSnackbar, createTransaction, addSafeOwner)
} catch (error) {
// eslint-disable-next-line
console.log('Error while removing an owner ' + error)
}
}
const onAddOwner = async () => {
onClose()
try {
sendAddOwner(values, safeAddress, owners, enqueueSnackbar, closeSnackbar, createTransaction, addSafeOwner)
} catch (error) {
console.error('Error while removing an owner', error)
}
}
return (
<Modal
title="Add owner to Safe"
description="Add owner to Safe"
handleClose={onClose}
open={isOpen}
paperClassName={classes.biggerModalWindow}
>
<>
{activeScreen === 'selectOwner' && (
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} owners={owners} />
)}
{activeScreen === 'selectThreshold' && (
<ThresholdForm
onClose={onClose}
owners={owners}
threshold={threshold}
onClickBack={onClickBack}
onSubmit={thresholdSubmitted}
/>
)}
{activeScreen === 'reviewAddOwner' && (
<ReviewAddOwner
onClose={onClose}
safeName={safeName}
owners={owners}
network={network}
values={values}
onClickBack={onClickBack}
onSubmit={onAddOwner}
/>
)}
</>
</Modal>
)
}}
</SharedSnackbarConsumer>
</>
return (
<Modal
title="Add owner to Safe"
description="Add owner to Safe"
handleClose={onClose}
open={isOpen}
paperClassName={classes.biggerModalWindow}
>
<>
{activeScreen === 'selectOwner' && (
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} owners={owners} />
)}
{activeScreen === 'selectThreshold' && (
<ThresholdForm
onClose={onClose}
owners={owners}
threshold={threshold}
onClickBack={onClickBack}
onSubmit={thresholdSubmitted}
/>
)}
{activeScreen === 'reviewAddOwner' && (
<ReviewAddOwner
onClose={onClose}
safeName={safeName}
owners={owners}
values={values}
onClickBack={onClickBack}
onSubmit={onAddOwner}
/>
)}
</>
</Modal>
)
}
export default withStyles(styles)(AddOwner)
export default withStyles(styles)(withSnackbar(AddOwner))

View File

@ -31,14 +31,13 @@ type Props = {
classes: Object,
safeName: string,
owners: List<Owner>,
network: string,
values: Object,
onClickBack: Function,
onSubmit: Function,
}
const ReviewAddOwner = ({
classes, onClose, safeName, owners, network, values, onClickBack, onSubmit,
classes, onClose, safeName, owners, values, onClickBack, onSubmit,
}: Props) => {
const handleSubmit = () => {
onSubmit()
@ -80,7 +79,6 @@ const ReviewAddOwner = ({
{values.threshold}
{' '}
out of
{' '}
{owners.size + 1}
{' '}
owner(s)
@ -112,7 +110,7 @@ const ReviewAddOwner = ({
<Paragraph size="md" color="disabled" noMargin>
{owner.address}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', owner.address, network)} target="_blank">
<Link className={classes.open} to={getEtherScanLink('address', owner.address)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Block>
@ -141,7 +139,11 @@ const ReviewAddOwner = ({
<Paragraph size="md" color="disabled" noMargin>
{values.ownerAddress}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', values.ownerAddress, network)} target="_blank">
<Link
className={classes.open}
to={getEtherScanLink('address', values.ownerAddress)}
target="_blank"
>
<OpenInNew style={openIconStyle} />
</Link>
</Block>

View File

@ -1,5 +1,6 @@
// @flow
import React from 'react'
import { withSnackbar } from 'notistack'
import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import OpenInNew from '@material-ui/icons/OpenInNew'
@ -14,8 +15,10 @@ import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import Paragraph from '~/components/layout/Paragraph'
import Identicon from '~/components/Identicon'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import { composeValidators, required, minMaxLength } from '~/components/forms/validator'
import { getNofiticationsFromTxType, showSnackbar } from '~/logic/notifications'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import Modal from '~/components/Modal'
import { styles } from './style'
import { secondary } from '~/theme/variables'
@ -34,9 +37,10 @@ type Props = {
isOpen: boolean,
safeAddress: string,
ownerAddress: string,
network: string,
selectedOwnerName: string,
editSafeOwner: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
const EditOwnerComponent = ({
@ -47,10 +51,15 @@ const EditOwnerComponent = ({
ownerAddress,
selectedOwnerName,
editSafeOwner,
network,
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const handleSubmit = (values) => {
editSafeOwner({ safeAddress, ownerAddress, ownerName: values.ownerName })
const notification = getNofiticationsFromTxType(TX_NOTIFICATION_TYPES.OWNER_NAME_CHANGE_TX)
showSnackbar(notification.afterExecution, enqueueSnackbar, closeSnackbar)
onClose()
}
@ -94,7 +103,7 @@ const EditOwnerComponent = ({
<Paragraph style={{ marginLeft: 10 }} size="md" color="disabled" noMargin>
{ownerAddress}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank">
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Block>
@ -116,6 +125,6 @@ const EditOwnerComponent = ({
)
}
const EditOwnerModal = withStyles(styles)(EditOwnerComponent)
const EditOwnerModal = withStyles(styles)(withSnackbar(EditOwnerComponent))
export default EditOwnerModal

View File

@ -2,10 +2,11 @@
import React, { useState, useEffect } from 'react'
import { List } from 'immutable'
import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal'
import { type Owner } from '~/routes/safe/store/models/owner'
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import CheckOwner from './screens/CheckOwner'
import ThresholdForm from './screens/ThresholdForm'
import ReviewRemoveOwner from './screens/Review'
@ -28,10 +29,12 @@ type Props = {
ownerName: string,
owners: List<Owner>,
threshold: number,
network: string,
createTransaction: Function,
removeSafeOwner: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
type ActiveScreen = 'checkOwner' | 'selectThreshold' | 'reviewRemoveOwner'
export const sendRemoveOwner = async (
@ -40,7 +43,8 @@ export const sendRemoveOwner = async (
ownerAddressToRemove: string,
ownerNameToRemove: string,
ownersOld: List<Owner>,
openSnackbar: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
createTransaction: Function,
removeSafeOwner: Function,
) => {
@ -54,7 +58,15 @@ export const sendRemoveOwner = async (
.removeOwner(prevAddress, ownerAddressToRemove, values.threshold)
.encodeABI()
const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar)
const txHash = await createTransaction(
safeAddress,
safeAddress,
0,
txData,
TX_NOTIFICATION_TYPES.OWNER_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
)
if (txHash) {
removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove })
@ -71,9 +83,10 @@ const RemoveOwner = ({
ownerName,
owners,
threshold,
network,
createTransaction,
removeSafeOwner,
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner')
const [values, setValues] = useState<Object>({})
@ -104,71 +117,62 @@ const RemoveOwner = ({
setActiveScreen('reviewRemoveOwner')
}
return (
<>
<SharedSnackbarConsumer>
{({ openSnackbar }) => {
const onRemoveOwner = () => {
onClose()
sendRemoveOwner(
values,
safeAddress,
ownerAddress,
ownerName,
owners,
openSnackbar,
createTransaction,
removeSafeOwner,
)
}
const onRemoveOwner = () => {
onClose()
sendRemoveOwner(
values,
safeAddress,
ownerAddress,
ownerName,
owners,
enqueueSnackbar,
closeSnackbar,
createTransaction,
removeSafeOwner,
)
}
return (
<Modal
title="Remove owner from Safe"
description="Remove owner from Safe"
handleClose={onClose}
open={isOpen}
paperClassName={classes.biggerModalWindow}
>
<>
{activeScreen === 'checkOwner' && (
<CheckOwner
onClose={onClose}
ownerAddress={ownerAddress}
ownerName={ownerName}
network={network}
onSubmit={ownerSubmitted}
/>
)}
{activeScreen === 'selectThreshold' && (
<ThresholdForm
onClose={onClose}
owners={owners}
threshold={threshold}
onClickBack={onClickBack}
onSubmit={thresholdSubmitted}
/>
)}
{activeScreen === 'reviewRemoveOwner' && (
<ReviewRemoveOwner
onClose={onClose}
safeName={safeName}
owners={owners}
network={network}
values={values}
ownerAddress={ownerAddress}
ownerName={ownerName}
onClickBack={onClickBack}
onSubmit={onRemoveOwner}
/>
)}
</>
</Modal>
)
}}
</SharedSnackbarConsumer>
</>
return (
<Modal
title="Remove owner from Safe"
description="Remove owner from Safe"
handleClose={onClose}
open={isOpen}
paperClassName={classes.biggerModalWindow}
>
<>
{activeScreen === 'checkOwner' && (
<CheckOwner
onClose={onClose}
ownerAddress={ownerAddress}
ownerName={ownerName}
onSubmit={ownerSubmitted}
/>
)}
{activeScreen === 'selectThreshold' && (
<ThresholdForm
onClose={onClose}
owners={owners}
threshold={threshold}
onClickBack={onClickBack}
onSubmit={thresholdSubmitted}
/>
)}
{activeScreen === 'reviewRemoveOwner' && (
<ReviewRemoveOwner
onClose={onClose}
safeName={safeName}
owners={owners}
values={values}
ownerAddress={ownerAddress}
ownerName={ownerName}
onClickBack={onClickBack}
onSubmit={onRemoveOwner}
/>
)}
</>
</Modal>
)
}
export default withStyles(styles)(RemoveOwner)
export default withStyles(styles)(withSnackbar(RemoveOwner))

View File

@ -29,12 +29,11 @@ type Props = {
classes: Object,
ownerAddress: string,
ownerName: string,
network: string,
onSubmit: Function,
}
const CheckOwner = ({
classes, onClose, ownerAddress, ownerName, network, onSubmit,
classes, onClose, ownerAddress, ownerName, onSubmit,
}: Props) => {
const handleSubmit = (values) => {
onSubmit(values)
@ -69,7 +68,7 @@ const CheckOwner = ({
<Paragraph size="md" color="disabled" noMargin>
{ownerAddress}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank">
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Block>

View File

@ -31,7 +31,6 @@ type Props = {
classes: Object,
safeName: string,
owners: List<Owner>,
network: string,
values: Object,
ownerAddress: string,
ownerName: string,
@ -44,7 +43,6 @@ const ReviewRemoveOwner = ({
onClose,
safeName,
owners,
network,
values,
ownerAddress,
ownerName,
@ -91,10 +89,10 @@ const ReviewRemoveOwner = ({
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
{values.threshold}
{' '}
out of
out of
{owners.size - 1}
{' '}
owner(s)
owner(s)
</Paragraph>
</Block>
</Block>
@ -104,7 +102,7 @@ owner(s)
<Paragraph size="lg" color="primary" noMargin>
{owners.size - 1}
{' '}
Safe owner(s)
Safe owner(s)
</Paragraph>
</Row>
<Hairline />
@ -126,7 +124,7 @@ Safe owner(s)
</Paragraph>
<Link
className={classes.open}
to={getEtherScanLink('address', owner.address, network)}
to={getEtherScanLink('address', owner.address)}
target="_blank"
>
<OpenInNew style={openIconStyle} />
@ -160,7 +158,7 @@ Safe owner(s)
</Paragraph>
<Link
className={classes.open}
to={getEtherScanLink('address', ownerAddress, network)}
to={getEtherScanLink('address', ownerAddress)}
target="_blank"
>
<OpenInNew style={openIconStyle} />

View File

@ -2,8 +2,10 @@
import React, { useState, useEffect } from 'react'
import { List } from 'immutable'
import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal'
import { type Owner } from '~/routes/safe/store/models/owner'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts'
import OwnerForm from './screens/OwnerForm'
import ReviewReplaceOwner from './screens/Review'
@ -25,10 +27,11 @@ type Props = {
ownerAddress: string,
ownerName: string,
owners: List<Owner>,
network: string,
threshold: string,
createTransaction: Function,
replaceSafeOwner: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
type ActiveScreen = 'checkOwner' | 'reviewReplaceOwner'
@ -36,7 +39,8 @@ export const sendReplaceOwner = async (
values: Object,
safeAddress: string,
ownerAddressToRemove: string,
openSnackbar: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
createTransaction: Function,
replaceSafeOwner: Function,
) => {
@ -50,7 +54,15 @@ export const sendReplaceOwner = async (
.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress)
.encodeABI()
const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar)
const txHash = await createTransaction(
safeAddress,
safeAddress,
0,
txData,
TX_NOTIFICATION_TYPES.OWNER_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
)
if (txHash) {
replaceSafeOwner({
@ -71,10 +83,11 @@ const ReplaceOwner = ({
ownerAddress,
ownerName,
owners,
network,
threshold,
createTransaction,
replaceSafeOwner,
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner')
const [values, setValues] = useState<Object>({})
@ -96,67 +109,57 @@ const ReplaceOwner = ({
setActiveScreen('reviewReplaceOwner')
}
return (
<>
<SharedSnackbarConsumer>
{({ openSnackbar }) => {
const onReplaceOwner = () => {
onClose()
try {
sendReplaceOwner(
values,
safeAddress,
ownerAddress,
openSnackbar,
createTransaction,
replaceSafeOwner,
)
} catch (error) {
// eslint-disable-next-line
console.log('Error while removing an owner ' + error)
}
}
const onReplaceOwner = () => {
onClose()
try {
sendReplaceOwner(
values,
safeAddress,
ownerAddress,
enqueueSnackbar,
closeSnackbar,
createTransaction,
replaceSafeOwner,
)
} catch (error) {
console.error('Error while removing an owner', error)
}
}
return (
<Modal
title="Replace owner from Safe"
description="Replace owner from Safe"
handleClose={onClose}
open={isOpen}
paperClassName={classes.biggerModalWindow}
>
<>
{activeScreen === 'checkOwner' && (
<OwnerForm
onClose={onClose}
ownerAddress={ownerAddress}
ownerName={ownerName}
owners={owners}
network={network}
onSubmit={ownerSubmitted}
/>
)}
{activeScreen === 'reviewReplaceOwner' && (
<ReviewReplaceOwner
onClose={onClose}
safeName={safeName}
owners={owners}
network={network}
values={values}
ownerAddress={ownerAddress}
ownerName={ownerName}
onClickBack={onClickBack}
onSubmit={onReplaceOwner}
threshold={threshold}
/>
)}
</>
</Modal>
)
}}
</SharedSnackbarConsumer>
</>
return (
<Modal
title="Replace owner from Safe"
description="Replace owner from Safe"
handleClose={onClose}
open={isOpen}
paperClassName={classes.biggerModalWindow}
>
<>
{activeScreen === 'checkOwner' && (
<OwnerForm
onClose={onClose}
ownerAddress={ownerAddress}
ownerName={ownerName}
owners={owners}
onSubmit={ownerSubmitted}
/>
)}
{activeScreen === 'reviewReplaceOwner' && (
<ReviewReplaceOwner
onClose={onClose}
safeName={safeName}
owners={owners}
values={values}
ownerAddress={ownerAddress}
ownerName={ownerName}
onClickBack={onClickBack}
onSubmit={onReplaceOwner}
threshold={threshold}
/>
)}
</>
</Modal>
)
}
export default withStyles(styles)(ReplaceOwner)
export default withStyles(styles)(withSnackbar(ReplaceOwner))

View File

@ -46,13 +46,12 @@ type Props = {
classes: Object,
ownerAddress: string,
ownerName: string,
network: string,
onSubmit: Function,
owners: List<Owner>,
}
const OwnerForm = ({
classes, onClose, ownerAddress, ownerName, network, onSubmit, owners,
classes, onClose, ownerAddress, ownerName, onSubmit, owners,
}: Props) => {
const handleSubmit = (values) => {
onSubmit(values)
@ -102,7 +101,7 @@ const OwnerForm = ({
</Paragraph>
<Link
className={classes.open}
to={getEtherScanLink('address', ownerAddress, network)}
to={getEtherScanLink('address', ownerAddress)}
target="_blank"
>
<OpenInNew style={openIconStyle} />

View File

@ -31,7 +31,6 @@ type Props = {
classes: Object,
safeName: string,
owners: List<Owner>,
network: string,
values: Object,
ownerAddress: string,
ownerName: string,
@ -45,7 +44,6 @@ const ReviewRemoveOwner = ({
onClose,
safeName,
owners,
network,
values,
ownerAddress,
ownerName,
@ -129,7 +127,7 @@ const ReviewRemoveOwner = ({
</Paragraph>
<Link
className={classes.open}
to={getEtherScanLink('address', owner.address, network)}
to={getEtherScanLink('address', owner.address)}
target="_blank"
>
<OpenInNew style={openIconStyle} />
@ -161,7 +159,7 @@ const ReviewRemoveOwner = ({
<Paragraph size="md" color="disabled" noMargin>
{ownerAddress}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank">
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Block>
@ -187,7 +185,7 @@ const ReviewRemoveOwner = ({
<Paragraph size="md" color="disabled" noMargin>
{values.ownerAddress}
</Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', values.ownerAddress, network)} target="_blank">
<Link className={classes.open} to={getEtherScanLink('address', values.ownerAddress)} target="_blank">
<OpenInNew style={openIconStyle} />
</Link>
</Block>

View File

@ -219,7 +219,6 @@ class ManageOwners extends React.Component<Props, State> {
safeName={safeName}
owners={owners}
threshold={threshold}
network={network}
userAddress={userAddress}
createTransaction={createTransaction}
addSafeOwner={addSafeOwner}
@ -233,7 +232,6 @@ class ManageOwners extends React.Component<Props, State> {
ownerName={selectedOwnerName}
owners={owners}
threshold={threshold}
network={network}
userAddress={userAddress}
createTransaction={createTransaction}
removeSafeOwner={removeSafeOwner}
@ -259,7 +257,6 @@ class ManageOwners extends React.Component<Props, State> {
ownerAddress={selectedOwnerAddress}
selectedOwnerName={selectedOwnerName}
owners={owners}
network={network}
editSafeOwner={editSafeOwner}
/>
</>

View File

@ -2,7 +2,7 @@
import React, { useState } from 'react'
import { withStyles } from '@material-ui/core/styles'
import { List } from 'immutable'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { withSnackbar } from 'notistack'
import Heading from '~/components/layout/Heading'
import Button from '~/components/layout/Button'
import Bold from '~/components/layout/Bold'
@ -10,10 +10,11 @@ import Block from '~/components/layout/Block'
import Row from '~/components/layout/Row'
import Modal from '~/components/Modal'
import Paragraph from '~/components/layout/Paragraph'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import ChangeThreshold from './ChangeThreshold'
import type { Owner } from '~/routes/safe/store/models/owner'
import { styles } from './style'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { styles } from './style'
type Props = {
owners: List<Owner>,
@ -22,10 +23,19 @@ type Props = {
createTransaction: Function,
safeAddress: string,
granted: boolean,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
const ThresholdSettings = ({
owners, threshold, classes, createTransaction, safeAddress, granted,
owners,
threshold,
classes,
createTransaction,
safeAddress,
granted,
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const [isModalOpen, setModalOpen] = useState(false)
@ -33,66 +43,66 @@ const ThresholdSettings = ({
setModalOpen((prevOpen) => !prevOpen)
}
const onChangeThreshold = async (newThreshold) => {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI()
createTransaction(
safeAddress,
safeAddress,
0,
txData,
TX_NOTIFICATION_TYPES.THRESHOLD_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
)
}
return (
<>
<SharedSnackbarConsumer>
{({ openSnackbar }) => {
const onChangeThreshold = async (newThreshold) => {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI()
createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar)
}
return (
<>
<Block className={classes.container}>
<Heading tag="h2">Required confirmations</Heading>
<Paragraph>
Any transaction requires the confirmation of:
</Paragraph>
<Paragraph size="lg" className={classes.ownersText}>
<Bold>{threshold}</Bold>
{' '}
out of
{' '}
<Bold>{owners.size}</Bold>
{' '}
owners
</Paragraph>
{owners.size > 1 && granted && (
<Row className={classes.buttonRow}>
<Button
color="primary"
minWidth={120}
className={classes.modifyBtn}
onClick={toggleModal}
variant="contained"
>
Modify
</Button>
</Row>
)}
</Block>
<Modal
title="Change Required Confirmations"
description="Change Required Confirmations Form"
handleClose={toggleModal}
open={isModalOpen}
>
<ChangeThreshold
onClose={toggleModal}
owners={owners}
threshold={threshold}
onChangeThreshold={onChangeThreshold}
/>
</Modal>
</>
)
}}
</SharedSnackbarConsumer>
<Block className={classes.container}>
<Heading tag="h2">Required confirmations</Heading>
<Paragraph>
Any transaction requires the confirmation of:
</Paragraph>
<Paragraph size="lg" className={classes.ownersText}>
<Bold>{threshold}</Bold>
{' '}
out of
{' '}
<Bold>{owners.size}</Bold>
{' '}
owners
</Paragraph>
{owners.size > 1 && granted && (
<Row className={classes.buttonRow}>
<Button
color="primary"
minWidth={120}
className={classes.modifyBtn}
onClick={toggleModal}
variant="contained"
>
Modify
</Button>
</Row>
)}
</Block>
<Modal
title="Change Required Confirmations"
description="Change Required Confirmations Form"
handleClose={toggleModal}
open={isModalOpen}
>
<ChangeThreshold
onClose={toggleModal}
owners={owners}
threshold={threshold}
onChangeThreshold={onChangeThreshold}
/>
</Modal>
</>
)
}
export default withStyles(styles)(ThresholdSettings)
export default withStyles(styles)(withSnackbar(ThresholdSettings))

View File

@ -5,7 +5,7 @@ import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import FormControlLabel from '@material-ui/core/FormControlLabel'
import Checkbox from '@material-ui/core/Checkbox'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal'
import Hairline from '~/components/layout/Hairline'
import Button from '~/components/layout/Button'
@ -13,6 +13,7 @@ import Row from '~/components/layout/Row'
import Bold from '~/components/layout/Bold'
import Block from '~/components/layout/Block'
import Paragraph from '~/components/layout/Paragraph'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { styles } from './style'
@ -29,6 +30,8 @@ type Props = {
threshold: number,
thresholdReached: boolean,
userAddress: string,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
const getModalTitleAndDescription = (thresholdReached: boolean) => {
@ -55,6 +58,8 @@ const ApproveTxModal = ({
threshold,
thresholdReached,
userAddress,
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const [approveAndExecute, setApproveAndExecute] = useState<boolean>(false)
const { title, description } = getModalTitleAndDescription(thresholdReached)
@ -62,68 +67,70 @@ const ApproveTxModal = ({
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
return (
<SharedSnackbarConsumer>
{({ openSnackbar }) => {
const approveTx = () => {
processTransaction(safeAddress, tx, openSnackbar, userAddress, approveAndExecute)
onClose()
}
const approveTx = () => {
processTransaction(
safeAddress,
tx,
userAddress,
TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
enqueueSnackbar,
closeSnackbar,
approveAndExecute,
)
onClose()
}
return (
<Modal title={title} description={description} handleClose={onClose} open={isOpen}>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
{title}
return (
<Modal title={title} description={description} handleClose={onClose} open={isOpen}>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
{title}
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<Row>
<Paragraph>{description}</Paragraph>
<Paragraph size="sm" color="medium">
Transaction nonce:
<br />
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
</Paragraph>
{!thresholdReached && oneConfirmationLeft && (
<>
<Paragraph color="error">
Approving transaction does not execute it immediately. If you want to approve and execute the
transaction right away, click on checkbox below.
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<Row>
<Paragraph>{description}</Paragraph>
<Paragraph size="sm" color="medium">
Transaction nonce:
<br />
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
</Paragraph>
{!thresholdReached && oneConfirmationLeft && (
<>
<Paragraph color="error">
Approving transaction does not execute it immediately. If you want to approve and execute the
transaction right away, click on checkbox below.
</Paragraph>
<FormControlLabel
control={<Checkbox onChange={handleExecuteCheckbox} checked={approveAndExecute} color="primary" />}
label="Execute transaction"
/>
</>
)}
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
variant="contained"
minWidth={214}
minHeight={42}
color="primary"
onClick={approveTx}
testId={APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID}
>
{title}
</Button>
</Row>
</Modal>
)
}}
</SharedSnackbarConsumer>
<FormControlLabel
control={<Checkbox onChange={handleExecuteCheckbox} checked={approveAndExecute} color="primary" />}
label="Execute transaction"
/>
</>
)}
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
variant="contained"
minWidth={214}
minHeight={42}
color="primary"
onClick={approveTx}
testId={APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID}
>
{title}
</Button>
</Row>
</Modal>
)
}
export default withStyles(styles)(ApproveTxModal)
export default withStyles(styles)(withSnackbar(ApproveTxModal))

View File

@ -3,7 +3,7 @@ import React from 'react'
import Close from '@material-ui/icons/Close'
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
import { withSnackbar } from 'notistack'
import Modal from '~/components/Modal'
import Hairline from '~/components/layout/Hairline'
import Button from '~/components/layout/Button'
@ -13,6 +13,7 @@ import Block from '~/components/layout/Block'
import Paragraph from '~/components/layout/Paragraph'
import { type Transaction } from '~/routes/safe/store/models/transaction'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { styles } from './style'
type Props = {
@ -22,67 +23,80 @@ type Props = {
createTransaction: Function,
tx: Transaction,
safeAddress: string,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
const CancelTxModal = ({
onClose, isOpen, classes, createTransaction, tx, safeAddress,
}: Props) => (
<SharedSnackbarConsumer>
{({ openSnackbar }) => {
const sendReplacementTransaction = () => {
createTransaction(safeAddress, safeAddress, 0, EMPTY_DATA, openSnackbar)
onClose()
}
onClose,
isOpen,
classes,
createTransaction,
tx,
safeAddress,
enqueueSnackbar,
closeSnackbar,
}: Props) => {
const sendReplacementTransaction = () => {
createTransaction(
safeAddress,
safeAddress,
0,
EMPTY_DATA,
TX_NOTIFICATION_TYPES.CANCELLATION_TX,
enqueueSnackbar,
closeSnackbar,
)
onClose()
}
return (
<Modal
title="Cancel Transaction"
description="Cancel Transaction"
handleClose={onClose}
open={isOpen}
// paperClassName={cn(smallerModalSize && classes.smallerModalWindow)}
return (
<Modal
title="Cancel Transaction"
description="Cancel Transaction"
handleClose={onClose}
open={isOpen}
// paperClassName={cn(smallerModalSize && classes.smallerModalWindow)}
>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
Cancel transaction
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<Row>
<Paragraph>
This action will cancel this transaction. A separate transaction will be performed to submit the
cancellation.
</Paragraph>
<Paragraph size="sm" color="medium">
Transaction nonce:
<br />
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
</Paragraph>
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
variant="contained"
minWidth={214}
minHeight={42}
color="secondary"
onClick={sendReplacementTransaction}
>
<Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.headingText} noMargin>
Cancel transaction
</Paragraph>
<IconButton onClick={onClose} disableRipple>
<Close className={classes.closeIcon} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<Row>
<Paragraph>
This action will cancel this transaction. A separate transaction will be performed to submit the
cancellation.
</Paragraph>
<Paragraph size="sm" color="medium">
Transaction nonce:
<br />
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
</Paragraph>
</Row>
</Block>
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} minHeight={42} onClick={onClose}>
Exit
</Button>
<Button
type="submit"
variant="contained"
minWidth={214}
minHeight={42}
color="secondary"
onClick={sendReplacementTransaction}
>
Cancel Transaction
</Button>
</Row>
</Modal>
)
}}
</SharedSnackbarConsumer>
)
Cancel Transaction
</Button>
</Row>
</Modal>
)
}
export default withStyles(styles)(CancelTxModal)
export default withStyles(styles)(withSnackbar(CancelTxModal))

View File

@ -41,7 +41,7 @@ const OwnerComponent = withStyles(styles)(({ owner, classes, isExecutor }: Owner
<ListItemText
primary={owner.name}
secondary={(
<a href={getEtherScanLink('address', owner.address, 'rinkeby')} target="_blank" rel="noopener noreferrer">
<a href={getEtherScanLink('address', owner.address)} target="_blank" rel="noopener noreferrer">
{shortVersionOf(owner.address, 4)}
{' '}
<OpenInNew style={openIconStyle} />

View File

@ -74,7 +74,7 @@ const ExpandedTx = ({
<Paragraph noMargin>
<Bold>TX hash: </Bold>
{tx.executionTxHash ? (
<a href={getEtherScanLink('tx', tx.executionTxHash, 'rinkeby')} target="_blank" rel="noopener noreferrer">
<a href={getEtherScanLink('tx', tx.executionTxHash)} target="_blank" rel="noopener noreferrer">
{shortVersionOf(tx.executionTxHash, 4)}
<OpenInNew style={openIconStyle} />
</a>

View File

@ -55,11 +55,11 @@ const TxsTable = ({
const [expandedTx, setExpandedTx] = useState<string | null>(null)
const handleTxExpand = (safeTxHash) => {
setExpandedTx(prevTx => (prevTx === safeTxHash ? null : safeTxHash))
setExpandedTx((prevTx) => (prevTx === safeTxHash ? null : safeTxHash))
}
const columns = generateColumns()
const autoColumns = columns.filter(c => !c.custom)
const autoColumns = columns.filter((c) => !c.custom)
const filteredData = getTxTableData(transactions)
return (
@ -124,8 +124,7 @@ const TxsTable = ({
</TableCell>
</TableRow>
</React.Fragment>
))
}
))}
</Table>
</Block>
)

View File

@ -16,6 +16,7 @@ type Props = {
granted: boolean,
createTransaction: Function,
processTransaction: Function,
currentNetwork: string,
}
const Transactions = ({
@ -28,6 +29,7 @@ const Transactions = ({
createTransaction,
processTransaction,
fetchTransactions,
currentNetwork,
}: Props) => {
useEffect(() => {
fetchTransactions(safeAddress)
@ -43,6 +45,7 @@ const Transactions = ({
threshold={threshold}
owners={owners}
userAddress={userAddress}
currentNetwork={currentNetwork}
granted={granted}
safeAddress={safeAddress}
createTransaction={createTransaction}

View File

@ -24,10 +24,7 @@ export const addSafe = createAction<string, Function, ActionReturn>(ADD_SAFE, (s
safe,
}))
const saveSafe = (safe: Safe) => (
dispatch: ReduxDispatch<GlobalState>,
getState: GetState<GlobalState>,
) => {
const saveSafe = (safe: Safe) => (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
const state = getState()
const safeList = safesListSelector(state)

View File

@ -2,4 +2,5 @@
import { createAction } from 'redux-actions'
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)

View File

@ -9,21 +9,29 @@ import {
getApprovalTransaction,
getExecutionTransaction,
CALL,
type Notifications,
DEFAULT_NOTIFICATIONS,
type NotifiedTransaction,
TX_TYPE_CONFIRMATION,
TX_TYPE_EXECUTION,
saveTxToHistory,
} from '~/logic/safe/transactions'
import {
type Notification,
type NotificationsQueue,
getNofiticationsFromTxType,
showSnackbar,
} from '~/logic/notifications'
import { getErrorMessage } from '~/test/utils/ethereumErrors'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
const createTransaction = (
safeAddress: string,
to: string,
valueInWei: string,
txData: string = EMPTY_DATA,
openSnackbar: Function,
notifiedTransaction: NotifiedTransaction,
enqueueSnackbar: Function,
closeSnackbar: Function,
shouldExecute?: boolean,
notifications?: Notifications = DEFAULT_NOTIFICATIONS,
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
const state: GlobalState = getState()
@ -33,19 +41,26 @@ const createTransaction = (
const nonce = (await safeInstance.nonce()).toString()
const isExecution = threshold.toNumber() === 1 || shouldExecute
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
const sigs = `0x000000000000000000000000${from.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
const notificationsQueue: NotificationsQueue = getNofiticationsFromTxType(notifiedTransaction)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
let pendingExecutionKey
let txHash
let tx
try {
if (isExecution) {
tx = await getExecutionTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
tx = await getExecutionTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from, sigs)
} else {
tx = await getApprovalTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
}
const sendParams = {
from,
}
const sendParams = { from }
// if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000'
@ -55,12 +70,21 @@ const createTransaction = (
.send(sendParams)
.once('transactionHash', (hash) => {
txHash = hash
openSnackbar(notifications.BEFORE_EXECUTION_OR_CREATION, 'success')
closeSnackbar(beforeExecutionKey)
const pendingExecutionNotification: Notification = isExecution ? {
message: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.message,
options: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.options,
} : {
message: notificationsQueue.pendingExecution.moreConfirmationsNeeded.message,
options: notificationsQueue.pendingExecution.moreConfirmationsNeeded.options,
}
pendingExecutionKey = showSnackbar(pendingExecutionNotification, enqueueSnackbar, closeSnackbar)
})
.on('error', (error) => {
console.error('Tx error: ', error)
})
.then(async (receipt) => {
closeSnackbar(pendingExecutionKey)
await saveTxToHistory(
safeInstance,
to,
@ -72,17 +96,22 @@ const createTransaction = (
from,
isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
)
if (isExecution) {
showSnackbar(notificationsQueue.afterExecution, enqueueSnackbar, closeSnackbar)
}
return receipt.transactionHash
})
openSnackbar(
isExecution ? notifications.AFTER_EXECUTION : notifications.CREATED_MORE_CONFIRMATIONS_NEEDED,
'success',
)
} catch (err) {
openSnackbar(notifications.ERROR, 'error')
console.error(`Error while creating transaction: ${err}`)
closeSnackbar(beforeExecutionKey)
closeSnackbar(pendingExecutionKey)
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
const executeDataUsedSignatures = safeInstance.contract.methods
.execTransaction(to, valueInWei, txData, CALL, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, from)
console.error(`Error executing the TX: ${errMsg}`)
}
dispatch(fetchTransactions(safeAddress))

View File

@ -14,7 +14,7 @@ import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { addTransactions } from './addTransactions'
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers'
import { TX_TYPE_EXECUTION } from '~/logic/safe/transactions/send'
import { TX_TYPE_EXECUTION } from '~/logic/safe/transactions'
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
let web3

View File

@ -13,6 +13,13 @@ import {
TX_TYPE_EXECUTION,
TX_TYPE_CONFIRMATION,
} from '~/logic/safe/transactions'
import {
type Notification,
type NotificationsQueue,
getNofiticationsFromTxType,
showSnackbar,
} from '~/logic/notifications'
import { getErrorMessage } from '~/test/utils/ethereumErrors'
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
// https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26
@ -38,8 +45,10 @@ const generateSignaturesFromTxConfirmations = (tx: Transaction, preApprovingOwne
const processTransaction = (
safeAddress: string,
tx: Transaction,
openSnackbar: Function,
userAddress: string,
notifiedTransaction: NotifiedTransaction,
enqueueSnackbar: Function,
closeSnackbar: Function,
approveAndExecute?: boolean,
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
const state: GlobalState = getState()
@ -49,54 +58,84 @@ const processTransaction = (
const nonce = (await safeInstance.nonce()).toString()
const threshold = (await safeInstance.getThreshold()).toNumber()
const shouldExecute = threshold === tx.confirmations.size || approveAndExecute
const sigs = generateSignaturesFromTxConfirmations(tx, approveAndExecute && userAddress)
let sigs = generateSignaturesFromTxConfirmations(tx, approveAndExecute && userAddress)
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
if (!sigs) {
sigs = `0x000000000000000000000000${from.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}
const notificationsQueue: NotificationsQueue = getNofiticationsFromTxType(notifiedTransaction)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
let pendingExecutionKey
let txHash
let transaction
if (shouldExecute) {
transaction = await getExecutionTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from, sigs)
} else {
transaction = await getApprovalTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from)
}
const sendParams = {
from,
}
// if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000'
}
await transaction
.send(sendParams)
.once('transactionHash', (hash) => {
txHash = hash
openSnackbar(
shouldExecute ? 'Transaction has been submitted' : 'Approval transaction has been submitted',
'success',
)
})
.on('error', (error) => {
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
await saveTxToHistory(
try {
if (shouldExecute) {
transaction = await getExecutionTransaction(
safeInstance,
tx.recipient,
tx.value,
tx.data,
CALL,
nonce,
receipt.transactionHash,
from,
shouldExecute ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
sigs,
)
} else {
transaction = await getApprovalTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from)
}
return receipt.transactionHash
})
const sendParams = { from }
// if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000'
}
openSnackbar(shouldExecute ? 'Transaction has been confirmed' : 'Approval transaction has been confirmed', 'success')
await transaction
.send(sendParams)
.once('transactionHash', (hash) => {
txHash = hash
closeSnackbar(beforeExecutionKey)
const notification: Notification = {
message: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.message,
options: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.options,
}
pendingExecutionKey = showSnackbar(notification, enqueueSnackbar, closeSnackbar)
})
.on('error', (error) => {
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
closeSnackbar(pendingExecutionKey)
await saveTxToHistory(
safeInstance,
tx.recipient,
tx.value,
tx.data,
CALL,
nonce,
receipt.transactionHash,
from,
shouldExecute ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
)
showSnackbar(notificationsQueue.afterExecution, enqueueSnackbar, closeSnackbar)
return receipt.transactionHash
})
} catch (err) {
closeSnackbar(beforeExecutionKey)
closeSnackbar(pendingExecutionKey)
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
const executeData = safeInstance.contract.methods.approveHash(txHash).encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeData, from)
console.error(`Error executing the TX: ${errMsg}`)
}
dispatch(fetchTransactions(safeAddress))

View File

@ -5,14 +5,18 @@ import {
combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store,
} from 'redux'
import thunk from 'redux-thunk'
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/logic/wallets/store/reducer/provider'
import safe, { SAFE_REDUCER_ID, type SafeReducerState as SafeState } from '~/routes/safe/store/reducer/safe'
import safeStorage from '~/routes/safe/store/middleware/safeStorage'
import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens'
import transactions, {
type State as TransactionsState,
TRANSACTIONS_REDUCER_ID,
} from '~/routes/safe/store/reducer/transactions'
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/logic/wallets/store/reducer/provider'
import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens'
import notifications, {
NOTIFICATIONS_REDUCER_ID,
type State as NotificationsState,
} from '~/logic/notifications/store/reducer/notifications'
export const history = createBrowserHistory()
@ -25,6 +29,7 @@ export type GlobalState = {
safes: SafeState,
tokens: TokensState,
transactions: TransactionsState,
notifications: NotificationsState,
}
export type GetState = () => GlobalState
@ -35,11 +40,13 @@ const reducers: Reducer<GlobalState> = combineReducers({
[SAFE_REDUCER_ID]: safe,
[TOKEN_REDUCER_ID]: tokens,
[TRANSACTIONS_REDUCER_ID]: transactions,
[NOTIFICATIONS_REDUCER_ID]: notifications,
})
export const store: Store<GlobalState> = createStore(
export const store: Store<GlobalState> = createStore(reducers, finalCreateStore)
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(
reducers,
localState,
finalCreateStore,
)
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(reducers, localState, finalCreateStore)

View File

@ -199,18 +199,27 @@ export default createMuiTheme({
},
},
},
MuiSnackbarContent: {
root: {
boxShadow: '0 0 10px 0 rgba(33, 48, 77, 0.1)',
borderRadius: '3px',
color: primary,
},
},
MuiSvgIcon: {
colorSecondary: {
color: secondaryText,
},
},
MuiSnackbar: {
root: {
width: '280px',
},
},
MuiSnackbarContent: {
message: {
maxWidth: '260px',
'& img': {
marginRight: '5px',
},
},
action: {
paddingLeft: 0,
},
},
MuiTab: {
root: {
fontFamily: 'Averta, monospace',

View File

@ -1,5 +1,6 @@
// @flow
import { ImmortalStorage, IndexedDbStore, LocalStorageStore } from 'immortal-db'
import { getNetwork } from '~/config'
// Don't use sessionStorage and cookieStorage
// https://github.com/gruns/ImmortalDB/issues/22
@ -7,7 +8,7 @@ import { ImmortalStorage, IndexedDbStore, LocalStorageStore } from 'immortal-db'
const stores = [IndexedDbStore, LocalStorageStore]
export const storage = new ImmortalStorage(stores)
const PREFIX = 'v1'
const PREFIX = `v1_${getNetwork()}`
export const loadFromStorage = async (key: string): Promise<*> => {
try {

View File

@ -15,8 +15,7 @@ export const storeSignature = async (safeAddress: string, nonce: number, signatu
const updatedSubjects = subjects.set(key, signatures)
await saveToStorage(signaturesKey, updatedSubjects)
} catch (err) {
// eslint-disable-next-line
console.log('Error storing signatures in localstorage')
console.error('Error storing signatures in localstorage', err)
}
}

View File

@ -12,8 +12,7 @@ export const storeSubject = async (safeAddress: string, nonce: number, subject:
const updatedSubjects = subjects.set(nonce, subject)
saveToStorage(key, updatedSubjects)
} catch (err) {
// eslint-disable-next-line
console.log('Error storing transaction subject in localstorage')
console.error('Error storing transaction subject in localstorage', err)
}
}

219
yarn.lock
View File

@ -418,7 +418,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-export-namespace-from" "^7.2.0"
"@babel/plugin-proposal-function-bind@^7.0.0":
"@babel/plugin-proposal-function-bind@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz#94dc2cdc505cafc4e225c0014335a01648056bf7"
integrity sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw==
@ -435,7 +435,7 @@
"@babel/helper-wrap-function" "^7.2.0"
"@babel/plugin-syntax-function-sent" "^7.2.0"
"@babel/plugin-proposal-json-strings@^7.0.0", "@babel/plugin-proposal-json-strings@^7.2.0":
"@babel/plugin-proposal-json-strings@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==
@ -443,7 +443,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-proposal-logical-assignment-operators@^7.0.0":
"@babel/plugin-proposal-logical-assignment-operators@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.2.0.tgz#8a5cea6c42a7c87446959e02fff5fad012c56f57"
integrity sha512-0w797xwdPXKk0m3Js74hDi0mCTZplIu93MOSfb1ZLd/XFe3abWypx1QknVk0J+ohnsjYpvjH4Gwfo2i3RicB6Q==
@ -459,7 +459,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.2.0"
"@babel/plugin-proposal-numeric-separator@^7.0.0":
"@babel/plugin-proposal-numeric-separator@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.2.0.tgz#646854daf4cd22fd6733f6076013a936310443ac"
integrity sha512-DohMOGDrZiMKS7LthjUZNNcWl8TAf5BZDwZAH4wpm55FuJTHgfqPGdibg7rZDmont/8Yg0zA03IgT6XLeP+4sg==
@ -515,7 +515,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-pipeline-operator" "^7.5.0"
"@babel/plugin-proposal-throw-expressions@^7.0.0":
"@babel/plugin-proposal-throw-expressions@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz#2d9e452d370f139000e51db65d0a85dc60c64739"
integrity sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw==
@ -562,7 +562,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-dynamic-import@7.2.0", "@babel/plugin-syntax-dynamic-import@^7.0.0", "@babel/plugin-syntax-dynamic-import@^7.2.0":
"@babel/plugin-syntax-dynamic-import@7.2.0", "@babel/plugin-syntax-dynamic-import@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==
@ -604,7 +604,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-import-meta@^7.0.0":
"@babel/plugin-syntax-import-meta@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz#2333ef4b875553a3bcd1e93f8ebc09f5b9213a40"
integrity sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA==
@ -1269,7 +1269,7 @@
js-levenshtein "^1.1.3"
semver "^5.5.0"
"@babel/preset-flow@^7.0.0", "@babel/preset-flow@^7.0.0-beta.40":
"@babel/preset-flow@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.0.0.tgz#afd764835d9535ec63d8c7d4caf1c06457263da2"
integrity sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ==
@ -1277,7 +1277,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-flow-strip-types" "^7.0.0"
"@babel/preset-react@7.0.0", "@babel/preset-react@^7.0.0", "@babel/preset-react@^7.0.0-beta.40":
"@babel/preset-react@7.0.0", "@babel/preset-react@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0"
integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==
@ -1913,7 +1913,7 @@
resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.0.2.tgz#1d94f02800b094753f9271c206a26c2a06ca14ee"
integrity sha512-8/qcMh15507AnXJ3lBeuhsdFwnWQqnp68EpUuHlYPixJ5vjVmls7/Jq48cnUlrZI8Jd9U1jkhfCl0gaT5KMgVw==
"@sambego/storybook-state@^1.0.7":
"@sambego/storybook-state@^1.3.6":
version "1.3.6"
resolved "https://registry.yarnpkg.com/@sambego/storybook-state/-/storybook-state-1.3.6.tgz#9a6511095d200b8ab2d6bc39def81c90312dd420"
integrity sha512-bTUE1ZTtI9ICyqz6l5gtUfo0/W77fPP7KOAd/HI1jM7m1Jxjxs1k1Qbcrqmxg1vaHemlXVkvxVCZf8BT9RzxGw==
@ -3044,6 +3044,11 @@ acorn-jsx@^5.0.0:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==
acorn-jsx@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f"
integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==
acorn-walk@^6.0.1, acorn-walk@^6.1.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
@ -3069,6 +3074,11 @@ acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.0, acorn@^6.2.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51"
integrity sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==
acorn@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a"
integrity sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==
address@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
@ -3922,7 +3932,7 @@ babel-plugin-dynamic-import-node@2.2.0:
dependencies:
object.assign "^4.1.0"
babel-plugin-dynamic-import-node@^2.2.0, babel-plugin-dynamic-import-node@^2.3.0:
babel-plugin-dynamic-import-node@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
@ -5519,7 +5529,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.5:
classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@ -6423,10 +6433,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.3.0.tgz#017eae725d0c46173b572da025fb5e4e534270fd"
integrity sha512-A8o+iXBVqQayl9Z39BHgb7m/zLOfhF7LK82t+n9Fq1adds1vaUn8ByVoADqWLe4OTc6BZYc/FdbdTwufNYqkJw==
date-fns@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.4.1.tgz#b53f9bb65ae6bd9239437035710e01cf383b625e"
integrity sha512-2RhmH/sjDSCYW2F3ZQxOUx/I7PvzXpi89aQL2d3OAxSTwLx6NilATeUbe0menFE3Lu5lFkOFci36ivimwYHHxw==
date-now@^0.1.4:
version "0.1.4"
@ -6745,7 +6755,7 @@ detect-port-alt@1.1.6:
address "^1.0.1"
debug "^2.6.0"
detect-port@^1.2.2, detect-port@^1.3.0:
detect-port@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1"
integrity sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==
@ -7366,6 +7376,14 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-scope@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9"
integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-utils@^1.3.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.0.tgz#e2c3c8dba768425f897cf0f9e51fe2e241485d4c"
@ -7373,12 +7391,67 @@ eslint-utils@^1.3.1:
dependencies:
eslint-visitor-keys "^1.0.0"
eslint-utils@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
dependencies:
eslint-visitor-keys "^1.0.0"
eslint-visitor-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
eslint@5.16.0, eslint@^5.0.0, eslint@^5.5.0:
eslint-visitor-keys@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
eslint@6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.4.0.tgz#5aa9227c3fbe921982b2eda94ba0d7fae858611a"
integrity sha512-WTVEzK3lSFoXUovDHEbkJqCVPEPwbhCq4trDktNI6ygs7aO41d4cDT0JFAT5MivzZeVLWlg7vHL+bgrQv/t3vA==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
chalk "^2.1.0"
cross-spawn "^6.0.5"
debug "^4.0.1"
doctrine "^3.0.0"
eslint-scope "^5.0.0"
eslint-utils "^1.4.2"
eslint-visitor-keys "^1.1.0"
espree "^6.1.1"
esquery "^1.0.1"
esutils "^2.0.2"
file-entry-cache "^5.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
globals "^11.7.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
inquirer "^6.4.1"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.3.0"
lodash "^4.17.14"
minimatch "^3.0.4"
mkdirp "^0.5.1"
natural-compare "^1.4.0"
optionator "^0.8.2"
progress "^2.0.0"
regexpp "^2.0.1"
semver "^6.1.2"
strip-ansi "^5.2.0"
strip-json-comments "^3.0.1"
table "^5.2.3"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
eslint@^5.0.0, eslint@^5.5.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea"
integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==
@ -7437,6 +7510,15 @@ espree@^5.0.1:
acorn-jsx "^5.0.0"
eslint-visitor-keys "^1.0.0"
espree@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de"
integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==
dependencies:
acorn "^7.0.0"
acorn-jsx "^5.0.2"
eslint-visitor-keys "^1.1.0"
esprima@^3.1.3, esprima@~3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
@ -9011,6 +9093,13 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
glob-parent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954"
integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==
dependencies:
is-glob "^4.0.1"
glob-stream@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4"
@ -9644,7 +9733,7 @@ html-tag-names@^1.1.1:
resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.4.tgz#51c559e36a077b5eb6c71e6cb49b1d70fffc9124"
integrity sha512-QCOY1/oHmo2BNwsTzuYlW51JLXSxfmMvve+2/9i2cbhxXxT6SuhsUWzcIoMwUi0HZW/NIQBSyJaj7fbcsimoKg==
html-webpack-plugin@^3.0.4:
html-webpack-plugin@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s=
@ -10008,6 +10097,25 @@ inquirer@^6.2.0, inquirer@^6.2.2:
strip-ansi "^5.1.0"
through "^2.3.6"
inquirer@^6.4.1:
version "6.5.2"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==
dependencies:
ansi-escapes "^3.2.0"
chalk "^2.4.2"
cli-cursor "^2.1.0"
cli-width "^2.0.0"
external-editor "^3.0.3"
figures "^2.0.0"
lodash "^4.17.12"
mute-stream "0.0.7"
run-async "^2.2.0"
rxjs "^6.4.0"
string-width "^2.1.0"
strip-ansi "^5.1.0"
through "^2.3.6"
internal-ip@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"
@ -10298,7 +10406,7 @@ is-glob@^3.1.0:
dependencies:
is-extglob "^2.1.0"
is-glob@^4.0.0:
is-glob@^4.0.0, is-glob@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
@ -12792,6 +12900,15 @@ normalize-url@^4.1.0:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee"
integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ==
"notistack@https://github.com/gnosis/notistack.git#v0.9.4":
version "0.9.4"
resolved "https://github.com/gnosis/notistack.git#077aa51fd066f2c3198b2f5ce773f741e21f24df"
dependencies:
classnames "^2.2.6"
hoist-non-react-statics "^3.3.0"
prop-types "^15.7.2"
react-is "^16.9.0"
now-and-later@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c"
@ -14595,15 +14712,15 @@ react-docgen@^4.1.0:
node-dir "^0.1.10"
recast "^0.17.3"
react-dom@16.9.0:
version "16.9.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962"
integrity sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==
react-dom@16.10.1:
version "16.10.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.10.1.tgz#479a6511ba34a429273c213cbc2a9ac4d296dac1"
integrity sha512-SmM4ZW0uug0rn95U8uqr52I7UdNf6wdGLeXDmNLfg3y5q5H9eAbdjF5ubQc3bjDyRrvdAB2IKG7X0GzSpnn5Mg==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.15.0"
scheduler "^0.16.1"
react-dom@^16.8.3:
version "16.8.6"
@ -14783,23 +14900,23 @@ react-redux@7.1.1:
prop-types "^15.7.2"
react-is "^16.9.0"
react-router-dom@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.0.tgz#48ad018d71fb7835212587e4c90bd2e3d2417e31"
integrity sha512-OkxKbMKjO7IkYqnoaZNX19MnwgjhxwZE871cPUTq0YU2wpIw7QwGxSnSoNRMOa7wO1TwvJJMFpgiEB4C/gVhTw==
react-router-dom@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.1.tgz#53caa089c291f64c1d597a52827b978b54d7c25d"
integrity sha512-r8R8H0Vt2ISqpk02rR6VZBLk+JZdR6pZV+h9K1y0ISh3/G4GGByNevYBS69x6czcOcWVRcZmXjwY8l9UBCKV+w==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.1.0"
react-router "5.1.1"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.0.tgz#739d0f3a57476363374e20d6e33e97f5ce2e00a3"
integrity sha512-n9HXxaL/6yRlig9XPfGyagI8+bUNdqcu7FUAx0/Z+Us22Z8iHsbkyJ21Inebn9HOxI5Nxlfc8GNabkNSeXfhqw==
react-router@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.1.tgz#9d65f54795f938c0c5b69eaeef58728134ce7c7c"
integrity sha512-ozTXqxKZsn4GfZqpG5rVFHSSxlNuDoMNxgyjM+mFJVhqlnPwwkRsAPkDm1PcNjBdYxMzqAhtz48HkQB6fSYaAQ==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
@ -14890,10 +15007,10 @@ react-transition-group@^4.3.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@16.9.0:
version "16.9.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa"
integrity sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w==
react@16.10.1:
version "16.10.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.10.1.tgz#967c1e71a2767dfa699e6ba702a00483e3b0573f"
integrity sha512-2bisHwMhxQ3XQz4LiJJwG3360pY965pTl/MRrZYxIBKVj4fOHoDs5aZAkYXGxDRO1Li+SyjTAilQEbOmtQJHzA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@ -15115,7 +15232,7 @@ reduce-reducers@^0.4.3:
resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.4.3.tgz#8e052618801cd8fc2714b4915adaa8937eb6d66c"
integrity sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw==
redux-actions@^2.3.0:
redux-actions@^2.6.5:
version "2.6.5"
resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.6.5.tgz#bdca548768ee99832a63910c276def85e821a27e"
integrity sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw==
@ -15159,7 +15276,7 @@ redux-saga@1.0.0:
dependencies:
"@redux-saga/core" "^1.0.0"
redux-thunk@^2.2.0:
redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
@ -15795,10 +15912,10 @@ scheduler@^0.13.6:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.15.0.tgz#6bfcf80ff850b280fed4aeecc6513bc0b4f17f8e"
integrity sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==
scheduler@^0.16.1:
version "0.16.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.16.1.tgz#a6fb6ddec12dc2119176e6eb54ecfe69a9eba8df"
integrity sha512-MIuie7SgsqMYOdCXVFZa8SKoNorJZUWHW8dPgto7uEHn1lX3fg2Gu0TzgK8USj76uxV7vB5eRMnZs/cdEHg+cg==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@ -15970,7 +16087,7 @@ semver@6.2.0, semver@^6.0.0, semver@^6.1.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db"
integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==
semver@^6.2.0, semver@^6.3.0:
semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@ -16554,7 +16671,7 @@ storybook-host@5.1.0:
ramda "^0.25.0"
tinycolor2 "^1.4.1"
storybook-router@^0.3.3:
storybook-router@^0.3.4:
version "0.3.4"
resolved "https://registry.yarnpkg.com/storybook-router/-/storybook-router-0.3.4.tgz#27c0c0de5eafa03b9003a850ac40e2b0c2a3ee65"
integrity sha512-WU8kyx06R5zFa3KT1TZey2fOadj0nFhWs5yuq3iVcfSbUhtwSg/QNGF7V6IXjiBOtYqmvN2/hlUnYneC/oQ16w==
@ -16771,6 +16888,11 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
strip-json-comments@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==
style-loader@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.0.0.tgz#1d5296f9165e8e2c85d24eee0b7caf9ec8ca1f82"
@ -18364,6 +18486,11 @@ v8-compile-cache@2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
v8flags@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"