diff --git a/.gitignore b/.gitignore index a75dc3eb..21b594e6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ build_webpack/ build_storybook/ .DS_Store build/ -yarn-error.log \ No newline at end of file +yarn-error.log +.env.* \ No newline at end of file diff --git a/package.json b/package.json index 5bf88bb4..55f59de0 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,14 @@ "dependencies": { "@gnosis.pm/safe-contracts": "^1.0.0", "@gnosis.pm/util-contracts": "2.0.4", - "@material-ui/core": "4.5.0", - "@material-ui/icons": "4.4.3", - "@testing-library/jest-dom": "4.1.0", - "@welldone-software/why-did-you-render": "3.3.5", + "@material-ui/core": "4.5.1", + "@material-ui/icons": "4.5.1", + "@testing-library/jest-dom": "4.1.2", + "@welldone-software/why-did-you-render": "3.3.8", "axios": "0.19.0", "bignumber.js": "9.0.0", "connected-react-router": "6.5.2", - "date-fns": "2.4.1", + "date-fns": "2.5.0", "ethereum-ens": "0.7.8", "final-form": "4.18.5", "history": "4.10.1", @@ -54,7 +54,7 @@ "react-dom": "16.10.2", "react-final-form": "6.3.0", "react-final-form-listeners": "^1.0.2", - "react-hot-loader": "4.12.14", + "react-hot-loader": "4.12.15", "react-infinite-scroll-component": "4.5.3", "react-qr-reader": "^2.2.1", "react-redux": "7.1.1", @@ -68,8 +68,8 @@ "web3": "1.2.1" }, "devDependencies": { - "@babel/cli": "7.6.2", - "@babel/core": "7.6.2", + "@babel/cli": "7.6.4", + "@babel/core": "7.6.4", "@babel/plugin-proposal-class-properties": "7.5.5", "@babel/plugin-proposal-decorators": "7.6.0", "@babel/plugin-proposal-do-expressions": "7.6.0", @@ -89,16 +89,16 @@ "@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-env": "7.6.3", "@babel/preset-flow": "^7.0.0", - "@babel/preset-react": "^7.0.0", + "@babel/preset-react": "7.6.3", "@sambego/storybook-state": "^1.3.6", - "@storybook/addon-actions": "5.2.1", - "@storybook/addon-knobs": "5.2.1", - "@storybook/addon-links": "5.2.1", - "@storybook/react": "5.2.1", + "@storybook/addon-actions": "5.2.4", + "@storybook/addon-knobs": "5.2.4", + "@storybook/addon-links": "5.2.4", + "@storybook/react": "5.2.4", "@testing-library/react": "9.3.0", - "autoprefixer": "9.6.1", + "autoprefixer": "9.6.5", "babel-core": "^7.0.0-bridge.0", "babel-eslint": "10.0.3", "babel-jest": "24.9.0", @@ -113,9 +113,9 @@ "eslint-config-airbnb": "18.0.1", "eslint-plugin-flowtype": "4.3.0", "eslint-plugin-import": "2.18.2", - "eslint-plugin-jest": "22.17.0", + "eslint-plugin-jest": "22.19.0", "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.15.0", + "eslint-plugin-react": "7.16.0", "ethereumjs-abi": "0.6.8", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "4.2.0", @@ -136,13 +136,13 @@ "storybook-host": "5.1.0", "storybook-router": "^0.3.4", "style-loader": "1.0.0", - "truffle": "5.0.39", + "truffle": "5.0.40", "truffle-contract": "4.0.31", "truffle-solidity-loader": "0.1.32", "uglifyjs-webpack-plugin": "2.2.0", - "url-loader": "^2.1.0", - "webpack": "4.41.0", - "webpack-bundle-analyzer": "3.5.2", + "url-loader": "2.2.0", + "webpack": "4.41.2", + "webpack-bundle-analyzer": "3.6.0", "webpack-cli": "3.3.9", "webpack-dev-server": "3.8.2", "webpack-manifest-plugin": "2.2.0" diff --git a/src/components/Notifier/index.js b/src/components/Notifier/index.js index c4a9fd10..6ad573b5 100644 --- a/src/components/Notifier/index.js +++ b/src/components/Notifier/index.js @@ -1,14 +1,20 @@ // @flow -import React, { Component } from 'react' +import { Component } from 'react' +import { List } from 'immutable' import { connect } from 'react-redux' import { withSnackbar } from 'notistack' -import actions from './actions' +import { type Notification } from '~/logic/notifications/store/models/notification' +import actions, { type Actions } from './actions' import selector from './selector' -class Notifier extends Component { +type Props = Actions & { + notifications: List, +} + +class Notifier extends Component { displayed = [] - shouldComponentUpdate({ notifications: newSnacks = [] }) { + shouldComponentUpdate({ notifications: newSnacks = List() }) { const { notifications: currentSnacks, closeSnackbar, removeSnackbar } = this.props if (!newSnacks.size) { @@ -27,7 +33,7 @@ class Notifier extends Component { if (notExists) { continue } - notExists = notExists || !currentSnacks.filter(({ key }) => newSnack.key === key).length + notExists = notExists || !currentSnacks.filter(({ key }) => newSnack.key === key).size } return notExists } diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/index.jsx b/src/components/ScanQRModal/index.jsx similarity index 100% rename from src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/index.jsx rename to src/components/ScanQRModal/index.jsx diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/style.js b/src/components/ScanQRModal/style.js similarity index 100% rename from src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/style.js rename to src/components/ScanQRModal/style.js diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/utils.js b/src/components/ScanQRModal/utils.js similarity index 100% rename from src/routes/open/components/SafeOwnersConfirmationsForm/ScanQRModal/utils.js rename to src/components/ScanQRModal/utils.js diff --git a/src/components/forms/validator.js b/src/components/forms/validator.js index 26b33e35..462b21d5 100644 --- a/src/components/forms/validator.js +++ b/src/components/forms/validator.js @@ -50,8 +50,8 @@ export const minValue = (min: number) => (value: string) => { return `Should be at least ${min}` } -export const maxValue = (max: number) => (value: string) => { - if (Number.isNaN(Number(value)) || Number.parseInt(value, 10) <= Number(max)) { +export const maxValue = (max: number | string) => (value: string) => { + if (Number.isNaN(Number(value)) || parseFloat(value, 10) <= parseFloat(max, 10)) { return undefined } diff --git a/src/components/layout/Col/index.jsx b/src/components/layout/Col/index.jsx index f09f2cac..f51a1658 100644 --- a/src/components/layout/Col/index.jsx +++ b/src/components/layout/Col/index.jsx @@ -1,6 +1,6 @@ // @flow +import * as React from 'react' import classNames from 'classnames/bind' -import React from 'react' import { capitalize } from '~/utils/css' import styles from './index.scss' diff --git a/src/logic/contracts/safeContracts.js b/src/logic/contracts/safeContracts.js index 30a005ed..9f8e039a 100644 --- a/src/logic/contracts/safeContracts.js +++ b/src/logic/contracts/safeContracts.js @@ -2,6 +2,7 @@ import contract from 'truffle-contract' import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/ProxyFactory.json' 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 { getWeb3 } from '~/logic/wallets/getWeb3' import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions' @@ -99,3 +100,27 @@ export const getGnosisSafeInstanceAt = async (safeAddress: string) => { return gnosisSafe } + +const cleanByteCodeMetadata = (bytecode: string): string => { + const metaData = 'a165' + return bytecode.substring(0, bytecode.lastIndexOf(metaData)) +} + +export const validateProxy = async (safeAddress: string): boolean => { + // https://solidity.readthedocs.io/en/latest/metadata.html#usage-for-source-code-verification + const web3 = getWeb3() + const code = await web3.eth.getCode(safeAddress) + const codeWithoutMetadata = cleanByteCodeMetadata(code) + const supportedProxies = [SafeProxy] + for (let i = 0; i < supportedProxies.length; i += 1) { + const proxy = supportedProxies[i] + const proxyCode = proxy.deployedBytecode + const proxyCodeWithoutMetadata = cleanByteCodeMetadata(proxyCode) + if (codeWithoutMetadata === proxyCodeWithoutMetadata) { + return true + } + } + // Old PayingProxyCode + const oldProxyCode = '0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600' + return codeWithoutMetadata === oldProxyCode +} diff --git a/src/logic/notifications/notificationBuilder.js b/src/logic/notifications/notificationBuilder.js index 84b5b908..893d17b6 100644 --- a/src/logic/notifications/notificationBuilder.js +++ b/src/logic/notifications/notificationBuilder.js @@ -5,7 +5,7 @@ import { Close as IconClose } from '@material-ui/icons' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { type Notification, NOTIFICATIONS } from './notificationTypes' -type NotificationsQueue = { +export type NotificationsQueue = { beforeExecution: Notification, pendingExecution: { noMoreConfirmationsNeeded: Notification, @@ -104,7 +104,7 @@ const defaultNotificationsQueue: NotificationsQueue = { afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG, } -export const getNofiticationsFromTxType = (txType: string) => { +export const getNotificationsFromTxType = (txType: string) => { let notificationsQueue: NotificationsQueue switch (txType) { diff --git a/src/logic/notifications/store/actions/enqueueSnackbar.js b/src/logic/notifications/store/actions/enqueueSnackbar.js index e14abe0c..fb88da5c 100644 --- a/src/logic/notifications/store/actions/enqueueSnackbar.js +++ b/src/logic/notifications/store/actions/enqueueSnackbar.js @@ -1,6 +1,6 @@ // @flow import { createAction } from 'redux-actions' -import type { Dispatch as ReduxDispatch, GetState } from 'redux' +import type { Dispatch as ReduxDispatch } from 'redux' import { type GlobalState } from '~/store' import { type NotificationProps } from '~/logic/notifications/store/models/notification' @@ -10,7 +10,6 @@ const addSnackbar = createAction(ENQUEUE_SNACKBAR) const enqueueSnackbar = (notification: NotificationProps) => ( dispatch: ReduxDispatch, - getState: GetState, ) => { const newNotification = { ...notification, diff --git a/src/logic/notifications/store/reducer/notifications.js b/src/logic/notifications/store/reducer/notifications.js index c4ae1c2b..d16e0c0c 100644 --- a/src/logic/notifications/store/reducer/notifications.js +++ b/src/logic/notifications/store/reducer/notifications.js @@ -29,7 +29,5 @@ export default handleActions( return state.delete(key) }, }, - Map({ - notifications: Map(), - }), + Map(), ) diff --git a/src/logic/notifications/store/selectors/index.js b/src/logic/notifications/store/selectors/index.js index 2ae6eb96..bf6371fa 100644 --- a/src/logic/notifications/store/selectors/index.js +++ b/src/logic/notifications/store/selectors/index.js @@ -5,7 +5,7 @@ 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 = ( +const notificationsMapSelector = ( state: GlobalState, ): Map => state[NOTIFICATIONS_REDUCER_ID] diff --git a/src/logic/safe/transactions/gasNew.js b/src/logic/safe/transactions/gasNew.js new file mode 100644 index 00000000..a1aff18c --- /dev/null +++ b/src/logic/safe/transactions/gasNew.js @@ -0,0 +1,57 @@ +// @flow +import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' +import { type Transaction } from '~/routes/safe/store/models/transaction' +import { getWeb3, getAccountFrom } from '~/logic/wallets/getWeb3' +import { generateSignaturesFromTxConfirmations } from '~/routes/safe/store/actions/processTransaction' +import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions' +import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' +import { CALL } from '.' + +export const estimateTxGasCosts = async ( + safeAddress: string, + to: string, + data: string, + tx?: Transaction, + preApprovingOwner?: string, +): Promise => { + try { + const web3 = getWeb3() + const from = await getAccountFrom(web3) + const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi, safeAddress) + const nonce = await safeInstance.methods.nonce().call() + const threshold = await safeInstance.methods.getThreshold().call() + + const isExecution = (tx && tx.confirmations.size === threshold) || !!preApprovingOwner || threshold === '1' + + let txData + if (isExecution) { + // https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures + const signatures = tx && tx.confirmations + ? generateSignaturesFromTxConfirmations(tx.confirmations, preApprovingOwner) + : `0x000000000000000000000000${from.replace( + '0x', + '', + )}000000000000000000000000000000000000000000000000000000000000000001` + txData = await safeInstance.methods + .execTransaction(to, tx ? tx.value : 0, data, CALL, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, signatures) + .encodeABI() + } else { + const txHash = await safeInstance.methods + .getTransactionHash(to, tx ? tx.value : 0, data, CALL, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, nonce) + .call({ + from, + }) + txData = await safeInstance.methods.approveHash(txHash).encodeABI() + } + + const gas = await calculateGasOf(txData, from, safeAddress) + const gasPrice = await calculateGasPrice() + + return gas * parseInt(gasPrice, 10) + } catch (err) { + console.error('Error while estimating transaction execution gas costs:') + console.error(err) + + return 10000 + } +} diff --git a/src/logic/tokens/utils/alternativeAbi.js b/src/logic/tokens/utils/alternativeAbi.js new file mode 100644 index 00000000..c6a8abfb --- /dev/null +++ b/src/logic/tokens/utils/alternativeAbi.js @@ -0,0 +1,47 @@ +// @flow +// https://github.com/ethers-io/ethers.js/issues/527 + +export const ALTERNATIVE_TOKEN_ABI = [ + { + constant: true, + inputs: [], + name: 'name', + outputs: [ + { + name: '', + type: 'bytes32', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'symbol', + outputs: [ + { + name: '', + type: 'bytes32', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'decimals', + outputs: [ + { + name: '', + type: 'uint8', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, +] diff --git a/src/logic/tokens/utils/formatAmount.js b/src/logic/tokens/utils/formatAmount.js index 0751c8a5..6c4221af 100644 --- a/src/logic/tokens/utils/formatAmount.js +++ b/src/logic/tokens/utils/formatAmount.js @@ -17,7 +17,11 @@ const lt1000tFormatter = new Intl.NumberFormat([], { maximumFractionDigits: 3, n export const formatAmount = (number: string | number) => { let numberFloat = parseFloat(number) - if (numberFloat < 1000) { + if (numberFloat === 0) { + numberFloat = '0.000' + } else if (numberFloat < 0.001) { + numberFloat = '< 0.001' + } else if (numberFloat < 1000) { numberFloat = lt1kFormatter.format(numberFloat) } else if (numberFloat < 10000) { numberFloat = lt10kFormatter.format(numberFloat) diff --git a/src/logic/wallets/ethTransactions.js b/src/logic/wallets/ethTransactions.js index 7ee0cf33..f8673ad6 100644 --- a/src/logic/wallets/ethTransactions.js +++ b/src/logic/wallets/ethTransactions.js @@ -56,6 +56,6 @@ export const calculateGasOf = async (data: Object, from: string, to: string) => return gas * 2 } catch (err) { - return Promise.reject(new Error(err)) + return Promise.reject(err) } } diff --git a/src/logic/wallets/getWeb3.js b/src/logic/wallets/getWeb3.js index 051b3fa0..1ca597ec 100644 --- a/src/logic/wallets/getWeb3.js +++ b/src/logic/wallets/getWeb3.js @@ -64,7 +64,7 @@ const getProviderName: Function = (web3Provider): string => { return name } -const getAccountFrom: Function = async (web3Provider): Promise => { +export const getAccountFrom: Function = async (web3Provider): Promise => { const accounts = await web3Provider.eth.getAccounts() if (process.env.NODE_ENV === 'test' && window.testAccountIndex) { diff --git a/src/routes/index.js b/src/routes/index.js index 79bd4845..7df2c155 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -64,7 +64,7 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => { /> - + diff --git a/src/routes/load/components/DetailsForm/index.jsx b/src/routes/load/components/DetailsForm/index.jsx index da0f8fd3..8c0d2374 100644 --- a/src/routes/load/components/DetailsForm/index.jsx +++ b/src/routes/load/components/DetailsForm/index.jsx @@ -1,7 +1,6 @@ // @flow import * as React from 'react' import { withStyles } from '@material-ui/core/styles' -import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/Proxy.json' import InputAdornment from '@material-ui/core/InputAdornment' import CheckCircle from '@material-ui/icons/CheckCircle' import Field from '~/components/forms/Field' @@ -15,7 +14,7 @@ import Paragraph from '~/components/layout/Paragraph' import OpenPaper from '~/components/Stepper/OpenPaper' import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields' import { getWeb3 } from '~/logic/wallets/getWeb3' -import { getSafeMasterContract } from '~/logic/contracts/safeContracts' +import { getSafeMasterContract, validateProxy } from '~/logic/contracts/safeContracts' import { secondary } from '~/theme/variables' type Props = { @@ -56,15 +55,8 @@ export const safeFieldsValidation = async (values: Object) => { return errors } - // https://solidity.readthedocs.io/en/latest/metadata.html#usage-for-source-code-verification - const metaData = 'a165' - - const code = await web3.eth.getCode(safeAddress) - const codeWithoutMetadata = code.substring(0, code.lastIndexOf(metaData)) - const proxyCode = SafeProxy.deployedBytecode - const proxyCodeWithoutMetadata = proxyCode.substring(0, proxyCode.lastIndexOf(metaData)) - const safeInstance = codeWithoutMetadata === proxyCodeWithoutMetadata - if (!safeInstance) { + const isValidProxy = await validateProxy(safeAddress) + if (!isValidProxy) { errors[FIELD_LOAD_ADDRESS] = SAFE_INSTANCE_ERROR return errors } diff --git a/src/routes/open/components/ReviewInformation/index.jsx b/src/routes/open/components/ReviewInformation/index.jsx index bcfadb71..bef00275 100644 --- a/src/routes/open/components/ReviewInformation/index.jsx +++ b/src/routes/open/components/ReviewInformation/index.jsx @@ -12,6 +12,7 @@ import OpenPaper from '~/components/Stepper/OpenPaper' import Col from '~/components/layout/Col' import Row from '~/components/layout/Row' import Paragraph from '~/components/layout/Paragraph' +import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { sm, md, lg, border, background, } from '~/theme/variables' @@ -73,7 +74,7 @@ type Props = { } const ReviewComponent = ({ values, classes, userAccount }: Props) => { - const [gasCosts, setGasCosts] = useState('0.00') + const [gasCosts, setGasCosts] = useState('< 0.001') const names = getNamesFrom(values) const addresses = getAccountsFrom(values) const numOwners = getNumOwnersFrom(values) @@ -85,9 +86,9 @@ const ReviewComponent = ({ values, classes, userAccount }: Props) => { const { fromWei, toBN } = web3.utils const estimatedGasCosts = await estimateGasForDeployingSafe(addresses, numOwners, userAccount) const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') - const roundedGasCosts = parseFloat(gasCostsAsEth).toFixed(3) + const formattedGasCosts = formatAmount(gasCostsAsEth) if (isCurrent) { - setGasCosts(roundedGasCosts) + setGasCosts(formattedGasCosts) } } diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/index.jsx b/src/routes/open/components/SafeOwnersConfirmationsForm/index.jsx index 54b6ac95..d1ac6441 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/index.jsx +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/index.jsx @@ -28,7 +28,7 @@ import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor' import Hairline from '~/components/layout/Hairline' import trash from '~/assets/icons/trash.svg' import QRIcon from '~/assets/icons/qrcode.svg' -import ScanQRModal from './ScanQRModal' +import ScanQRModal from '~/components/ScanQRModal' import { getAddressValidator } from './validators' import { styles } from './style' diff --git a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.jsx b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.jsx index 03084b4d..697db2c9 100644 --- a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.jsx @@ -1,23 +1,17 @@ // @flow import React from 'react' -import OpenInNew from '@material-ui/icons/OpenInNew' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Row from '~/components/layout/Row' import Col from '~/components/layout/Col' import Paragraph from '~/components/layout/Paragraph' -import Link from '~/components/layout/Link' +import EtherscanBtn from '~/components/EtherscanBtn' +import CopyBtn from '~/components/CopyBtn' import Bold from '~/components/layout/Bold' import Block from '~/components/layout/Block' import Identicon from '~/components/Identicon' -import { copyToClipboard } from '~/utils/clipboard' -import { secondary, xs, border } from '~/theme/variables' +import { xs, border } from '~/theme/variables' -const openIconStyle = { - height: '16px', - color: secondary, -} - -const styles = () => ({ +const useStyles = makeStyles({ balanceContainer: { fontSize: '12px', lineHeight: 1.08, @@ -28,20 +22,22 @@ const styles = () => ({ marginTop: xs, borderRadius: '3px', }, + address: { + marginRight: xs, + }, }) type Props = { - classes: Object, safeAddress: string, - etherScanLink: string, safeName: string, ethBalance: string, } const SafeInfo = (props: Props) => { const { - safeAddress, safeName, etherScanLink, ethBalance, classes, + safeAddress, safeName, ethBalance, } = props + const classes = useStyles() return ( @@ -52,21 +48,18 @@ const SafeInfo = (props: Props) => { {safeName} - - {safeAddress} - - - - + + + {safeAddress} + + + + Balance: {' '} - - {ethBalance} - {' '} - ETH - + {`${ethBalance} ETH`} @@ -74,4 +67,4 @@ const SafeInfo = (props: Props) => { ) } -export default withStyles(styles)(SafeInfo) +export default SafeInfo diff --git a/src/routes/safe/components/Balances/SendModal/index.jsx b/src/routes/safe/components/Balances/SendModal/index.jsx index 347f7bc7..9526da23 100644 --- a/src/routes/safe/components/Balances/SendModal/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/index.jsx @@ -24,7 +24,6 @@ type Props = { classes: Object, isOpen: boolean, safeAddress: string, - etherScanLink: string, safeName: string, ethBalance: string, tokens: List, @@ -65,7 +64,6 @@ const Send = ({ isOpen, classes, safeAddress, - etherScanLink, safeName, ethBalance, tokens, @@ -113,7 +111,6 @@ const Send = ({ )} {activeScreen === 'sendCustomTx' && ( { + const [gasCosts, setGasCosts] = useState('< 0.001') + + useEffect(() => { + let isCurrent = true + const estimateGas = async () => { + const web3 = getWeb3() + const { fromWei, toBN } = web3.utils + + const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.recipientAddress, tx.data.trim()) + const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') + const formattedGasCosts = formatAmount(gasCostsAsEth) + if (isCurrent) { + setGasCosts(formattedGasCosts) + } + } + + estimateGas() + + return () => { + isCurrent = false + } + }, []) + const submitTx = async () => { const web3 = getWeb3() const txRecipient = tx.recipientAddress - const txData = tx.data + const txData = tx.data.trim() const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0 createTransaction( @@ -87,12 +103,7 @@ const ReviewCustomTx = ({ - + Arrow Down @@ -111,12 +122,13 @@ const ReviewCustomTx = ({ - - {tx.recipientAddress} - - - - + + + {tx.recipientAddress} + + + + @@ -125,7 +137,7 @@ const ReviewCustomTx = ({ - Ether + Ether {tx.value || 0} {' ETH'} @@ -143,6 +155,11 @@ const ReviewCustomTx = ({ + + + {`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`} + + @@ -165,5 +182,4 @@ const ReviewCustomTx = ({ ) } - export default withStyles(styles)(withSnackbar(ReviewCustomTx)) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.jsx index 93b58c18..d0449fb0 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.jsx @@ -1,30 +1,32 @@ // @flow -import React from 'react' +import React, { useEffect, useState } from 'react' +import { List } from 'immutable' import { BigNumber } from 'bignumber.js' -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 { withSnackbar } from 'notistack' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' -import Link from '~/components/layout/Link' import Col from '~/components/layout/Col' import Button from '~/components/layout/Button' import Img from '~/components/layout/Img' import Block from '~/components/layout/Block' +import EtherscanBtn from '~/components/EtherscanBtn' +import CopyBtn from '~/components/CopyBtn' import Identicon from '~/components/Identicon' -import { copyToClipboard } from '~/utils/clipboard' import Hairline from '~/components/layout/Hairline' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils' import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens' +import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' +import { type Token } from '~/logic/tokens/store/model/token' +import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { getWeb3 } from '~/logic/wallets/getWeb3' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' +import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers' import ArrowDown from '../assets/arrow-down.svg' -import { secondary } from '~/theme/variables' -import { isEther } from '~/logic/tokens/utils/tokenHelpers' import { styles } from './style' type Props = { @@ -32,43 +34,70 @@ type Props = { setActiveScreen: Function, classes: Object, safeAddress: string, - etherScanLink: string, safeName: string, ethBalance: string, tx: Object, + tokens: List, createTransaction: Function, enqueueSnackbar: Function, closeSnackbar: Function, } -const openIconStyle = { - height: '16px', - color: secondary, -} - const ReviewTx = ({ onClose, setActiveScreen, classes, safeAddress, - etherScanLink, safeName, ethBalance, tx, + tokens, createTransaction, enqueueSnackbar, closeSnackbar, }: Props) => { + const [gasCosts, setGasCosts] = useState('< 0.001') + const txToken = tokens.find((token) => token.address === tx.token) + const isSendingETH = txToken.address === ETH_ADDRESS + const txRecipient = isSendingETH ? tx.recipientAddress : txToken.address + + useEffect(() => { + let isCurrent = true + const estimateGas = async () => { + const web3 = getWeb3() + const { fromWei, toBN } = web3.utils + let txData = EMPTY_DATA + + if (!isSendingETH) { + const StandardToken = await getHumanFriendlyToken() + const tokenInstance = await StandardToken.at(txToken.address) + + txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, 0).encodeABI() + } + + const estimatedGasCosts = await estimateTxGasCosts(safeAddress, txRecipient, txData) + const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') + const formattedGasCosts = formatAmount(gasCostsAsEth) + if (isCurrent) { + setGasCosts(formattedGasCosts) + } + } + + estimateGas() + + return () => { + isCurrent = false + } + }, []) + 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 HumanFriendlyToken = await getHumanFriendlyToken() - const tokenInstance = await HumanFriendlyToken.at(tx.token.address) + const tokenInstance = await HumanFriendlyToken.at(txToken.address) const decimals = await tokenInstance.decimals() txAmount = new BigNumber(tx.amount).times(10 ** decimals.toNumber()).toString() @@ -104,12 +133,7 @@ const ReviewTx = ({ - + Arrow Down @@ -128,12 +152,13 @@ const ReviewTx = ({ - - {tx.recipientAddress} - - - - + + + {tx.recipientAddress} + + + + @@ -142,11 +167,16 @@ const ReviewTx = ({ - {tx.token.name} + {txToken.name} {tx.amount} {' '} - {tx.token.symbol} + {txToken.symbol} + + + + + {`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`} diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.js b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.js index 6b44b167..8341a286 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.js +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.js @@ -29,6 +29,9 @@ export const styles = () => ({ amount: { marginLeft: sm, }, + address: { + marginRight: sm, + }, buttonRow: { height: '84px', justifyContent: 'center', diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/index.jsx index b36d895e..c03a8d93 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/index.jsx @@ -1,5 +1,5 @@ // @flow -import React from 'react' +import React, { useState } from 'react' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import InputAdornment from '@material-ui/core/InputAdornment' @@ -10,19 +10,19 @@ import GnoForm from '~/components/forms/GnoForm' import AddressInput from '~/components/forms/AddressInput' import Col from '~/components/layout/Col' import Button from '~/components/layout/Button' +import ScanQRModal from '~/components/ScanQRModal' import Block from '~/components/layout/Block' +import Img from '~/components/layout/Img' import Hairline from '~/components/layout/Hairline' import ButtonLink from '~/components/layout/ButtonLink' import Field from '~/components/forms/Field' import TextField from '~/components/forms/TextField' import TextareaField from '~/components/forms/TextareaField' import { - composeValidators, - mustBeFloat, - maxValue, - mustBeEthereumContractAddress, + composeValidators, mustBeFloat, maxValue, mustBeEthereumContractAddress, } from '~/components/forms/validator' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' +import QRIcon from '~/assets/icons/qrcode.svg' import ArrowDown from '../assets/arrow-down.svg' import { styles } from './style' @@ -30,7 +30,6 @@ type Props = { onClose: () => void, classes: Object, safeAddress: string, - etherScanLink: string, safeName: string, ethBalance: string, onSubmit: Function, @@ -41,18 +40,26 @@ const SendCustomTx = ({ classes, onClose, safeAddress, - etherScanLink, safeName, ethBalance, onSubmit, initialValues, }: Props) => { + const [qrModalOpen, setQrModalOpen] = useState(false) const handleSubmit = (values: Object) => { if (values.data || values.value) { onSubmit(values) } } + const openQrModal = () => { + setQrModalOpen(true) + } + + const closeQrModal = () => { + setQrModalOpen(false) + } + const formMutators = { setMax: (args, state, utils) => { utils.changeValue(state, 'value', () => ethBalance) @@ -78,15 +85,21 @@ const SendCustomTx = ({ {(...args) => { const mutators = args[3] + const handleScan = (value) => { + let scannedAddress = value + + if (scannedAddress.startsWith('ethereum:')) { + scannedAddress = scannedAddress.replace('ethereum:', '') + } + + mutators.setRecipient(scannedAddress) + closeQrModal() + } + return ( <> - + Arrow Down @@ -96,7 +109,7 @@ const SendCustomTx = ({ - + + + Scan QR { + openQrModal() + }} + /> + @@ -124,10 +149,7 @@ const SendCustomTx = ({ name="value" component={TextField} type="text" - validate={composeValidators( - mustBeFloat, - maxValue(ethBalance), - )} + validate={composeValidators(mustBeFloat, maxValue(ethBalance))} placeholder="Value*" text="Value*" className={classes.addressInput} @@ -164,6 +186,7 @@ const SendCustomTx = ({ Review + {qrModalOpen && } ) }} diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/style.js b/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/style.js index 96a1149e..38f2a9e5 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/style.js +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/style.js @@ -21,6 +21,9 @@ export const styles = () => ({ height: '35px', width: '35px', }, + qrCodeBtn: { + cursor: 'pointer', + }, formContainer: { padding: `${md} ${lg}`, }, diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.jsx index 61264c67..b7d17e78 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.jsx @@ -1,5 +1,5 @@ // @flow -import React, { useEffect, useState } from 'react' +import React from 'react' import { List } from 'immutable' import { withStyles } from '@material-ui/core/styles' import MenuItem from '@material-ui/core/MenuItem' @@ -22,63 +22,57 @@ type SelectFieldProps = { } type SelectedTokenProps = { - token?: Token, + tokenAddress?: string, classes: Object, + tokens: List, } -const SelectedToken = ({ token, classes }: SelectedTokenProps) => ( - - {token ? ( - <> - - {token.name} - - - - ) : ( - - Select an asset* - - )} - -) - -const SelectedTokenStyled = withStyles(selectedTokenStyles)(SelectedToken) - -type InitialTokenType = Token | string - -const TokenSelectField = ({ tokens, classes, initialValue }: SelectFieldProps) => { - const [initialToken, setInitialToken] = useState('') - - useEffect(() => { - const selectedToken = tokens.find((token) => token.name === initialValue) - setInitialToken(selectedToken || '') - }, [initialValue]) +const SelectedToken = ({ tokenAddress, tokens, classes }: SelectedTokenProps) => { + const token = tokens.find(({ address }) => address === tokenAddress) return ( - } - initialValue={initialToken} - displayEmpty - > - {tokens.map((token) => ( - - + + {token ? ( + <> + {token.name} - - - ))} - + + + ) : ( + + Select an asset* + + )} + ) } +const SelectedTokenStyled = withStyles(selectedTokenStyles)(SelectedToken) + +const TokenSelectField = ({ tokens, classes, initialValue }: SelectFieldProps) => ( + } + initialValue={initialValue} + displayEmpty + > + {tokens.map((token) => ( + + + {token.name} + + + + ))} + +) + export default withStyles(selectStyles)(TokenSelectField) diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.jsx index 0d342252..3414ca22 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.jsx @@ -1,5 +1,5 @@ // @flow -import React from 'react' +import React, { useState } from 'react' import { List } from 'immutable' import { withStyles } from '@material-ui/core/styles' import { OnChange } from 'react-final-form-listeners' @@ -13,6 +13,7 @@ import AddressInput from '~/components/forms/AddressInput' import Col from '~/components/layout/Col' import Button from '~/components/layout/Button' import Block from '~/components/layout/Block' +import Img from '~/components/layout/Img' import Hairline from '~/components/layout/Hairline' import ButtonLink from '~/components/layout/ButtonLink' import Field from '~/components/forms/Field' @@ -23,14 +24,15 @@ import { } from '~/components/forms/validator' import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' +import ScanQRModal from '~/components/ScanQRModal' import ArrowDown from '../assets/arrow-down.svg' +import QRIcon from '~/assets/icons/qrcode.svg' import { styles } from './style' type Props = { onClose: () => void, classes: Object, safeAddress: string, - etherScanLink: string, safeName: string, ethBalance: string, selectedToken: string, @@ -39,11 +41,22 @@ type Props = { initialValues: Object, } +const formMutators = { + setMax: (args, state, utils) => { + utils.changeValue(state, 'amount', () => args[0]) + }, + onTokenChange: (args, state, utils) => { + utils.changeValue(state, 'amount', () => '') + }, + setRecipient: (args, state, utils) => { + utils.changeValue(state, 'recipientAddress', () => args[0]) + }, +} + const SendFunds = ({ classes, onClose, safeAddress, - etherScanLink, safeName, ethBalance, tokens, @@ -51,22 +64,18 @@ const SendFunds = ({ initialValues, onSubmit, }: Props) => { + const [qrModalOpen, setQrModalOpen] = useState(false) + const handleSubmit = (values) => { onSubmit(values) } - const formMutators = { - setMax: (args, state, utils) => { - const { token } = state.formState.values + const openQrModal = () => { + setQrModalOpen(true) + } - utils.changeValue(state, 'amount', () => token && token.balance) - }, - onTokenChange: (args, state, utils) => { - utils.changeValue(state, 'amount', () => '') - }, - setRecipient: (args, state, utils) => { - utils.changeValue(state, 'recipientAddress', () => args[0]) - }, + const closeQrModal = () => { + setQrModalOpen(false) } return ( @@ -85,16 +94,24 @@ const SendFunds = ({ {(...args) => { const formState = args[2] const mutators = args[3] - const { token } = formState.values + const { token: tokenAddress } = formState.values + const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress) + + const handleScan = (value) => { + let scannedAddress = value + + if (scannedAddress.startsWith('ethereum:')) { + scannedAddress = scannedAddress.replace('ethereum:', '') + } + + mutators.setRecipient(scannedAddress) + closeQrModal() + } + return ( <> - + Arrow Down @@ -104,7 +121,7 @@ const SendFunds = ({ - + + + Scan QR { + openQrModal() + }} + /> + @@ -125,7 +154,7 @@ const SendFunds = ({ Amount - + mutators.setMax(selectedTokenRecord.balance)}> Send max @@ -140,14 +169,14 @@ const SendFunds = ({ required, mustBeFloat, greaterThan(0), - maxValue(token && token.balance), + maxValue(selectedTokenRecord && selectedTokenRecord.balance), )} placeholder="Amount*" text="Amount*" className={classes.addressInput} inputAdornment={ - token && { - endAdornment: {token.symbol}, + selectedTokenRecord && { + endAdornment: {selectedTokenRecord.symbol}, } } /> @@ -175,6 +204,7 @@ const SendFunds = ({ Review + {qrModalOpen && } ) }} diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.js b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.js index 31d3c991..85ddeac7 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.js +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.js @@ -21,6 +21,9 @@ export const styles = () => ({ height: '35px', width: '35px', }, + qrCodeBtn: { + cursor: 'pointer', + }, formContainer: { padding: `${md} ${lg}`, }, diff --git a/src/routes/safe/components/Balances/dataFetcher.js b/src/routes/safe/components/Balances/dataFetcher.js index 11b5c509..af3a146c 100644 --- a/src/routes/safe/components/Balances/dataFetcher.js +++ b/src/routes/safe/components/Balances/dataFetcher.js @@ -18,7 +18,7 @@ export type BalanceRow = SortRow export const getBalanceData = (activeTokens: List): List => { const rows = activeTokens.map((token: Token) => ({ - [BALANCE_TABLE_ASSET_ID]: { name: token.name, logoUri: token.logoUri }, + [BALANCE_TABLE_ASSET_ID]: { name: token.name, logoUri: token.logoUri, address: token.address }, [buildOrderFieldFrom(BALANCE_TABLE_ASSET_ID)]: token.name, [BALANCE_TABLE_BALANCE_ID]: `${formatAmount(token.balance)} ${token.symbol}`, [buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: Number(token.balance), diff --git a/src/routes/safe/components/Balances/index.jsx b/src/routes/safe/components/Balances/index.jsx index 2f0a5a88..575f9e8a 100644 --- a/src/routes/safe/components/Balances/index.jsx +++ b/src/routes/safe/components/Balances/index.jsx @@ -43,7 +43,6 @@ type Props = { activeTokens: List, safeAddress: string, safeName: string, - etherScanLink: string, ethBalance: string, createTransaction: Function, } @@ -72,11 +71,11 @@ class Balances extends React.Component { this.setState(() => ({ [`show${action}`]: false })) } - showSendFunds = (token: Token) => { + showSendFunds = (tokenAddress: string) => { this.setState({ sendFunds: { isOpen: true, - selectedToken: token, + selectedToken: tokenAddress, }, }) } @@ -107,7 +106,6 @@ class Balances extends React.Component { safeAddress, activeTokens, safeName, - etherScanLink, ethBalance, createTransaction, } = this.props @@ -176,7 +174,7 @@ class Balances extends React.Component { size="small" color="primary" className={classes.send} - onClick={() => this.showSendFunds(row.asset.name)} + onClick={() => this.showSendFunds(row.asset.address)} testId="balance-send-btn" > @@ -201,7 +199,6 @@ class Balances extends React.Component { { - constructor(props) { - super(props) - this.state = { - tabIndex: 0, - } +const Layout = (props: Props) => { + const { + safe, + provider, + network, + classes, + granted, + tokens, + activeTokens, + createTransaction, + processTransaction, + fetchTransactions, + updateSafe, + transactions, + userAddress, + sendFunds, + showReceive, + onShow, + onHide, + showSendFunds, + hideSendFunds, + match, + location, + } = props + + const handleCallToRouter = (_, value) => { + const { history } = props + + history.push(value) } - handleChange = (event, tabIndex) => { - this.setState({ tabIndex }) + if (!safe) { + return } - copyAddress = () => { - const { safe } = this.props + const { address, ethBalance, name } = safe + const etherScanLink = getEtherScanLink('address', address) - if (safe.address) { - copyToClipboard(safe.address) - } - } - - render() { - const { - safe, - provider, - network, - classes, - granted, - tokens, - activeTokens, - createTransaction, - processTransaction, - fetchTransactions, - updateSafe, - transactions, - userAddress, - sendFunds, - showReceive, - onShow, - onHide, - showSendFunds, - hideSendFunds, - } = this.props - const { tabIndex } = this.state - - if (!safe) { - return - } - - const { address, ethBalance, name } = safe - const etherScanLink = getEtherScanLink('address', address) - - return ( - <> - - - - - - {name} - - {!granted && Read Only} - - - - {address} - - - - - - - - - - + return ( + <> + + + + + + {name} + + {!granted && Read Only} + + + + {address} + + + - - - - - - - - - {tabIndex === 0 && ( - - )} - {tabIndex === 1 && ( - - )} - {tabIndex === 2 && ( - - )} - - + + + + + + + + - - - - ) - } + + + + + + + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + + + + + + ) } -export default withStyles(styles)(Layout) +export default withStyles(styles)(withRouter(Layout)) diff --git a/src/routes/safe/components/Settings/ChangeSafeName/index.jsx b/src/routes/safe/components/Settings/ChangeSafeName/index.jsx index 764d7994..ec5a969e 100644 --- a/src/routes/safe/components/Settings/ChangeSafeName/index.jsx +++ b/src/routes/safe/components/Settings/ChangeSafeName/index.jsx @@ -12,7 +12,7 @@ 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 { getNotificationsFromTxType, showSnackbar } from '~/logic/notifications' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { styles } from './style' @@ -36,7 +36,7 @@ const ChangeSafeName = (props: Props) => { const handleSubmit = (values) => { updateSafe({ address: safeAddress, name: values.safeName }) - const notification = getNofiticationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX) + const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX) showSnackbar(notification.afterExecution, enqueueSnackbar, closeSnackbar) } diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx index ca2c0acd..7da3a0a2 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx @@ -16,6 +16,7 @@ const styles = () => ({ width: '775px', minHeight: '500px', position: 'static', + height: 'auto', }, }) @@ -146,6 +147,7 @@ const AddOwner = ({ safeName={safeName} owners={owners} values={values} + safeAddress={safeAddress} onClickBack={onClickBack} onSubmit={onAddOwner} /> diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx index 920e852f..8fc42b58 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx @@ -1,13 +1,14 @@ // @flow -import React from 'react' +import React, { useState, useEffect } from 'react' import { List } from 'immutable' import classNames from 'classnames' import { withStyles } from '@material-ui/core/styles' -import OpenInNew from '@material-ui/icons/OpenInNew' import Close from '@material-ui/icons/Close' import IconButton from '@material-ui/core/IconButton' import Identicon from '~/components/Identicon' -import Link from '~/components/layout/Link' +import EtherscanBtn from '~/components/EtherscanBtn' +import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' +import CopyBtn from '~/components/CopyBtn' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' import Col from '~/components/layout/Col' @@ -15,17 +16,13 @@ import Button from '~/components/layout/Button' import Block from '~/components/layout/Block' import Hairline from '~/components/layout/Hairline' import type { Owner } from '~/routes/safe/store/models/owner' -import { getEtherScanLink } from '~/logic/wallets/getWeb3' -import { secondary } from '~/theme/variables' +import { getWeb3 } from '~/logic/wallets/getWeb3' +import { formatAmount } from '~/logic/tokens/utils/formatAmount' +import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { styles } from './style' export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn' -const openIconStyle = { - height: '16px', - color: secondary, -} - type Props = { onClose: () => void, classes: Object, @@ -34,11 +31,39 @@ type Props = { values: Object, onClickBack: Function, onSubmit: Function, + safeAddress: string, } const ReviewAddOwner = ({ - classes, onClose, safeName, owners, values, onClickBack, onSubmit, + classes, onClose, safeName, owners, values, onClickBack, onSubmit, safeAddress, }: Props) => { + const [gasCosts, setGasCosts] = useState('< 0.001') + useEffect(() => { + let isCurrent = true + const estimateGas = async () => { + const web3 = getWeb3() + const { fromWei, toBN } = web3.utils + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + + const txData = safeInstance.contract.methods + .addOwnerWithThreshold(values.ownerAddress, values.threshold) + .encodeABI() + const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData) + + const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') + const formattedGasCosts = formatAmount(gasCostsAsEth) + if (isCurrent) { + setGasCosts(formattedGasCosts) + } + } + + estimateGas() + + return () => { + isCurrent = false + } + }, []) + const handleSubmit = () => { onSubmit() } @@ -76,12 +101,7 @@ const ReviewAddOwner = ({ Any transaction requires the confirmation of: - {values.threshold} - {' '} - out of - {owners.size + 1} - {' '} - owner(s) + {`${values.threshold} out of ${owners.size + 1} owner(s)`} @@ -89,9 +109,7 @@ const ReviewAddOwner = ({ - {owners.size + 1} - {' '} - Safe owner(s) + {`${owners.size + 1} Safe owner(s)`} @@ -107,12 +125,11 @@ const ReviewAddOwner = ({ {owner.name} - + {owner.address} - - - + + @@ -136,16 +153,11 @@ const ReviewAddOwner = ({ {values.ownerName} - + {values.ownerAddress} - - - + + @@ -155,6 +167,14 @@ const ReviewAddOwner = ({ + + + You're about to create a transaction and will have to confirm it with your currently connected wallet. +
+ {`Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`} +
+
+ - diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx index d7db66d1..027e1ba2 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx @@ -16,6 +16,7 @@ const styles = () => ({ width: '775px', minHeight: '500px', position: 'static', + height: 'auto', }, }) @@ -168,6 +169,7 @@ const RemoveOwner = ({ ownerName={ownerName} onClickBack={onClickBack} onSubmit={onRemoveOwner} + safeAddress={safeAddress} /> )} diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.jsx index 58096ae1..93e6871b 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.jsx @@ -4,26 +4,19 @@ import classNames from 'classnames/bind' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import IconButton from '@material-ui/core/IconButton' -import OpenInNew from '@material-ui/icons/OpenInNew' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' import Col from '~/components/layout/Col' import Button from '~/components/layout/Button' import Block from '~/components/layout/Block' import Hairline from '~/components/layout/Hairline' -import Link from '~/components/layout/Link' +import EtherscanBtn from '~/components/EtherscanBtn' +import CopyBtn from '~/components/CopyBtn' import Identicon from '~/components/Identicon' -import { getEtherScanLink } from '~/logic/wallets/getWeb3' import { styles } from './style' -import { secondary } from '~/theme/variables' export const REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID = 'remove-owner-next-btn' -const openIconStyle = { - height: '16px', - color: secondary, -} - type Props = { onClose: () => void, classes: Object, @@ -65,12 +58,11 @@ const CheckOwner = ({ {ownerName} - + {ownerAddress} - - - + + diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/style.js b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/style.js index 2086f27f..eb685f20 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/style.js +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/style.js @@ -19,6 +19,9 @@ export const styles = () => ({ manage: { fontSize: '24px', }, + address: { + marginRight: sm, + }, closeIcon: { height: '35px', width: '35px', diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx index 6d61189a..7bd38d2e 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx @@ -1,13 +1,13 @@ // @flow -import React from 'react' +import React, { useState, useEffect } from 'react' import { List } from 'immutable' import classNames from 'classnames' import { withStyles } from '@material-ui/core/styles' -import OpenInNew from '@material-ui/icons/OpenInNew' import Close from '@material-ui/icons/Close' import IconButton from '@material-ui/core/IconButton' +import EtherscanBtn from '~/components/EtherscanBtn' +import CopyBtn from '~/components/CopyBtn' import Identicon from '~/components/Identicon' -import Link from '~/components/layout/Link' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' import Col from '~/components/layout/Col' @@ -15,17 +15,14 @@ import Button from '~/components/layout/Button' import Block from '~/components/layout/Block' import Hairline from '~/components/layout/Hairline' import type { Owner } from '~/routes/safe/store/models/owner' -import { getEtherScanLink } from '~/logic/wallets/getWeb3' -import { secondary } from '~/theme/variables' +import { getWeb3 } from '~/logic/wallets/getWeb3' +import { formatAmount } from '~/logic/tokens/utils/formatAmount' +import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts' +import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { styles } from './style' export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn' -const openIconStyle = { - height: '16px', - color: secondary, -} - type Props = { onClose: () => void, classes: Object, @@ -36,6 +33,7 @@ type Props = { ownerName: string, onClickBack: Function, onSubmit: Function, + safeAddress: string, } const ReviewRemoveOwner = ({ @@ -48,10 +46,36 @@ const ReviewRemoveOwner = ({ ownerName, onClickBack, onSubmit, + safeAddress, }: Props) => { - const handleSubmit = () => { - onSubmit() - } + const [gasCosts, setGasCosts] = useState('< 0.001') + + useEffect(() => { + let isCurrent = true + + const estimateGas = async () => { + const web3 = getWeb3() + const { fromWei, toBN } = web3.utils + + const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) + const safeOwners = await gnosisSafe.getOwners() + const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase()) + const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] + const txData = gnosisSafe.contract.methods.removeOwner(prevAddress, ownerAddress, values.threshold).encodeABI() + const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData) + const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') + const formattedGasCosts = formatAmount(gasCostsAsEth) + + if (isCurrent) { + setGasCosts(formattedGasCosts) + } + } + + estimateGas() + return () => { + isCurrent = false + } + }, []) return ( <> @@ -87,12 +111,7 @@ const ReviewRemoveOwner = ({ Any transaction requires the confirmation of: - {values.threshold} - {' '} - out of - {owners.size - 1} - {' '} - owner(s) + {`${values.threshold} out of ${owners.size - 1} owner(s)`} @@ -100,9 +119,7 @@ const ReviewRemoveOwner = ({ - {owners.size - 1} - {' '} - Safe owner(s) + {`${owners.size - 1} Safe owner(s)`} @@ -119,16 +136,11 @@ const ReviewRemoveOwner = ({ {owner.name} - + {owner.address} - - - + + @@ -153,16 +165,11 @@ const ReviewRemoveOwner = ({ {ownerName} - + {ownerAddress} - - - + + @@ -172,13 +179,21 @@ const ReviewRemoveOwner = ({ + + + You're about to create a transaction and will have to confirm it with your currently connected wallet. +
+ {`Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`} +
+
+