Merge pull request #229 from gnosis/124-web3connect-new

Feature 124: Web3connect
This commit is contained in:
Mikhail Mikheev 2019-11-11 16:08:29 +04:00 committed by GitHub
commit 4579f88fa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 3188 additions and 3951 deletions

2
.gitignore vendored
View File

@ -4,4 +4,4 @@ build_storybook/
.DS_Store
build/
yarn-error.log
.env.*
.env*

View File

@ -35,27 +35,29 @@
"@gnosis.pm/util-contracts": "2.0.4",
"@material-ui/core": "4.6.0",
"@material-ui/icons": "4.5.1",
"@portis/web3": "^2.0.0-beta.45",
"@testing-library/jest-dom": "4.2.3",
"@welldone-software/why-did-you-render": "3.3.8",
"@toruslabs/torus-embed": "0.2.6",
"@walletconnect/web3-provider": "^1.0.0-beta.37",
"@welldone-software/why-did-you-render": "3.3.9",
"axios": "0.19.0",
"bignumber.js": "9.0.0",
"connected-react-router": "6.5.2",
"date-fns": "2.6.0",
"date-fns": "2.7.0",
"ethereum-ens": "0.7.8",
"final-form": "4.18.5",
"final-form": "4.18.6",
"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",
"qrcode.react": "1.0.0",
"react": "16.11.0",
"react-dom": "16.11.0",
"react-final-form": "6.3.0",
"react-final-form-listeners": "^1.0.2",
"react-hot-loader": "4.12.15",
"react-infinite-scroll-component": "5.0.2",
"react-hot-loader": "4.12.16",
"react-qr-reader": "^2.2.1",
"react-redux": "7.1.3",
"react-router-dom": "5.1.2",
@ -65,11 +67,13 @@
"redux-actions": "^2.6.5",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"web3": "1.2.2"
"squarelink": "^1.1.3",
"web3": "1.2.2",
"web3connect": "^1.0.0-beta.23"
},
"devDependencies": {
"@babel/cli": "7.7.0",
"@babel/core": "7.7.0",
"@babel/core": "7.7.2",
"@babel/plugin-proposal-class-properties": "7.7.0",
"@babel/plugin-proposal-decorators": "7.7.0",
"@babel/plugin-proposal-do-expressions": "7.6.0",
@ -93,10 +97,10 @@
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "7.7.0",
"@sambego/storybook-state": "^1.3.6",
"@storybook/addon-actions": "5.2.5",
"@storybook/addon-knobs": "5.2.5",
"@storybook/addon-links": "5.2.5",
"@storybook/react": "5.2.5",
"@storybook/addon-actions": "5.2.6",
"@storybook/addon-knobs": "5.2.6",
"@storybook/addon-links": "5.2.6",
"@storybook/react": "5.2.6",
"@testing-library/react": "9.3.2",
"autoprefixer": "9.7.1",
"babel-core": "^7.0.0-bridge.0",
@ -106,14 +110,15 @@
"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",
"babel-polyfill": "^6.26.0",
"classnames": "^2.2.6",
"css-loader": "3.2.0",
"detect-port": "^1.3.0",
"eslint": "6.6.0",
"eslint": "5.16.0",
"eslint-config-airbnb": "18.0.1",
"eslint-plugin-flowtype": "4.3.0",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jest": "23.0.2",
"eslint-plugin-jest": "23.0.3",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.16.0",
"ethereumjs-abi": "0.6.8",
@ -136,7 +141,7 @@
"storybook-host": "5.1.0",
"storybook-router": "^0.3.4",
"style-loader": "1.0.0",
"truffle": "5.0.43",
"truffle": "5.0.44",
"truffle-contract": "4.0.31",
"truffle-solidity-loader": "0.1.32",
"uglifyjs-webpack-plugin": "2.2.0",

View File

@ -0,0 +1,82 @@
// @flow
import React from 'react'
import { connect } from 'react-redux'
import Web3Connect from 'web3connect'
// import Torus from '@toruslabs/torus-embed'
import WalletConnectProvider from '@walletconnect/web3-provider'
import Portis from '@portis/web3'
import Squarelink from 'squarelink'
import Button from '~/components/layout/Button'
import { fetchProvider } from '~/logic/wallets/store/actions'
import { getNetwork } from '~/config'
import { store } from '~/store'
const PORTIS_DAPP_ID = process.env.REACT_APP_NETWORK === 'mainnet' ? process.env.REACT_APP_PORTIS_ID : '852b763d-f28b-4463-80cb-846d7ec5806b'
const SQUARELINK_CLIENT_ID = process.env.REACT_APP_NETWORK === 'mainnet' ? process.env.REACT_APP_SQUARELINK_ID : '46ce08fe50913cfa1b78'
export const web3Connect = new Web3Connect.Core({
network: getNetwork().toLowerCase(),
providerOptions: {
walletconnect: {
package: WalletConnectProvider,
options: {
infuraId: process.env.REACT_APP_INFURA_TOKEN,
},
},
portis: {
package: Portis,
options: {
id: PORTIS_DAPP_ID,
},
},
squarelink: {
package: Squarelink,
options: {
id: SQUARELINK_CLIENT_ID,
},
},
// torus: {
// package: Torus,
// options: {
// enableLogging: false,
// buttonPosition: 'bottom-left',
// buildEnv: process.env.NODE_ENV,
// showTorusButton: true,
// },
// },
},
})
web3Connect.on('connect', (provider: any) => {
if (provider) {
store.dispatch(fetchProvider(provider))
}
})
type Props = {
registerProvider: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
const ConnectButton = ({
registerProvider, ...props
}: Props) => (
<Button
color="primary"
variant="contained"
minWidth={140}
onClick={() => {
web3Connect.toggleModal()
}}
{...props}
>
Connect
</Button>
)
export default connect(
null,
{ registerProvider: fetchProvider },
)(ConnectButton)

View File

@ -1 +0,0 @@
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><mask id="a" fill="#fff"><path d="m6.72043466.05975844h-6.62356495v11.84970376h6.62356495z" fill="#fff" fill-rule="evenodd"/></mask><g fill="#001428" fill-rule="evenodd"><path d="m4.14977665 6.07551536-1.71737342-1.70732942c-.154797.20785847-.24831026.46589514-.24831026.74441596 0 .67391239.54853704 1.22126772 1.22091168 1.22126772.27926965 0 .53821724-.09336958.744772-.25835426"/><path d="m1.87126776 2.19027617c1.03982725-1.02106347 2.40224018-1.57576779 3.8633547-1.57576779h.00917804c.01609919 0 .03219839.00091234.04814712.00121646v5.5467391zm3.85282252-2.13062417c-1.69583171 0-3.2740039.69414069-4.43103929 1.93158142l-.18657008.19904275 4.68546671 4.75589009v.85273625l-.01850655.01870302-1.20548342-1.2283173c-.55925885.37618928-1.27484536.48551834-1.95071051.24739691-1.13777654-.41618029-1.72637503-1.67445284-1.31411535-2.81366468.05852884-.17821093.14669825-.3371106.24449708-.48536628l-.51938702-.5253573-.09794929.16847927c-.5392477.89211904-.83339649 1.92230593-.83339649 2.98260013-.00947896 3.16111517 2.53900808 5.74578182 5.66631374 5.74578182l.95722485.0003041v-11.84889785z" mask="url(#a)" transform="translate(.218409)"/><path d="m11.7375061 6.11561396c0-2.64879063-2.15538824-4.8037754-4.80500459-4.80515818v.57492892c2.33248021.00122914 4.22991669 1.89824874 4.22991669 4.23022926 0 2.33136596-1.89743648 4.22869284-4.22991669 4.22992194v.574929c2.64961635-.0013828 4.80500459-2.15636759 4.80500459-4.80485094"/><path d="m8.02004901 6.01820975-.22912891 1.40770636h1.1220214l-.2288201-1.40770636c.25522242-.12241582.43247309-.37950419.43247309-.67820486 0-.41582039-.34214937-.75340965-.76443209-.75340965-.42197392 0-.76443209.33758926-.76443209.75340965-.00046319.2990033.17647867.55578904.4323187.67820486"/><path d="m7.75184322 2.83111882c.28114484.0716844.38948776-.34809944.1084868-.42035731-.28100096-.07197114-.38934388.34809944-.1084868.42035731"/><path d="m8.97850178 3.44035856c.21945107.18400118.4955759-.14470408.27612484-.3285634-.21945107-.18371744-.49543387.14484596-.27612484.3285634"/><path d="m9.79686881 4.68163059c.12643597.25912811.51314779.0686432.38671179-.19048491-.1261512-.25841307-.51314776-.06821418-.38671179.19048491"/><path d="m7.75184322 9.3998634c.28114484-.07200813.38948776.34827835.1084868.42028648s-.38934388-.34799147-.1084868-.42028648"/><path d="m8.97850178 8.5721264c.21945107-.18396063.4955759.14481403.27612484.32849098-.21945107.18396062-.49543387-.14481403-.27612484-.32849098"/><path d="m9.79686881 7.54913594c.12643597-.25888516.51314779-.06847377.38671179.19069729-.1261512.25831336-.51314776.06818787-.38671179-.19069729"/><path d="m10.208641 6.11535204c0 .29135677.4368186.29135677.4368186 0 0-.29106802-.4368186-.29106802-.4368186 0"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13">
<path fill="#001428" fill-rule="evenodd" d="M.658 0L0 1.936l.43 2.053-.278.216.404.313-.308.238.41.368-.26.175.595.686-.881 2.78.83 2.798 2.975-.777 1.58 1.231h1.976l1.72-1.26 2.866.806.83-2.798h.003l-.885-2.78.594-.686-.26-.174.41-.368-.307-.239.404-.313-.278-.217.43-2.052L12.341 0 8.197 1.522H4.804L.658 0zm7.427 6.826l1.693.796-2.369.683.676-1.48zm-4.863.794l1.693-.794.676 1.479-2.37-.685zm2.276 2.274l.294-.17h1.416l.277.178.093 1.033H5.401l.097-1.041z"/>
</svg>

Before

Width:  |  Height:  |  Size: 557 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="14" viewBox="0 0 19 14">
<path fill="#B2B5B2" fill-rule="nonzero" d="M16 4V2H2v10h14v-2h-3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3zm2 6v2a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1zm-4-4v2h3V6h-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@ -2,14 +2,14 @@
import * as React from 'react'
import { withStyles } from '@material-ui/core/styles'
import Paragraph from '~/components/layout/Paragraph'
import Button from '~/components/layout/Button'
import ConnectButton from '~/components/ConnectButton'
import Row from '~/components/layout/Row'
import Block from '~/components/layout/Block'
import { md, lg } from '~/theme/variables'
import CircleDot from '~/components/Header/component/CircleDot'
import CircleDot from '~/components/Header/components/CircleDot'
type Props = {
classes: Object,
onConnect: Function,
}
const styles = () => ({
@ -26,6 +26,7 @@ const styles = () => ({
},
connect: {
padding: `${md} ${lg}`,
textAlign: 'center',
},
connectText: {
letterSpacing: '1px',
@ -35,7 +36,7 @@ const styles = () => ({
},
})
const ConnectDetails = ({ classes, onConnect }: Props) => (
const ConnectDetails = ({ classes }: Props) => (
<>
<div className={classes.container}>
<Row margin="lg" align="center">
@ -47,13 +48,9 @@ const ConnectDetails = ({ classes, onConnect }: Props) => (
<Row className={classes.logo} margin="lg">
<CircleDot keySize={32} circleSize={75} dotSize={25} dotTop={50} dotRight={25} center mode="error" />
</Row>
<Row className={classes.connect}>
<Button onClick={onConnect} size="medium" variant="contained" color="primary" fullWidth>
<Paragraph className={classes.connectText} size="sm" weight="regular" color="white" noMargin>
CONNECT
</Paragraph>
</Button>
</Row>
<Block className={classes.connect}>
<ConnectButton />
</Block>
</>
)

View File

@ -18,10 +18,9 @@ import {
} from '~/theme/variables'
import { upperFirst } from '~/utils/css'
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
import CircleDot from '~/components/Header/component/CircleDot'
import CircleDot from '~/components/Header/components/CircleDot'
const metamaskIcon = require('../../assets/metamask-icon.svg')
const safeIcon = require('../../assets/gnosis-safe-icon.svg')
const walletIcon = require('../../assets/wallet.svg')
const dot = require('../../assets/dotRinkeby.svg')
type Props = {
@ -141,11 +140,7 @@ const UserDetails = ({
Wallet
</Paragraph>
<Spacer />
{provider === 'safe' ? (
<Img className={classes.logo} src={safeIcon} height={14} alt="Safe client" />
) : (
<Img className={classes.logo} src={metamaskIcon} height={14} alt="Metamask client" />
)}
<Img className={classes.logo} src={walletIcon} height={14} alt="Wallet icon" />
<Paragraph noMargin align="right" weight="bolder" className={classes.labels}>
{upperFirst(provider)}
</Paragraph>

View File

@ -7,7 +7,7 @@ import Col from '~/components/layout/Col'
import { connected as connectedBg, sm } from '~/theme/variables'
import Identicon from '~/components/Identicon'
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
import CircleDot from '~/components/Header/component/CircleDot'
import CircleDot from '~/components/Header/components/CircleDot'
type Props = {
provider: string,

View File

@ -5,7 +5,7 @@ import Paragraph from '~/components/layout/Paragraph'
import Col from '~/components/layout/Col'
import { type Open } from '~/components/hoc/OpenHoc'
import { sm } from '~/theme/variables'
import CircleDot from '~/components/Header/component/CircleDot'
import CircleDot from '~/components/Header/components/CircleDot'
type Props = Open & {
classes: Object,
@ -29,7 +29,7 @@ const styles = () => ({
},
})
const ProviderDesconnected = ({ classes }: Props) => (
const ProviderDisconnected = ({ classes }: Props) => (
<>
<CircleDot keySize={17} circleSize={35} dotSize={16} dotTop={24} dotRight={11} mode="error" />
<Col end="sm" middle="xs" layout="column" className={classes.account}>
@ -43,4 +43,4 @@ const ProviderDesconnected = ({ classes }: Props) => (
</>
)
export default withStyles(styles)(ProviderDesconnected)
export default withStyles(styles)(ProviderDisconnected)

View File

@ -3,20 +3,22 @@ import * as React from 'react'
import { connect } from 'react-redux'
import { withSnackbar } from 'notistack'
import { logComponentStack, type Info } from '~/utils/logBoundaries'
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'
import ConnectDetails from './component/ProviderDetails/ConnectDetails'
import Layout from './component/Layout'
import { web3Connect } from '~/components/ConnectButton'
import { INJECTED_PROVIDERS } from '~/logic/wallets/getWeb3'
import { loadLastUsedProvider } from '~/logic/wallets/store/middlewares/providerWatcher'
import ProviderAccessible from './components/ProviderInfo/ProviderAccessible'
import UserDetails from './components/ProviderDetails/UserDetails'
import ProviderDisconnected from './components/ProviderInfo/ProviderDisconnected'
import ConnectDetails from './components/ProviderDetails/ConnectDetails'
import Layout from './components/Layout'
import actions, { type Actions } from './actions'
import selector, { type SelectorProps } from './selector'
type Props = Actions &
SelectorProps & {
enqueueSnackbar: Function,
closeSnackbar: Function,
}
type State = {
@ -24,8 +26,6 @@ type State = {
}
class HeaderComponent extends React.PureComponent<Props, State> {
providerListener: ?IntervalID
constructor(props) {
super(props)
@ -34,8 +34,11 @@ class HeaderComponent extends React.PureComponent<Props, State> {
}
}
componentDidMount() {
this.onConnect()
async componentDidMount() {
const lastUsedProvider = await loadLastUsedProvider()
if (INJECTED_PROVIDERS.includes(lastUsedProvider) || process.env.NODE_ENV === 'test') {
web3Connect.connectToInjected()
}
}
componentDidCatch(error: Error, info: Info) {
@ -50,27 +53,9 @@ class HeaderComponent extends React.PureComponent<Props, State> {
onDisconnect = () => {
const { removeProvider, enqueueSnackbar, closeSnackbar } = this.props
clearInterval(this.providerListener)
removeProvider(enqueueSnackbar, closeSnackbar)
}
onConnect = async () => {
const { fetchProvider, enqueueSnackbar, closeSnackbar } = this.props
clearInterval(this.providerListener)
let currentProvider: ProviderProps = await getProviderInfo()
fetchProvider(currentProvider, enqueueSnackbar, closeSnackbar)
this.providerListener = setInterval(async () => {
const newProvider: ProviderProps = await getProviderInfo()
if (currentProvider && JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) {
fetchProvider(newProvider, enqueueSnackbar, closeSnackbar)
}
currentProvider = newProvider
}, 2000)
}
getProviderInfoBased = () => {
const { hasError } = this.state
const {
@ -81,7 +66,7 @@ class HeaderComponent extends React.PureComponent<Props, State> {
return <ProviderDisconnected />
}
return <ProviderAccesible provider={provider} network={network} userAddress={userAddress} connected={available} />
return <ProviderAccessible provider={provider} network={network} userAddress={userAddress} connected={available} />
}
getProviderDetailsBased = () => {
@ -91,7 +76,7 @@ class HeaderComponent extends React.PureComponent<Props, State> {
} = this.props
if (hasError || !loaded) {
return <ConnectDetails onConnect={this.onConnect} />
return <ConnectDetails />
}
return (

View File

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

View File

@ -9,6 +9,8 @@ import selector from './selector'
type Props = Actions & {
notifications: List<Notification>,
closeSnackbar: Function,
enqueueSnackbar: Function,
}
class Notifier extends Component<Props> {
@ -46,8 +48,10 @@ class Notifier extends Component<Props> {
if (this.displayed.includes(notification.key)) {
return
}
// Display snackbar using notistack
enqueueSnackbar(notification.message, {
key: notification.key,
...notification.options,
onClose: (event, reason, key) => {
if (notification.options.onClose) {

View File

@ -73,7 +73,7 @@ const SafeList = ({
{safes.map((safe) => (
<React.Fragment key={safe.address}>
<Link
to={`${SAFELIST_ADDRESS}/${safe.address}`}
to={`${SAFELIST_ADDRESS}/${safe.address}/balances`}
onClick={onSafeClick}
data-testid={SIDEBAR_SAFELIST_ROW_TESTID}
>

View File

@ -4,6 +4,7 @@ import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/ProxyFact
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/Proxy.json'
import { ensureOnce } from '~/utils/singleton'
import { simpleMemoize } from '~/components/forms/validator'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
@ -27,10 +28,10 @@ const createProxyFactoryContract = (web3: any) => {
return proxyFactory
}
export const getGnosisSafeContract = ensureOnce(createGnosisSafeContract)
const getCreateProxyFactoryContract = ensureOnce(createProxyFactoryContract)
export const getGnosisSafeContract = simpleMemoize(createGnosisSafeContract)
const getCreateProxyFactoryContract = simpleMemoize(createProxyFactoryContract)
const instanciateMasterCopies = async () => {
const instantiateMasterCopies = async () => {
const web3 = getWeb3()
// Create ProxyFactory Master Copy
@ -55,7 +56,7 @@ const createMasterCopies = async () => {
safeMaster = await GnosisSafe.new({ from: userAccount, gas: '7000000' })
}
export const initContracts = ensureOnce(process.env.NODE_ENV === 'test' ? createMasterCopies : instanciateMasterCopies)
export const initContracts = process.env.NODE_ENV === 'test' ? ensureOnce(createMasterCopies) : instantiateMasterCopies
export const getSafeMasterContract = async () => {
await initContracts()
@ -106,7 +107,7 @@ const cleanByteCodeMetadata = (bytecode: string): string => {
return bytecode.substring(0, bytecode.lastIndexOf(metaData))
}
export const validateProxy = async (safeAddress: string): boolean => {
export const validateProxy = async (safeAddress: string): Promise<boolean> => {
// https://solidity.readthedocs.io/en/latest/metadata.html#usage-for-source-code-verification
const web3 = getWeb3()
const code = await web3.eth.getCode(safeAddress)

View File

@ -3,6 +3,8 @@ 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 { store } from '~/store'
import closeSnackbarAction from '~/logic/notifications/store/actions/closeSnackbar'
import { type Notification, NOTIFICATIONS } from './notificationTypes'
export type NotificationsQueue = {
@ -52,12 +54,12 @@ const cancellationTxNotificationsQueue: NotificationsQueue = {
const ownerChangeTxNotificationsQueue: NotificationsQueue = {
beforeExecution: NOTIFICATIONS.SIGN_OWNER_CHANGE_MSG,
pendingExecution: {
noMoreConfirmationsNeeded: NOTIFICATIONS.ONWER_CHANGE_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.ONWER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG,
noMoreConfirmationsNeeded: NOTIFICATIONS.OWNER_CHANGE_PENDING_MSG,
moreConfirmationsNeeded: NOTIFICATIONS.OWNER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG,
},
afterRejection: NOTIFICATIONS.ONWER_CHANGE_REJECTED_MSG,
afterRejection: NOTIFICATIONS.OWNER_CHANGE_REJECTED_MSG,
afterExecution: NOTIFICATIONS.OWNER_CHANGE_EXECUTED_MSG,
afterExecutionError: NOTIFICATIONS.ONWER_CHANGE_FAILED_MSG,
afterExecutionError: NOTIFICATIONS.OWNER_CHANGE_FAILED_MSG,
}
const safeNameChangeNotificationsQueue: NotificationsQueue = {
@ -67,7 +69,7 @@ const safeNameChangeNotificationsQueue: NotificationsQueue = {
moreConfirmationsNeeded: null,
},
afterRejection: null,
afterExecution: NOTIFICATIONS.SAFE_NAME_CHANGE_EXECUTED_MSG,
afterExecution: NOTIFICATIONS.SAFE_NAME_CHANGED_MSG,
afterExecutionError: null,
}
@ -145,11 +147,19 @@ export const getNotificationsFromTxType = (txType: string) => {
return notificationsQueue
}
export const showSnackbar = (
notification: Notification,
enqueueSnackbar: Function,
closeSnackbar: Function,
) => enqueueSnackbar(notification.message, {
export const enhanceSnackbarForAction = (notification: Notification) => ({
...notification,
options: {
...notification.options,
action: (key) => (
<IconButton onClick={() => store.dispatch(closeSnackbarAction(key))}>
<IconClose />
</IconButton>
),
},
})
export const showSnackbar = (notification: Notification, enqueueSnackbar: Function, closeSnackbar: Function) => enqueueSnackbar(notification.message, {
...notification.options,
action: (key) => (
<IconButton onClick={() => closeSnackbar(key)}>

View File

@ -10,15 +10,15 @@ export const INFO = 'info'
const shortDuration = 5000
const longDuration = 10000
export type Variant = SUCCESS | ERROR | WARNING | INFO
export type Variant = 'success' | 'error' | 'warning' | 'info'
export type Notification = {
message: string,
options: {
variant: Variant,
persist: boolean,
autoHideDuration?: shortDuration | longDuration,
preventDuplicate: boolean,
autoHideDuration?: 5000 | 10000,
preventDuplicate?: boolean,
},
}
@ -52,11 +52,11 @@ export type Notifications = {
// 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,
OWNER_CHANGE_PENDING_MSG: Notification,
OWNER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: Notification,
OWNER_CHANGE_REJECTED_MSG: Notification,
OWNER_CHANGE_EXECUTED_MSG: Notification,
OWNER_CHANGE_FAILED_MSG: Notification,
// Threshold
SIGN_THRESHOLD_CHANGE_MSG: Notification,
@ -147,7 +147,7 @@ export const NOTIFICATIONS: Notifications = {
},
// Safe Name
SAFE_NAME_CHANGE_EXECUTED_MSG: {
SAFE_NAME_CHANGED_MSG: {
message: 'Safe name changed',
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
},
@ -163,15 +163,15 @@ export const NOTIFICATIONS: Notifications = {
message: 'Please sign the owner change',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_PENDING_MSG: {
OWNER_CHANGE_PENDING_MSG: {
message: 'Owner change pending',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: {
OWNER_CHANGE_PENDING_MORE_CONFIRMATIONS_MSG: {
message: 'Owner change pending: More confirmations required to execute',
options: { variant: SUCCESS, persist: true },
},
ONWER_CHANGE_REJECTED_MSG: {
OWNER_CHANGE_REJECTED_MSG: {
message: 'Owner change rejected',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},
@ -179,7 +179,7 @@ export const NOTIFICATIONS: Notifications = {
message: 'Owner change successfully executed',
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
},
ONWER_CHANGE_FAILED_MSG: {
OWNER_CHANGE_FAILED_MSG: {
message: 'Owner change failed',
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
},

View File

@ -15,6 +15,7 @@ const enqueueSnackbar = (notification: NotificationProps) => (
...notification,
key: new Date().getTime(),
}
dispatch(addSnackbar(newNotification))
}

View File

@ -6,7 +6,7 @@ export type NotificationProps = {
key?: number,
message: string,
options: Object,
dismissed: boolean,
dismissed?: boolean,
}
export const makeNotification: RecordFactory<NotificationProps> = Record({

View File

@ -18,10 +18,9 @@ export default handleActions<NotificationReducerState, *>(
return state.set(notification.key, makeNotification(notification))
},
[CLOSE_SNACKBAR]: (state: NotificationReducerState, action: ActionType<Function>): NotificationReducerState => {
const { notification }: { notification: NotificationProps } = action.payload
notification.dismissed = true
const key = action.payload
return state.update(notification.key, (prev) => prev.merge(notification))
return state.update(key, (prev) => prev.set('dismissed', true))
},
[REMOVE_SNACKBAR]: (state: NotificationReducerState, action: ActionType<Function>): NotificationReducerState => {
const key = action.payload

View File

@ -1,7 +1,7 @@
// @flow
import { BigNumber } from 'bignumber.js'
import axios from 'axios'
import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getWeb3, web3ReadOnly } from '~/logic/wallets/getWeb3'
// const MAINNET_NETWORK = 1
export const EMPTY_DATA = '0x'
@ -11,8 +11,7 @@ export const checkReceiptStatus = async (hash: string) => {
return Promise.reject(new Error('No valid Tx hash to get receipt from'))
}
const web3 = getWeb3()
const txReceipt = await web3.eth.getTransactionReceipt(hash)
const txReceipt = await web3ReadOnly.eth.getTransactionReceipt(hash)
const { status } = txReceipt
if (!status) {

View File

@ -17,11 +17,23 @@ export const ETHEREUM_NETWORK = {
export const WALLET_PROVIDER = {
SAFE: 'SAFE',
METAMASK: 'METAMASK',
PARITY: 'PARITY',
REMOTE: 'REMOTE',
UPORT: 'UPORT',
TORUS: 'TORUS',
PORTIS: 'PORTIS',
FORTMATIC: 'FORTMATIC',
SQUARELINK: 'SQUARELINK',
WALLETCONNECT: 'WALLETCONNECT',
OPERA: 'OPERA',
DAPPER: 'DAPPER',
}
export const INJECTED_PROVIDERS = [
WALLET_PROVIDER.SAFE,
WALLET_PROVIDER.METAMASK,
WALLET_PROVIDER.OPERA,
WALLET_PROVIDER.DAPPER,
]
export const ETHEREUM_NETWORK_IDS = {
// $FlowFixMe
1: ETHEREUM_NETWORK.MAINNET,
@ -44,8 +56,24 @@ export const getEtherScanLink = (type: 'address' | 'tx', value: string) => {
}etherscan.io/${type}/${value}`
}
let web3
export const getWeb3 = () => web3 || (window.web3 && new Web3(window.web3.currentProvider)) || (window.ethereum && new Web3(window.ethereum))
const getInfuraUrl = () => {
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
return `https://${isMainnet ? '' : 'rinkeby.'}infura.io:443/v3/${process.env.REACT_APP_INFURA_TOKEN}`
}
// With some wallets from web3connect you have to use their provider instance only for signing
// And our own one to fetch data
export const web3ReadOnly = process.env.NODE_ENV !== 'test'
? new Web3(new Web3.providers.HttpProvider(getInfuraUrl()))
: new Web3(window.web3.currentProvider)
let web3 = web3ReadOnly
export const getWeb3 = () => web3
export const resetWeb3 = () => {
web3 = web3ReadOnly
}
const getProviderName: Function = (web3Provider): string => {
let name
@ -56,11 +84,41 @@ const getProviderName: Function = (web3Provider): string => {
break
case 'MetamaskInpageProvider':
name = WALLET_PROVIDER.METAMASK
if (web3Provider.currentProvider.isTorus) {
name = WALLET_PROVIDER.TORUS
}
break
case 'Object':
if (navigator && /Opera|OPR\//.test(navigator.userAgent)) {
name = WALLET_PROVIDER.OPERA
} else {
name = 'Wallet'
}
break
case 'DapperLegacyProvider':
name = WALLET_PROVIDER.DAPPER
break
default:
name = 'Wallet'
}
if (web3Provider.currentProvider.isPortis) {
name = WALLET_PROVIDER.PORTIS
}
if (web3Provider.currentProvider.isFortmatic) {
name = WALLET_PROVIDER.FORTMATIC
}
if (web3Provider.currentProvider.isSquarelink) {
name = WALLET_PROVIDER.SQUARELINK
}
if (web3Provider.currentProvider.isWalletConnect) {
name = WALLET_PROVIDER.WALLETCONNECT
}
return name
}
@ -80,28 +138,7 @@ const getNetworkIdFrom = async (web3Provider) => {
return networkId
}
export const getProviderInfo: Function = async (): Promise<ProviderProps> => {
let web3Provider
if (window.ethereum) {
web3Provider = window.ethereum
try {
await web3Provider.enable()
} catch (error) {
console.error('Error when enabling web3 provider', error)
}
} else if (window.web3) {
web3Provider = window.web3.currentProvider
} else {
return {
name: '',
available: false,
loaded: false,
account: '',
network: 0,
}
}
export const getProviderInfo: Function = async (web3Provider): Promise<ProviderProps> => {
web3 = new Web3(web3Provider)
const name = getProviderName(web3)

View File

@ -4,6 +4,6 @@ import { type Provider } from '~/logic/wallets/store/model/provider'
export const ADD_PROVIDER = 'ADD_PROVIDER'
const addProvider = createAction(ADD_PROVIDER, (provider: Provider) => provider)
const addProvider = createAction<string, *, *>(ADD_PROVIDER, (provider: Provider) => provider)
export default addProvider

View File

@ -1,10 +1,13 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
import { ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK, getProviderInfo } 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 { NOTIFICATIONS, showSnackbar, enhanceSnackbarForAction } from '~/logic/notifications'
import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbar from '~/logic/notifications/store/actions/closeSnackbar'
import addProvider from './addProvider'
export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: ProviderProps) => {
@ -23,24 +26,20 @@ export const processProviderResponse = (dispatch: ReduxDispatch<*>, provider: Pr
dispatch(addProvider(walletRecord))
}
const handleProviderNotification = (
provider: ProviderProps,
enqueueSnackbar: Function,
closeSnackbar: Function,
) => {
const { loaded, available, network } = provider
const handleProviderNotification = (provider: ProviderProps, dispatch: Function) => {
const { loaded, network, available } = provider
if (!loaded) {
showSnackbar(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.CONNECT_WALLET_ERROR_MSG)))
return
}
if (ETHEREUM_NETWORK_IDS[network] !== getNetwork()) {
showSnackbar(NOTIFICATIONS.WRONG_NETWORK_MSG, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.WRONG_NETWORK_MSG)))
return
}
if (ETHEREUM_NETWORK.RINKEBY === getNetwork()) {
showSnackbar(NOTIFICATIONS.RINKEBY_VERSION_MSG, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.RINKEBY_VERSION_MSG)))
}
if (available) {
@ -49,15 +48,14 @@ const handleProviderNotification = (
// 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)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.WALLET_CONNECTED_MSG)))
} else {
showSnackbar(NOTIFICATIONS.UNLOCK_WALLET_MSG, enqueueSnackbar, closeSnackbar)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.UNLOCK_WALLET_MSG)))
}
}
export default (provider: ProviderProps, enqueueSnackbar: Function, closeSnackbar: Function) => (
dispatch: ReduxDispatch<*>,
) => {
handleProviderNotification(provider, enqueueSnackbar, closeSnackbar)
processProviderResponse(dispatch, provider)
export default (provider: Object) => async (dispatch: ReduxDispatch<*>) => {
const providerInfo: ProviderProps = await getProviderInfo(provider)
await handleProviderNotification(providerInfo, dispatch)
processProviderResponse(dispatch, providerInfo)
}

View File

@ -1,6 +1,7 @@
// @flow
export * from './addProvider'
export * from './fetchProvider'
export * from './removeProvider'
export { default as addProvider } from './addProvider'
export { default as fetchProvider } from './fetchProvider'
export { default as removeProvider } from './removeProvider'

View File

@ -1,20 +1,22 @@
// @flow
import { createAction } from 'redux-actions'
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'
import { getWeb3, resetWeb3 } from '~/logic/wallets/getWeb3'
export default (enqueueSnackbar: Function, closeSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => {
const providerProps: ProviderProps = {
name: '',
available: false,
loaded: false,
account: '',
network: 0,
}
export const REMOVE_PROVIDER = 'REMOVE_PROVIDER'
const provider: Provider = makeProvider(providerProps)
const removeProvider = createAction<string, *, *>(REMOVE_PROVIDER)
export default (enqueueSnackbar: Function, closeSnackbar: Function) => (dispatch: ReduxDispatch<*>) => {
showSnackbar(NOTIFICATIONS.WALLET_DISCONNECTED_MSG, enqueueSnackbar, closeSnackbar)
dispatch(addProvider(provider))
const web3 = getWeb3()
if (web3.currentProvider && web3.currentProvider.close) {
web3.currentProvider.close()
}
resetWeb3()
dispatch(removeProvider())
}

View File

@ -0,0 +1,61 @@
// @flow
import type { Store, AnyAction } from 'redux'
import { type GlobalState } from '~/store/'
import { ADD_PROVIDER, REMOVE_PROVIDER } from '../actions'
import { getWeb3, getProviderInfo } from '~/logic/wallets/getWeb3'
import { fetchProvider } from '~/logic/wallets/store/actions'
import { loadFromStorage, saveToStorage, removeFromStorage } from '~/utils/storage'
const watchedActions = [ADD_PROVIDER, REMOVE_PROVIDER]
const LAST_USED_PROVIDER_KEY = 'LAST_USED_PROVIDER'
export const loadLastUsedProvider = async () => {
const lastUsedProvider = await loadFromStorage(LAST_USED_PROVIDER_KEY)
return lastUsedProvider || ''
}
let watcherInterval = null
const providerWatcherMware = (store: Store<GlobalState>) => (next: Function) => async (action: AnyAction) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
switch (action.type) {
case ADD_PROVIDER: {
const currentProviderProps = action.payload.toJS()
if (watcherInterval) {
clearInterval(watcherInterval)
}
saveToStorage(LAST_USED_PROVIDER_KEY, currentProviderProps.name)
watcherInterval = setInterval(async () => {
const web3 = getWeb3()
const providerInfo = await getProviderInfo(web3)
if (
currentProviderProps.account !== providerInfo.account
|| currentProviderProps.network !== providerInfo.network
) {
store.dispatch(fetchProvider(web3, () => {}, () => {}))
}
}, 2000)
break
}
case REMOVE_PROVIDER:
clearInterval(watcherInterval)
removeFromStorage(LAST_USED_PROVIDER_KEY)
break
default:
break
}
}
return handledAction
}
export default providerWatcherMware

View File

@ -2,6 +2,7 @@
import { handleActions, type ActionType } from 'redux-actions'
import { makeProvider, type Provider } from '~/logic/wallets/store/model/provider'
import addProvider, { ADD_PROVIDER } from '~/logic/wallets/store/actions/addProvider'
import { REMOVE_PROVIDER } from '~/logic/wallets/store/actions/removeProvider'
export const PROVIDER_REDUCER_ID = 'providers'
@ -10,6 +11,7 @@ export type State = Provider
export default handleActions<State, Function>(
{
[ADD_PROVIDER]: (state: State, { payload }: ActionType<typeof addProvider>) => makeProvider(payload),
[REMOVE_PROVIDER]: () => makeProvider(),
},
makeProvider(),
)

View File

@ -56,7 +56,7 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => {
setInitialLoad(false)
if (defaultSafe) {
return <Redirect to={`${SAFELIST_ADDRESS}/${defaultSafe}`} />
return <Redirect to={`${SAFELIST_ADDRESS}/${defaultSafe}/balances`} />
}
return <Redirect to={WELCOME_ADDRESS} />

View File

@ -15,7 +15,7 @@ import { type SelectorProps } from '~/routes/load/container/selector'
const getSteps = () => ['Name and address', 'Owners', 'Review']
type Props = SelectorProps & {
export type LayoutProps = SelectorProps & {
onLoadSafeSubmit: (values: Object) => Promise<void>,
}

View File

@ -18,6 +18,7 @@ import { shortVersionOf } from '~/logic/wallets/ethAddresses'
import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'
import { getOwnerNameBy, getOwnerAddressBy, getNumOwnersFrom } from '~/routes/open/components/fields'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS, THRESHOLD } from '~/routes/load/components/fields'
import type { LayoutProps } from '../Layout'
const styles = () => ({
root: {

View File

@ -47,7 +47,7 @@ class Load extends React.Component<Props> {
await loadSafe(safeName, safeAddress, owners, addSafe)
const url = `${SAFELIST_ADDRESS}/${safeAddress}`
const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances`
history.push(url)
} catch (error) {
console.error('Error while loading the Safe', error)

View File

@ -42,10 +42,9 @@ export const createSafe = async (values: Object, userAccount: string, addSafe: A
safeProps.owners = owners
addSafe(safeProps)
if (stillInOpeningView()) {
const url = {
pathname: `${SAFELIST_ADDRESS}/${safeContract.address}`,
pathname: `${SAFELIST_ADDRESS}/${safeContract.address}/balances`,
state: {
name,
tx: safe.tx,

View File

@ -72,34 +72,42 @@ const createTransaction = (
await tx
.send(sendParams)
.once('transactionHash', (hash) => {
.once('transactionHash', async (hash) => {
txHash = hash
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,
}
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)
try {
await saveTxToHistory(
safeInstance,
to,
valueInWei,
txData,
CALL,
nonce,
txHash,
from,
isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
)
} catch (err) {
console.error(err)
}
})
.on('error', (error) => {
console.error('Tx error: ', error)
})
.then(async (receipt) => {
.then((receipt) => {
closeSnackbar(pendingExecutionKey)
await saveTxToHistory(
safeInstance,
to,
valueInWei,
txData,
CALL,
nonce,
receipt.transactionHash,
from,
isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
)
if (isExecution) {
showSnackbar(notificationsQueue.afterExecution, enqueueSnackbar, closeSnackbar)
}

View File

@ -130,7 +130,11 @@ export const loadSafeTransactions = async (safeAddress: string) => {
}
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
const transactions: Map<string, List<Transaction>> = await loadSafeTransactions(safeAddress)
try {
const transactions: Map<string, List<Transaction>> = await loadSafeTransactions(safeAddress)
return dispatch(addTransactions(transactions))
return dispatch(addTransactions(transactions))
} catch (err) {
console.error(`Requests for transactions for ${safeAddress} failed with 404`)
}
}

View File

@ -26,7 +26,10 @@ 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
export const generateSignaturesFromTxConfirmations = (confirmations: List<Confirmation>, preApprovingOwner?: string) => {
export const generateSignaturesFromTxConfirmations = (
confirmations: List<Confirmation>,
preApprovingOwner?: string,
) => {
// The constant parts need to be sorted so that the recovered signers are sorted ascending
// (natural order) by address (not checksummed).
let confirmedAdresses = confirmations.map((conf) => conf.owner.address)
@ -101,7 +104,7 @@ const processTransaction = (
await transaction
.send(sendParams)
.once('transactionHash', (hash) => {
.once('transactionHash', async (hash) => {
txHash = hash
closeSnackbar(beforeExecutionKey)
const notification: Notification = {
@ -109,23 +112,29 @@ const processTransaction = (
options: notificationsQueue.pendingExecution.noMoreConfirmationsNeeded.options,
}
pendingExecutionKey = showSnackbar(notification, enqueueSnackbar, closeSnackbar)
try {
await saveTxToHistory(
safeInstance,
tx.recipient,
tx.value,
tx.data,
CALL,
nonce,
txHash,
from,
shouldExecute ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
)
} catch (err) {
console.error(err)
}
})
.on('error', (error) => {
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
.then((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)
dispatch(fetchTransactions(safeAddress))

View File

@ -6,6 +6,7 @@ import Heading from '~/components/layout/Heading'
import Img from '~/components/layout/Img'
import Button from '~/components/layout/Button'
import Link from '~/components/layout/Link'
import ConnectButton from '~/components/ConnectButton'
import { OPEN_ADDRESS, LOAD_ADDRESS } from '~/routes/routes'
import { marginButtonImg, secondary } from '~/theme/variables'
import styles from './Layout.scss'
@ -84,12 +85,23 @@ const Welcome = ({ provider }: Props) => (
<OpenInNew style={openIconStyle} />
</a>
</Heading>
<Block className={styles.safeActions} margin="md">
<CreateSafe size="large" provider={provider} />
</Block>
<Block className={styles.safeActions} margin="md">
<LoadSafe size="large" provider={provider} />
</Block>
{provider ? (
<>
<Block className={styles.safeActions} margin="md">
<CreateSafe size="large" provider={provider} />
</Block>
<Block className={styles.safeActions} margin="md">
<LoadSafe size="large" provider={provider} />
</Block>
</>
) : (
<Block margin="md" className={styles.connectWallet}>
<Heading tag="h3" align="center" margin="md">
Get Started by Connecting a Wallet
</Heading>
<ConnectButton minWidth={240} minHeight={42} />
</Block>
)}
</Block>
)

View File

@ -17,3 +17,7 @@
.learnMoreLink {
color: $secondary;
}
.connectWallet {
text-align: center;
}

View File

@ -7,6 +7,7 @@ import {
import thunk from 'redux-thunk'
import safe, { SAFE_REDUCER_ID, type SafeReducerState as SafeState } from '~/routes/safe/store/reducer/safe'
import safeStorage from '~/routes/safe/store/middleware/safeStorage'
import providerWatcher from '~/logic/wallets/store/middlewares/providerWatcher'
import transactions, {
type State as TransactionsState,
TRANSACTIONS_REDUCER_ID,
@ -22,7 +23,9 @@ export const history = createBrowserHistory()
// eslint-disable-next-line
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const finalCreateStore = composeEnhancers(applyMiddleware(thunk, routerMiddleware(history), safeStorage))
const finalCreateStore = composeEnhancers(
applyMiddleware(thunk, routerMiddleware(history), safeStorage, providerWatcher),
)
export type GlobalState = {
providers: ProviderState,
@ -45,8 +48,4 @@ const reducers: Reducer<GlobalState> = combineReducers({
export const store: Store<GlobalState> = createStore(reducers, finalCreateStore)
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(
reducers,
localState,
finalCreateStore,
)
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(reducers, localState, finalCreateStore)

View File

@ -102,7 +102,7 @@ const renderApp = (store: Store) => ({
export const renderSafeView = (store: Store<GlobalState>, address: string) => {
const app = renderApp(store)
const url = `${SAFELIST_ADDRESS}/${address}`
const url = `${SAFELIST_ADDRESS}/${address}/balances`
history.push(url)
return app
@ -115,7 +115,7 @@ export const whenSafeDeployed = (): Promise<string> => new Promise((resolve, rej
const interval = setInterval(() => {
if (times >= MAX_TIMES_EXECUTED) {
clearInterval(interval)
reject()
reject(new Error('Didn\'t load the safe'))
}
const url = `${window.location}`
console.log(url)

View File

@ -1,4 +1,5 @@
// @flow
/* eslint-disable max-classes-per-file */
import SafeRecord, { type Safe } from '~/routes/safe/store/models/safe'
import addSafe, { buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
import {
@ -74,7 +75,7 @@ export const aMinedSafe = async (
threshold: number = 1,
name: string = 'Safe Name',
): Promise<string> => {
const provider = await getProviderInfo()
const provider = await getProviderInfo(window.web3.currentProvider)
const walletRecord = makeProvider(provider)
store.dispatch(addProvider(walletRecord))

View File

@ -31,7 +31,7 @@ afterAll(() => {
})
const renderOpenSafeForm = async (localStore: Store<GlobalState>) => {
const provider = await getProviderInfo()
const provider = await getProviderInfo(window.web3.currentProvider)
const walletRecord = makeProvider(provider)
localStore.dispatch(addProvider(walletRecord))

View File

@ -2,7 +2,7 @@
import * as React from 'react'
import { type Store } from 'redux'
import { Provider } from 'react-redux'
import { render, fireEvent } from '@testing-library/react'
import { render, fireEvent, act } from '@testing-library/react'
import { ConnectedRouter } from 'connected-react-router'
import Load from '~/routes/load/container/Load'
import { aNewStore, history, type GlobalState } from '~/store'
@ -29,7 +29,7 @@ afterAll(() => {
})
const renderLoadSafe = async (localStore: Store<GlobalState>) => {
const provider = await getProviderInfo()
const provider = await getProviderInfo(window.web3.currentProvider)
const walletRecord = makeProvider(provider)
localStore.dispatch(addProvider(walletRecord))
@ -52,19 +52,18 @@ describe('DOM > Feature > LOAD a Safe', () => {
const safeAddressInput = LoadSafePage.getByPlaceholderText('Safe Address*')
// Fill Safe's name
fireEvent.change(safeNameInput, { target: { value: 'A Safe To Load' } })
fireEvent.change(safeAddressInput, { target: { value: address } })
await sleep(400)
// Click next
fireEvent.submit(form)
await sleep(400)
await act(async () => {
fireEvent.change(safeNameInput, { target: { value: 'A Safe To Load' } })
fireEvent.change(safeAddressInput, { target: { value: address } })
fireEvent.submit(form)
// submit form with owners names
fireEvent.submit(form)
await sleep(400)
await sleep(500)
fireEvent.submit(form)
await sleep(500)
fireEvent.submit(form)
})
// Submit
fireEvent.submit(form)
const deployedAddress = await whenSafeDeployed()
expect(deployedAddress).toBe(address)
})

View File

@ -40,6 +40,31 @@ import {
} from '~/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm'
import { REPLACE_OWNER_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
const originalError = console.error
beforeAll(() => {
console.error = (...args) => {
if (/Warning.*not wrapped in act/.test(args[0])) {
return
}
originalError.call(console, ...args)
}
})
afterAll(() => {
console.error = originalError
})
const travelToOwnerSettings = async (dom) => {
const settingsBtn = await waitForElement(() => dom.getByTestId(SETTINGS_TAB_BTN_TEST_ID))
fireEvent.click(settingsBtn)
// click on owners settings
const ownersSettingsBtn = await waitForElement(() => dom.getByTestId(OWNERS_SETTINGS_TAB_TEST_ID))
fireEvent.click(ownersSettingsBtn)
await sleep(500)
}
describe('DOM > Feature > Settings - Manage owners', () => {
let store
let safeAddress
@ -55,12 +80,7 @@ describe('DOM > Feature > Settings - Manage owners', () => {
await sleep(1300)
// Travel to settings
const settingsBtn = await waitForElement(() => SafeDom.getByTestId(SETTINGS_TAB_BTN_TEST_ID))
fireEvent.click(settingsBtn)
// click on owners settings
const ownersSettingsBtn = await waitForElement(() => SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TEST_ID))
fireEvent.click(ownersSettingsBtn)
await travelToOwnerSettings(SafeDom)
// open rename owner modal
const renameOwnerBtn = await waitForElement(() => SafeDom.getByTestId(RENAME_OWNER_BTN_TEST_ID))
@ -85,14 +105,7 @@ describe('DOM > Feature > Settings - Manage owners', () => {
await sleep(1300)
// Travel to settings
const settingsBtn = SafeDom.getByTestId(SETTINGS_TAB_BTN_TEST_ID)
fireEvent.click(settingsBtn)
await sleep(200)
// click on owners settings
const ownersSettingsBtn = SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TEST_ID)
fireEvent.click(ownersSettingsBtn)
await sleep(200)
await travelToOwnerSettings(SafeDom)
// check if there are 2 owners
let ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TEST_ID)
@ -118,6 +131,8 @@ describe('DOM > Feature > Settings - Manage owners', () => {
await sleep(1300)
// check if owner was removed
await travelToOwnerSettings(SafeDom)
ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TEST_ID)
expect(ownerRows.length).toBe(1)
expect(ownerRows[0]).toHaveTextContent('Adol 1 Eth Account')
@ -135,14 +150,7 @@ describe('DOM > Feature > Settings - Manage owners', () => {
await sleep(1300)
// Travel to settings
const settingsBtn = SafeDom.getByTestId(SETTINGS_TAB_BTN_TEST_ID)
fireEvent.click(settingsBtn)
await sleep(200)
// click on owners settings
const ownersSettingsBtn = SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TEST_ID)
fireEvent.click(ownersSettingsBtn)
await sleep(200)
await travelToOwnerSettings(SafeDom)
// check if there is 1 owner
let ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TEST_ID)
@ -169,13 +177,14 @@ describe('DOM > Feature > Settings - Manage owners', () => {
await sleep(1500)
// check if owner was added
await travelToOwnerSettings(SafeDom)
ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TEST_ID)
expect(ownerRows.length).toBe(2)
expect(ownerRows[0]).toHaveTextContent('Adol 1 Eth Account')
expect(ownerRows[0]).toHaveTextContent('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1')
expect(ownerRows[1]).toHaveTextContent(NEW_OWNER_NAME)
expect(ownerRows[1]).toHaveTextContent(NEW_OWNER_ADDRESS)
// Check that the transaction was registered
await checkRegisteredTxAddOwner(SafeDom, NEW_OWNER_ADDRESS)
})
@ -190,14 +199,7 @@ describe('DOM > Feature > Settings - Manage owners', () => {
await sleep(1300)
// Travel to settings
const settingsBtn = SafeDom.getByTestId(SETTINGS_TAB_BTN_TEST_ID)
fireEvent.click(settingsBtn)
await sleep(200)
// click on owners settings
const ownersSettingsBtn = SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TEST_ID)
fireEvent.click(ownersSettingsBtn)
await sleep(200)
await travelToOwnerSettings(SafeDom)
// check if there are 2 owners
let ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TEST_ID)
@ -225,6 +227,8 @@ describe('DOM > Feature > Settings - Manage owners', () => {
await sleep(1000)
// check if the owner was replaced
await travelToOwnerSettings(SafeDom)
ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TEST_ID)
expect(ownerRows.length).toBe(2)
expect(ownerRows[0]).toHaveTextContent('Adol 1 Eth Account')

View File

@ -4,7 +4,7 @@ import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { renderSafeView } from '~/test/builder/safe.dom.utils'
import '@testing-library/jest-dom/extend-expect'
import { TOGGLE_SIDEBAR_BTN_TESTID } from '~/components/Header/component/SafeListHeader'
import { TOGGLE_SIDEBAR_BTN_TESTID } from '~/components/Header/components/SafeListHeader'
import { SIDEBAR_SAFELIST_ROW_TESTID } from '~/components/Sidebar/SafeList'
import { sleep } from '~/utils/timer'

6377
yarn.lock

File diff suppressed because it is too large Load Diff