Pull from dev, conflict fixes
This commit is contained in:
commit
99ddb5989f
29
.travis.yml
29
.travis.yml
|
@ -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
|
|
@ -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 we’re 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 we’re 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
|
||||
|
|
|
@ -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
|
47
package.json
47
package.json
|
@ -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",
|
||||
|
|
22
readme.md
22
readme.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)}
|
||||
</>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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),
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
import { createStructuredSelector } from 'reselect'
|
||||
import { notificationsListSelector } from '~/logic/notifications/store/selectors'
|
||||
|
||||
export default createStructuredSelector<Object, *>({
|
||||
notifications: notificationsListSelector,
|
||||
})
|
|
@ -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
|
|
@ -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)
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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
|
|
@ -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/',
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
export * from './notificationTypes'
|
||||
export * from './notificationBuilder'
|
|
@ -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>
|
||||
),
|
||||
})
|
|
@ -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 },
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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(),
|
||||
}),
|
||||
)
|
|
@ -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(),
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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',
|
||||
}
|
|
@ -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',
|
||||
}
|
|
@ -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
|
||||
// // }
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
||||
|
||||
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
219
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue