diff --git a/package.json b/package.json index 3b840efa..13f26c75 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "react-dom": "^16.8.6", "react-final-form": "6.3.0", "react-final-form-listeners": "^1.0.2", - "react-hot-loader": "4.11.1", + "react-hot-loader": "4.12.3", "react-infinite-scroll-component": "^4.5.2", "react-redux": "7.1.0", "react-router-dom": "^4.3.1", @@ -61,9 +61,9 @@ "web3": "1.0.0-beta.37" }, "devDependencies": { - "@babel/cli": "7.4.4", - "@babel/core": "7.4.5", - "@babel/plugin-proposal-class-properties": "7.4.4", + "@babel/cli": "7.5.0", + "@babel/core": "7.5.0", + "@babel/plugin-proposal-class-properties": "7.5.0", "@babel/plugin-proposal-decorators": "7.4.4", "@babel/plugin-proposal-do-expressions": "^7.0.0", "@babel/plugin-proposal-export-default-from": "^7.0.0", @@ -82,7 +82,7 @@ "@babel/plugin-transform-member-expression-literals": "^7.2.0", "@babel/plugin-transform-property-literals": "^7.2.0", "@babel/polyfill": "7.4.4", - "@babel/preset-env": "7.4.5", + "@babel/preset-env": "7.5.0", "@babel/preset-flow": "^7.0.0-beta.40", "@babel/preset-react": "^7.0.0-beta.40", "@sambego/storybook-state": "^1.0.7", @@ -106,14 +106,14 @@ "eslint-config-airbnb": "^17.1.0", "eslint-plugin-flowtype": "3.11.1", "eslint-plugin-import": "2.18.0", - "eslint-plugin-jest": "22.7.1", + "eslint-plugin-jest": "22.7.2", "eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-react": "7.14.2", "ethereumjs-abi": "^0.6.7", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "4.0.0", "flow-bin": "0.102.0", - "fs-extra": "8.0.1", + "fs-extra": "8.1.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.0.4", "jest": "24.8.0", @@ -129,11 +129,11 @@ "storybook-host": "^5.0.3", "storybook-router": "^0.3.3", "style-loader": "^0.23.1", - "truffle": "5.0.24", - "truffle-contract": "4.0.21", - "truffle-solidity-loader": "0.1.23", + "truffle": "5.0.26", + "truffle-contract": "4.0.23", + "truffle-solidity-loader": "0.1.25", "uglifyjs-webpack-plugin": "2.1.3", - "webpack": "4.35.0", + "webpack": "4.35.2", "webpack-bundle-analyzer": "3.3.2", "webpack-cli": "3.3.5", "webpack-dev-server": "3.7.2", diff --git a/src/components/Footer/index.scss b/src/components/Footer/index.scss index 91887ad4..0f3b3eba 100644 --- a/src/components/Footer/index.scss +++ b/src/components/Footer/index.scss @@ -7,6 +7,7 @@ align-items: center; border: solid 0.5px $border; background-color: white; + margin-top: 50px; } @media only screen and (max-width: $(screenXs)px) { diff --git a/src/components/Table/index.jsx b/src/components/Table/index.jsx index 0cb2219d..d9d2fdcc 100644 --- a/src/components/Table/index.jsx +++ b/src/components/Table/index.jsx @@ -2,12 +2,12 @@ import * as React from 'react' import classNames from 'classnames' import { List } from 'immutable' -import Row from '~/components/layout/Row' import Table from '@material-ui/core/Table' import TableBody from '@material-ui/core/TableBody' import { withStyles } from '@material-ui/core/styles' import CircularProgress from '@material-ui/core/CircularProgress' import TablePagination from '@material-ui/core/TablePagination' +import Row from '~/components/layout/Row' import { type Order, stableSort, getSorting } from '~/components/Table/sorting' import TableHead, { type Column } from '~/components/Table/TableHead' import { xl } from '~/theme/variables' @@ -21,6 +21,7 @@ type Props = { children: Function, size: number, defaultFixed?: boolean, + noBorder: boolean, } type State = { @@ -99,7 +100,7 @@ class GnoTable extends React.Component, State> { render() { const { - data, label, columns, classes, children, size, defaultOrderBy, defaultFixed, + data, label, columns, classes, children, size, defaultOrderBy, defaultFixed, noBorder, } = this.props const { order, orderBy, page, orderProp, rowsPerPage, fixed, @@ -117,7 +118,7 @@ class GnoTable extends React.Component, State> { const paginationClasses = { selectRoot: classes.selectRoot, - root: classes.paginationRoot, + root: !noBorder && classes.paginationRoot, input: classes.white, } @@ -132,13 +133,16 @@ class GnoTable extends React.Component, State> { return ( {!isEmpty && ( - +
{children(sortedData)}
)} {isEmpty && ( - + )} diff --git a/src/components/forms/validator.js b/src/components/forms/validator.js index d885bc59..de0f30ff 100644 --- a/src/components/forms/validator.js +++ b/src/components/forms/validator.js @@ -2,9 +2,21 @@ import { type FieldValidator } from 'final-form' import { getWeb3 } from '~/logic/wallets/getWeb3' +export const simpleMemoize = (fn: Function) => { + let lastArg + let lastResult + return (arg: any) => { + if (arg !== lastArg) { + lastArg = arg + lastResult = fn(arg) + } + return lastResult + } +} + type Field = boolean | string | null | typeof undefined -export const required = (value: Field) => (value ? undefined : 'Required') +export const required = simpleMemoize((value: Field) => (value ? undefined : 'Required')) export const mustBeInteger = (value: string) => (!Number.isInteger(Number(value)) || value.includes('.') ? 'Must be an integer' : undefined) @@ -46,17 +58,17 @@ export const maxValue = (max: number) => (value: string) => { export const ok = () => undefined -export const mustBeEthereumAddress = (address: Field) => { +export const mustBeEthereumAddress = simpleMemoize((address: Field) => { const isAddress: boolean = getWeb3().utils.isAddress(address) return isAddress ? undefined : 'Address should be a valid Ethereum address' -} +}) export const minMaxLength = (minLen: string | number, maxLen: string | number) => (value: string) => (value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`) export const ADDRESS_REPEATED_ERROR = 'Address already introduced' -export const uniqueAddress = (addresses: string[]) => (value: string) => (addresses.includes(value) ? ADDRESS_REPEATED_ERROR : undefined) +export const uniqueAddress = (addresses: string[]) => simpleMemoize((value: string) => (addresses.includes(value) ? ADDRESS_REPEATED_ERROR : undefined)) export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field) => validators.reduce((error, validator) => error || validator(value), undefined) diff --git a/src/components/layout/Img/index.jsx b/src/components/layout/Img/index.jsx index dd5886ff..94dbd5f4 100644 --- a/src/components/layout/Img/index.jsx +++ b/src/components/layout/Img/index.jsx @@ -10,15 +10,16 @@ type Props = { fullwidth?: boolean, bordered?: boolean, className?: string, - style?: React.Node, + style?: Object, + testId?: string, } const Img = ({ - fullwidth, alt, bordered, className, style, ...props + fullwidth, alt, bordered, className, style, testId = '', ...props }: Props) => { const classes = cx(styles.img, { fullwidth, bordered }, className) - return {alt} + return {alt} } export default Img diff --git a/src/components/layout/Row/index.jsx b/src/components/layout/Row/index.jsx index 0d4433ae..1bb6e855 100644 --- a/src/components/layout/Row/index.jsx +++ b/src/components/layout/Row/index.jsx @@ -12,10 +12,11 @@ type Props = { margin?: 'xs' | 'sm' | 'md' | 'lg' | 'xl', align?: 'center' | 'end' | 'start', grow?: boolean, + testId?: string, } const Row = ({ - children, className, margin, align, grow, ...props + children, className, margin, align, grow, testId = '', ...props }: Props) => { const rowClassNames = cx( styles.row, @@ -26,7 +27,7 @@ const Row = ({ ) return ( -
+
{children}
) diff --git a/src/components/layout/Table/index.jsx b/src/components/layout/Table/index.jsx index 886e9ee4..7c21f556 100644 --- a/src/components/layout/Table/index.jsx +++ b/src/components/layout/Table/index.jsx @@ -20,7 +20,7 @@ const buildWidthFrom = (size: number) => ({ }) const overflowStyle = { - overflowX: 'scroll', + overflowX: 'auto', } // see: https://css-tricks.com/responsive-data-tables/ diff --git a/src/logic/contracts/safeContracts.js b/src/logic/contracts/safeContracts.js index cc4cab95..d90d8366 100644 --- a/src/logic/contracts/safeContracts.js +++ b/src/logic/contracts/safeContracts.js @@ -1,12 +1,14 @@ // @flow import contract from 'truffle-contract' -import { ensureOnce } from '~/utils/singleton' -import { getWeb3 } from '~/logic/wallets/getWeb3' import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/ProxyFactory.json' +import { ensureOnce } from '~/utils/singleton' +import { getWeb3 } from '~/logic/wallets/getWeb3' import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions' import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' +export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001' + let proxyFactoryMaster let safeMaster diff --git a/src/logic/safe/utils/safeStorage.js b/src/logic/safe/utils/safeStorage.js index 3a5ad40e..88681021 100644 --- a/src/logic/safe/utils/safeStorage.js +++ b/src/logic/safe/utils/safeStorage.js @@ -1,6 +1,6 @@ // @flow -import { type Owner } from '~/routes/safe/store/models/owner' import { List, Map } from 'immutable' +import { type Owner } from '~/routes/safe/store/models/owner' import { loadFromStorage, saveToStorage, removeFromStorage } from '~/utils/storage' export const SAFES_KEY = 'SAFES' @@ -28,7 +28,7 @@ export const saveSafes = async (safes: Object) => { export const setOwners = async (safeAddress: string, owners: List) => { try { - const ownersAsMap = Map(owners.map((owner: Owner) => [owner.get('address').toLowerCase(), owner.get('name')])) + const ownersAsMap = Map(owners.map((owner: Owner) => [owner.address.toLowerCase(), owner.name])) await saveToStorage(`${OWNERS_KEY}-${safeAddress}`, ownersAsMap) } catch (err) { // eslint-disable-next-line @@ -42,7 +42,7 @@ export const getOwners = async (safeAddress: string): Map => { return data ? Map(data) : Map() } -export const removeOwners = async (safeAddress: string): Map => { +export const removeOwners = async (safeAddress: string) => { try { await removeFromStorage(`${OWNERS_KEY}-${safeAddress}`) } catch (err) { diff --git a/src/logic/tokens/store/actions/removeToken.js b/src/logic/tokens/store/actions/removeToken.js index 40059789..0dedb20a 100644 --- a/src/logic/tokens/store/actions/removeToken.js +++ b/src/logic/tokens/store/actions/removeToken.js @@ -1,9 +1,9 @@ // @flow import { createAction } from 'redux-actions' +import type { Dispatch as ReduxDispatch } from 'redux' import { type Token } from '~/logic/tokens/store/model/token' import { removeTokenFromStorage, removeFromActiveTokens } from '~/logic/tokens/utils/tokensStorage' import { type GlobalState } from '~/store/index' -import type { Dispatch as ReduxDispatch } from 'redux' export const REMOVE_TOKEN = 'REMOVE_TOKEN' diff --git a/src/logic/wallets/tokens.js b/src/logic/wallets/tokens.js index 890ef840..b573f250 100644 --- a/src/logic/wallets/tokens.js +++ b/src/logic/wallets/tokens.js @@ -1,6 +1,6 @@ // @flow -import { getWeb3 } from '~/logic/wallets/getWeb3' import { BigNumber } from 'bignumber.js' +import { getWeb3 } from '~/logic/wallets/getWeb3' export const toNative = (amt: string | number | BigNumber, decimal: number): BigNumber => { const web3 = getWeb3() diff --git a/src/routes/load/components/Layout.jsx b/src/routes/load/components/Layout.jsx index cdbf5ade..0fa0a213 100644 --- a/src/routes/load/components/Layout.jsx +++ b/src/routes/load/components/Layout.jsx @@ -1,11 +1,11 @@ // @flow import * as React from 'react' import ChevronLeft from '@material-ui/icons/ChevronLeft' +import IconButton from '@material-ui/core/IconButton' import Stepper from '~/components/Stepper' import Block from '~/components/layout/Block' import Heading from '~/components/layout/Heading' import Row from '~/components/layout/Row' -import IconButton from '@material-ui/core/IconButton' import ReviewInformation from '~/routes/load/components/ReviewInformation' import OwnerList from '~/routes/load/components/OwnerList' import DetailsForm, { safeFieldsValidation } from '~/routes/load/components/DetailsForm' diff --git a/src/routes/open/components/ReviewInformation/index.jsx b/src/routes/open/components/ReviewInformation/index.jsx index 36b424d8..704a1498 100644 --- a/src/routes/open/components/ReviewInformation/index.jsx +++ b/src/routes/open/components/ReviewInformation/index.jsx @@ -1,10 +1,10 @@ // @flow import * as React from 'react' import classNames from 'classnames' -import { getNamesFrom, getAccountsFrom } from '~/routes/open/utils/safeDataExtractor' -import Block from '~/components/layout/Block' import { withStyles } from '@material-ui/core/styles' import OpenInNew from '@material-ui/icons/OpenInNew' +import { getNamesFrom, getAccountsFrom } from '~/routes/open/utils/safeDataExtractor' +import Block from '~/components/layout/Block' import Identicon from '~/components/Identicon' import OpenPaper from '~/components/Stepper/OpenPaper' import Col from '~/components/layout/Col' diff --git a/src/routes/open/components/SafeOwnersForm/index.jsx b/src/routes/open/components/SafeOwnersForm/index.jsx index a76d156f..cec64ddc 100644 --- a/src/routes/open/components/SafeOwnersForm/index.jsx +++ b/src/routes/open/components/SafeOwnersForm/index.jsx @@ -1,6 +1,8 @@ // @flow import * as React from 'react' import { withStyles } from '@material-ui/core/styles' +import InputAdornment from '@material-ui/core/InputAdornment' +import CheckCircle from '@material-ui/icons/CheckCircle' import Field from '~/components/forms/Field' import TextField from '~/components/forms/TextField' import { @@ -15,8 +17,6 @@ import Button from '~/components/layout/Button' import Row from '~/components/layout/Row' import Img from '~/components/layout/Img' import Col from '~/components/layout/Col' -import InputAdornment from '@material-ui/core/InputAdornment' -import CheckCircle from '@material-ui/icons/CheckCircle' import { getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields' import Paragraph from '~/components/layout/Paragraph' import OpenPaper from '~/components/Stepper/OpenPaper' @@ -74,8 +74,11 @@ const styles = () => ({ }) const getAddressValidators = (addresses: string[], position: number) => { + // thanks Rich Harris + // https://twitter.com/Rich_Harris/status/1125850391155965952 const copy = addresses.slice() - copy.splice(position, 1) + copy[position] = copy[copy.length - 1] + copy.pop() return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy)) } @@ -97,7 +100,7 @@ export const calculateValuesAfterRemoving = (index: number, notRemovedOwners: nu return initialValues } -class SafeOwners extends React.Component { +class SafeOwners extends React.PureComponent { state = { numOwners: 1, } diff --git a/src/routes/safe/components/AddOwner/AddOwnerForm/index.jsx b/src/routes/safe/components/AddOwner/AddOwnerForm/index.jsx deleted file mode 100644 index b756f2d3..00000000 --- a/src/routes/safe/components/AddOwner/AddOwnerForm/index.jsx +++ /dev/null @@ -1,70 +0,0 @@ -// @flow -import * as React from 'react' -import Field from '~/components/forms/Field' -import OpenPaper from '~/components/Stepper/OpenPaper' -import TextField from '~/components/forms/TextField' -import Checkbox from '~/components/forms/Checkbox' -import { - composeValidators, required, mustBeEthereumAddress, uniqueAddress, -} from '~/components/forms/validator' -import Block from '~/components/layout/Block' -import Heading from '~/components/layout/Heading' - -export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners' - -export const NAME_PARAM = 'name' -export const OWNER_ADDRESS_PARAM = 'ownerAddress' -export const INCREASE_PARAM = 'increase' - -export const safeFieldsValidation = (values: Object) => { - const errors = {} - - if (Number.parseInt(values.owners, 10) < Number.parseInt(values.confirmations, 10)) { - errors.confirmations = CONFIRMATIONS_ERROR - } - - return errors -} - -type Props = { - numOwners: number, - threshold: number, - addresses: string[], -} - -const AddOwnerForm = ({ addresses, numOwners, threshold }: Props) => (controls: React.Node) => ( - - - Add Owner - - - {`Actual number of owners: ${numOwners}, with threshold: ${threshold}`} - - - - - - - - - - Increase threshold? - - -) - -export default AddOwnerForm diff --git a/src/routes/safe/components/AddOwner/Review/index.jsx b/src/routes/safe/components/AddOwner/Review/index.jsx deleted file mode 100644 index 4ce516ad..00000000 --- a/src/routes/safe/components/AddOwner/Review/index.jsx +++ /dev/null @@ -1,48 +0,0 @@ -// @flow -import * as React from 'react' -import CircularProgress from '@material-ui/core/CircularProgress' -import OpenPaper from '~/components/Stepper/OpenPaper' -import Block from '~/components/layout/Block' -import Bold from '~/components/layout/Bold' -import Heading from '~/components/layout/Heading' -import Paragraph from '~/components/layout/Paragraph' -import { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from '~/routes/safe/components/AddOwner/AddOwnerForm' - -type FormProps = { - values: Object, - submitting: boolean, -} - -const spinnerStyle = { - minHeight: '50px', -} - -const Review = () => (controls: React.Node, { values, submitting }: FormProps) => { - const text = values[INCREASE_PARAM] - ? 'This operation will increase the threshold of the safe' - : 'This operation will not modify the threshold of the safe' - - return ( - - Review the Add Owner operation - - Owner Name: - {' '} - {values[NAME_PARAM]} - - - Owner Address: - {' '} - {values[OWNER_ADDRESS_PARAM]} - - - {text} - - - { submitting && } - - - ) -} - -export default Review diff --git a/src/routes/safe/components/AddOwner/actions.js b/src/routes/safe/components/AddOwner/actions.js deleted file mode 100644 index 32f51f38..00000000 --- a/src/routes/safe/components/AddOwner/actions.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' - -type FetchTransactions = typeof fetchTransactions - -export type Actions = { - fetchTransactions: FetchTransactions, -} - -export default { - fetchTransactions, -} diff --git a/src/routes/safe/components/AddOwner/index.jsx b/src/routes/safe/components/AddOwner/index.jsx deleted file mode 100644 index 920f056e..00000000 --- a/src/routes/safe/components/AddOwner/index.jsx +++ /dev/null @@ -1,105 +0,0 @@ -// @flow -import * as React from 'react' -import { List } from 'immutable' -import Stepper from '~/components/Stepper' -import { connect } from 'react-redux' -import { type Safe } from '~/routes/safe/store/models/safe' -import { type Owner, makeOwner } from '~/routes/safe/store/models/owner' -import { setOwners } from '~/logic/safe/utils' -import { createTransaction } from '~/logic/safe/safeFrontendOperations' -import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' -import AddOwnerForm, { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from './AddOwnerForm' -import Review from './Review' -import selector, { type SelectorProps } from './selector' -import actions, { type Actions } from './actions' - -const getSteps = () => ['Fill Owner Form', 'Review Add order operation'] - -type Props = SelectorProps & - Actions & { - safe: Safe, - threshold: number, - } - -type State = { - done: boolean, -} - -export const ADD_OWNER_RESET_BUTTON_TEXT = 'RESET' - -const getOwnerAddressesFrom = (owners: List) => { - if (!owners) { - return [] - } - - return owners.map((owner: Owner) => owner.get('address')) -} - -export const addOwner = async (values: Object, safe: Safe, threshold: number, executor: string) => { - const safeAddress = safe.get('address') - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) - const nonce = await gnosisSafe.nonce() - - const newThreshold = values[INCREASE_PARAM] ? threshold + 1 : threshold - const newOwnerAddress = values[OWNER_ADDRESS_PARAM] - const newOwnerName = values[NAME_PARAM] - - const data = gnosisSafe.contract.methods.addOwnerWithThreshold(newOwnerAddress, newThreshold).encodeABI() - await createTransaction(safe, `Add Owner ${newOwnerName}`, safeAddress, '0', nonce, executor, data) - setOwners(safeAddress, safe.get('owners').push(makeOwner({ name: newOwnerName, address: newOwnerAddress }))) -} - -class AddOwner extends React.Component { - state = { - done: false, - } - - onAddOwner = async (values: Object) => { - try { - const { - safe, threshold, userAddress, fetchTransactions, - } = this.props - await addOwner(values, safe, threshold, userAddress) - fetchTransactions(safe.get('address')) - this.setState({ done: true }) - } catch (error) { - this.setState({ done: false }) - // eslint-disable-next-line - console.log('Error while adding owner ' + error) - } - } - - onReset = () => { - this.setState({ done: false }) - } - - render() { - const { safe } = this.props - const { done } = this.state - const steps = getSteps() - const finishedButton = - const addresses = getOwnerAddressesFrom(safe.get('owners')) - - return ( - - - - {AddOwnerForm} - - {Review} - - - ) - } -} - -export default connect( - selector, - actions, -)(AddOwner) diff --git a/src/routes/safe/components/AddOwner/selector.js b/src/routes/safe/components/AddOwner/selector.js deleted file mode 100644 index cefe3460..00000000 --- a/src/routes/safe/components/AddOwner/selector.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import { createStructuredSelector } from 'reselect' -import { userAccountSelector } from '~/logic/wallets/store/selectors' - -export type SelectorProps = { - userAddress: userAccountSelector, -} - -export default createStructuredSelector({ - userAddress: userAccountSelector, -}) diff --git a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.js b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.js index 07feaadb..d9db666d 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.js +++ b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.js @@ -3,20 +3,9 @@ import { List } from 'immutable' import { getWeb3 } from '~/logic/wallets/getWeb3' import { type Token } from '~/logic/tokens/store/model/token' import { sameAddress } from '~/logic/wallets/ethAddresses' +import { simpleMemoize } from '~/components/forms/validator' // import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens' -export const simpleMemoize = (fn: Function) => { - let lastArg - let lastResult - return (arg: any) => { - if (arg !== lastArg) { - lastArg = arg - lastResult = fn(arg) - } - return lastResult - } -} - // eslint-disable-next-line export const addressIsTokenContract = simpleMemoize(async (tokenAddress: string) => { // SECOND APPROACH: diff --git a/src/routes/safe/components/Layout.jsx b/src/routes/safe/components/Layout.jsx index cf95ec53..b107e09c 100644 --- a/src/routes/safe/components/Layout.jsx +++ b/src/routes/safe/components/Layout.jsx @@ -103,6 +103,7 @@ class Layout extends React.Component { activeTokens, createTransaction, updateSafe, + userAddress, } = this.props const { tabIndex } = this.state @@ -163,6 +164,8 @@ class Layout extends React.Component { updateSafe={updateSafe} threshold={safe.threshold} owners={safe.owners} + network={network} + userAddress={userAddress} createTransaction={createTransaction} /> )} diff --git a/src/routes/safe/components/RemoveOwner/RemoveOwnerForm/index.jsx b/src/routes/safe/components/RemoveOwner/RemoveOwnerForm/index.jsx deleted file mode 100644 index 5f1a61a4..00000000 --- a/src/routes/safe/components/RemoveOwner/RemoveOwnerForm/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -// @flow -import * as React from 'react' -import Field from '~/components/forms/Field' -import SnackbarContent from '~/components/SnackbarContent' -import OpenPaper from '~/components/Stepper/OpenPaper' -import Checkbox from '~/components/forms/Checkbox' -import Block from '~/components/layout/Block' -import Heading from '~/components/layout/Heading' - -export const DECREASE_PARAM = 'decrease' - -type Props = { - numOwners: number, - threshold: number, - name: string, - disabled: boolean, - pendingTransactions: boolean, -} - -const RemoveOwnerForm = ({ - numOwners, threshold, name, disabled, pendingTransactions, -}: Props) => ( - controls: React.Node, -) => ( - - - Remove Owner - {' '} - {!!name && name} - - - {`Actual number of owners: ${numOwners}, threhsold of safe: ${threshold}`} - - {pendingTransactions && ( - - )} - - - - {disabled && '(disabled) '} - Decrease threshold? - - - -) - -export default RemoveOwnerForm diff --git a/src/routes/safe/components/RemoveOwner/Review/index.jsx b/src/routes/safe/components/RemoveOwner/Review/index.jsx deleted file mode 100644 index f9e35c2d..00000000 --- a/src/routes/safe/components/RemoveOwner/Review/index.jsx +++ /dev/null @@ -1,47 +0,0 @@ -// @flow -import * as React from 'react' -import CircularProgress from '@material-ui/core/CircularProgress' -import Block from '~/components/layout/Block' -import OpenPaper from '~/components/Stepper/OpenPaper' -import Bold from '~/components/layout/Bold' -import Heading from '~/components/layout/Heading' -import Paragraph from '~/components/layout/Paragraph' -import { DECREASE_PARAM } from '~/routes/safe/components/RemoveOwner/RemoveOwnerForm' - -type Props = { - name: string, -} - -type FormProps = { - values: Object, - submitting: boolean, -} - -const spinnerStyle = { - minHeight: '50px', -} - -const Review = ({ name }: Props) => (controls: React.Node, { values, submitting }: FormProps) => { - const text = values[DECREASE_PARAM] - ? 'This operation will decrease the threshold of the safe' - : 'This operation will not modify the threshold of the safe' - - return ( - - Review the Remove Owner operation - - Owner Name: - {' '} - {name} - - - {text} - - - { submitting && } - - - ) -} - -export default Review diff --git a/src/routes/safe/components/RemoveOwner/actions.js b/src/routes/safe/components/RemoveOwner/actions.js deleted file mode 100644 index 32f51f38..00000000 --- a/src/routes/safe/components/RemoveOwner/actions.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' - -type FetchTransactions = typeof fetchTransactions - -export type Actions = { - fetchTransactions: FetchTransactions, -} - -export default { - fetchTransactions, -} diff --git a/src/routes/safe/components/RemoveOwner/index.jsx b/src/routes/safe/components/RemoveOwner/index.jsx deleted file mode 100644 index d82592ab..00000000 --- a/src/routes/safe/components/RemoveOwner/index.jsx +++ /dev/null @@ -1,121 +0,0 @@ -// @flow -import * as React from 'react' -import Stepper from '~/components/Stepper' -import { connect } from 'react-redux' -import { type Safe } from '~/routes/safe/store/models/safe' -import { createTransaction } from '~/logic/safe/safeFrontendOperations' -import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' -import RemoveOwnerForm, { DECREASE_PARAM } from './RemoveOwnerForm' -import Review from './Review' -import selector, { type SelectorProps } from './selector' -import actions, { type Actions } from './actions' - -const getSteps = () => [ - 'Fill Owner Form', 'Review Remove order operation', -] - -type Props = SelectorProps & Actions & { - safe: Safe, - threshold: number, - name: string, - userToRemove: string, -} - -type State = { - done: boolean, -} - -const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001' -export const REMOVE_OWNER_RESET_BUTTON_TEXT = 'RESET' - -export const initialValuesFrom = (decreaseMandatory: boolean = false) => ({ - [DECREASE_PARAM]: decreaseMandatory, -}) - -export const shouldDecrease = (numOwners: number, threshold: number) => threshold === numOwners - -export const removeOwner = async ( - values: Object, - safe: Safe, - threshold: number, - userToRemove: string, - name: string, - executor: string, -) => { - const safeAddress = safe.get('address') - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) - const nonce = await gnosisSafe.nonce() - const newThreshold = values[DECREASE_PARAM] ? threshold - 1 : threshold - const storedOwners = await gnosisSafe.getOwners() - const index = storedOwners.findIndex(ownerAddress => ownerAddress === userToRemove) - const prevAddress = index === 0 ? SENTINEL_ADDRESS : storedOwners[index - 1] - const data = gnosisSafe.contract.removeOwner(prevAddress, userToRemove, newThreshold).encodeABI() - const text = name || userToRemove - - return createTransaction(safe, `Remove Owner ${text}`, safeAddress, '0', nonce, executor, data) -} - -class RemoveOwner extends React.Component { - state = { - done: false, - } - - onRemoveOwner = async (values: Object) => { - try { - const { - safe, threshold, executor, fetchTransactions, userToRemove, name, - } = this.props - await removeOwner(values, safe, threshold, userToRemove, name, executor) - fetchTransactions(safe.get('address')) - this.setState({ done: true }) - } catch (error) { - this.setState({ done: false }) - // eslint-disable-next-line - console.log('Error while adding owner ' + error) - } - } - - onReset = () => { - this.setState({ done: false }) - } - - render() { - const { safe, name, pendingTransactions } = this.props - const { done } = this.state - const steps = getSteps() - const numOwners = safe.get('owners').count() - const threshold = safe.get('threshold') - const finishedButton = - const decrease = shouldDecrease(numOwners, threshold) - const initialValues = initialValuesFrom(decrease) - const disabled = decrease || threshold === 1 - - return ( - - - - { RemoveOwnerForm } - - - { Review } - - - - ) - } -} - -export default connect(selector, actions)(RemoveOwner) diff --git a/src/routes/safe/components/RemoveOwner/selector.js b/src/routes/safe/components/RemoveOwner/selector.js deleted file mode 100644 index 92dacedb..00000000 --- a/src/routes/safe/components/RemoveOwner/selector.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import { List } from 'immutable' -import { createStructuredSelector, createSelector } from 'reselect' -import { userAccountSelector } from '~/logic/wallets/store/selectors' -import { type Transaction } from '~/routes/safe/store/models/transaction' -import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index' - -const pendingTransactionsSelector = createSelector( - safeTransactionsSelector, - (transactions: List) => transactions.findEntry((tx: Transaction) => tx.get('isExecuted')), -) - -export type SelectorProps = { - executor: typeof userAccountSelector, - pendingTransactions: typeof pendingTransactionsSelector, -} - -export default createStructuredSelector({ - executor: userAccountSelector, - pendingTransactions: pendingTransactionsSelector, -}) diff --git a/src/routes/safe/components/Safe/Address.jsx b/src/routes/safe/components/Safe/Address.jsx deleted file mode 100644 index 52dd3135..00000000 --- a/src/routes/safe/components/Safe/Address.jsx +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import * as React from 'react' -import ListItem from '@material-ui/core/ListItem' -import Avatar from '@material-ui/core/Avatar' -import Mail from '@material-ui/icons/Mail' -import ListItemText from '~/components/List/ListItemText' - -type Props = { - address: string, -} - -const Address = ({ address }: Props) => ( - - - - - - -) - -export default Address diff --git a/src/routes/safe/components/Safe/BalanceInfo.jsx b/src/routes/safe/components/Safe/BalanceInfo.jsx deleted file mode 100644 index 470ae2a4..00000000 --- a/src/routes/safe/components/Safe/BalanceInfo.jsx +++ /dev/null @@ -1,80 +0,0 @@ -// @flow -import * as React from 'react' -import classNames from 'classnames' -import AccountBalance from '@material-ui/icons/AccountBalance' -import Avatar from '@material-ui/core/Avatar' -import Collapse from '@material-ui/core/Collapse' -import IconButton from '@material-ui/core/IconButton' -import List from '@material-ui/core/List' -import Img from '~/components/layout/Img' -import ListItem from '@material-ui/core/ListItem' -import ListItemIcon from '@material-ui/core/ListItemIcon' -import ListItemText from '@material-ui/core/ListItemText' -import { withStyles } from '@material-ui/core/styles' -import ExpandLess from '@material-ui/icons/ExpandLess' -import ExpandMore from '@material-ui/icons/ExpandMore' -import { Map } from 'immutable' -import Button from '~/components/layout/Button' -import openHoc, { type Open } from '~/components/hoc/OpenHoc' -import { type WithStyles } from '~/theme/mui' -import { type Token } from '~/logic/tokens/store/model/token' - -type Props = Open & WithStyles & { - tokens: Map, - onMoveFunds: (token: Token) => void, -} - -const styles = { - nested: { - paddingLeft: '40px', - }, -} - -export const MOVE_FUNDS_BUTTON_TEXT = 'Move' - -const BalanceComponent = openHoc(({ - open, toggle, tokens, classes, onMoveFunds, -}: Props) => { - const hasBalances = tokens.count() > 0 - - return ( - - - - - - - - {open - ? - : - } - - - - - {tokens.valueSeq().map((token: Token) => { - const symbol = token.get('symbol') - const name = token.get('name') - const disabled = Number(token.get('funds')) === 0 - const onMoveFundsClick = () => onMoveFunds(token) - - return ( - - - {name} - - - - - ) - })} - - - - ) -}) - -export default withStyles(styles)(BalanceComponent) diff --git a/src/routes/safe/components/Safe/Confirmations.jsx b/src/routes/safe/components/Safe/Confirmations.jsx deleted file mode 100644 index 06aeb23d..00000000 --- a/src/routes/safe/components/Safe/Confirmations.jsx +++ /dev/null @@ -1,36 +0,0 @@ -// @flow -import * as React from 'react' -import ListItem from '@material-ui/core/ListItem' -import Avatar from '@material-ui/core/Avatar' -import DoneAll from '@material-ui/icons/DoneAll' -import ListItemText from '~/components/List/ListItemText' -import Button from '~/components/layout/Button' - -type Props = { - confirmations: number, - onEditThreshold: () => void, -} - -const EDIT_THRESHOLD_BUTTON_TEXT = 'EDIT' - -const Confirmations = ({ confirmations, onEditThreshold }: Props) => ( - - - - - - - -) - -export default Confirmations diff --git a/src/routes/safe/components/Safe/MultisigTx.jsx b/src/routes/safe/components/Safe/MultisigTx.jsx deleted file mode 100644 index db9b3c4f..00000000 --- a/src/routes/safe/components/Safe/MultisigTx.jsx +++ /dev/null @@ -1,35 +0,0 @@ -// @flow -import * as React from 'react' -import ListItem from '@material-ui/core/ListItem' -import Avatar from '@material-ui/core/Avatar' -import AcoountBalanceWallet from '@material-ui/icons/AccountBalanceWallet' -import Button from '~/components/layout/Button' -import ListItemText from '~/components/List/ListItemText' - -type Props = { - onSeeTxs: () => void, -} - -export const SEE_MULTISIG_BUTTON_TEXT = 'TXs' - -const MultisigTransactionsComponent = ({ onSeeTxs }: Props) => { - const text = 'See multisig txs executed on this Safe' - - return ( - - - - - - - - ) -} - -export default MultisigTransactionsComponent diff --git a/src/routes/safe/components/Safe/Owners.jsx b/src/routes/safe/components/Safe/Owners.jsx deleted file mode 100644 index acc106c3..00000000 --- a/src/routes/safe/components/Safe/Owners.jsx +++ /dev/null @@ -1,91 +0,0 @@ -// @flow -import * as React from 'react' -import openHoc, { type Open } from '~/components/hoc/OpenHoc' -import { withStyles } from '@material-ui/core/styles' -import Collapse from '@material-ui/core/Collapse' -import ListItemText from '~/components/List/ListItemText' -import List from '@material-ui/core/List' -import ListItem from '@material-ui/core/ListItem' -import ListItemIcon from '@material-ui/core/ListItemIcon' -import Avatar from '@material-ui/core/Avatar' -import IconButton from '@material-ui/core/IconButton' -import Button from '~/components/layout/Button' -import Group from '@material-ui/icons/Group' -import Delete from '@material-ui/icons/Delete' -import Person from '@material-ui/icons/Person' -import ExpandLess from '@material-ui/icons/ExpandLess' -import ExpandMore from '@material-ui/icons/ExpandMore' -import { type OwnerProps } from '~/routes/safe/store/models/owner' -import { type WithStyles } from '~/theme/mui' -import { sameAddress } from '~/logic/wallets/ethAddresses' - -const styles = { - nested: { - paddingLeft: '40px', - }, -} - -type Props = Open & WithStyles & { - owners: List, - userAddress: string, - onAddOwner: () => void, - onRemoveOwner: (name: string, addres: string) => void, -} - -export const ADD_OWNER_BUTTON_TEXT = 'Add' -export const REMOVE_OWNER_BUTTON_TEXT = 'Delete' - -const Owners = openHoc(({ - open, toggle, owners, classes, onAddOwner, userAddress, onRemoveOwner, -}: Props) => ( - - - - - - - - {open - ? - : - } - - - - - - {owners.map((owner) => { - const onRemoveIconClick = () => onRemoveOwner(owner.name, owner.address) - - return ( - - - - - - { !sameAddress(userAddress, owner.address) - && ( - - - - ) - } - - ) - })} - - - -)) - -export default withStyles(styles)(Owners) diff --git a/src/routes/safe/components/Safe/assets/gnosis_safe.svg b/src/routes/safe/components/Safe/assets/gnosis_safe.svg deleted file mode 100644 index c7bf5502..00000000 --- a/src/routes/safe/components/Safe/assets/gnosis_safe.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/routes/safe/components/Safe/index.jsx b/src/routes/safe/components/Safe/index.jsx deleted file mode 100644 index 23af1114..00000000 --- a/src/routes/safe/components/Safe/index.jsx +++ /dev/null @@ -1,135 +0,0 @@ -// @flow -import ListComponent from '@material-ui/core/List' -import * as React from 'react' -import { List } from 'immutable' -import Block from '~/components/layout/Block' -import Col from '~/components/layout/Col' -import Bold from '~/components/layout/Bold' -import Img from '~/components/layout/Img' -import Paragraph from '~/components/layout/Paragraph' -import Row from '~/components/layout/Row' -import { type Safe } from '~/routes/safe/store/models/safe' -import { type Token } from '~/logic/tokens/store/model/token' - -import Transactions from '~/routes/safe/components/Transactions' -import Threshold from '~/routes/safe/components/Threshold' -import AddOwner from '~/routes/safe/components/AddOwner' -import RemoveOwner from '~/routes/safe/components/RemoveOwner' -import SendToken from '~/routes/safe/components/SendToken' - -import Address from './Address' -import BalanceInfo from './BalanceInfo' -import Owners from './Owners' -import Confirmations from './Confirmations' -import MultisigTx from './MultisigTx' - -const safeIcon = require('./assets/gnosis_safe.svg') - -type SafeProps = { - safe: Safe, - tokens: List, - userAddress: string, -} - -type State = { - component?: React.Node, -} - -const listStyle = { - width: '100%', -} - -class GnoSafe extends React.PureComponent { - state = { - component: undefined, - } - - onListTransactions = () => { - const { safe } = this.props - - this.setState({ - component: ( - - ), - }) - } - - onEditThreshold = () => { - const { safe } = this.props - - this.setState({ - component: , - }) - } - - onAddOwner = (e: SyntheticEvent) => { - const { safe } = this.props - e.stopPropagation() - this.setState({ component: }) - } - - onRemoveOwner = (name: string, address: string) => { - const { safe } = this.props - - this.setState({ - component: ( - - ), - }) - } - - onMoveTokens = (ercToken: Token) => { - const { safe } = this.props - - this.setState({ - component: ( - - ), - }) - } - - render() { - const { safe, tokens, userAddress } = this.props - const { component } = this.state - const address = safe.get('address') - - return ( - - - - - - -
- - - - - - - {safe.name.toUpperCase()} - - - - - {component || Safe Icon} - - - - - ) - } -} - -export default GnoSafe diff --git a/src/routes/safe/components/Settings/ChangeSafeName/index.jsx b/src/routes/safe/components/Settings/ChangeSafeName/index.jsx index 84290bbc..70319bdc 100644 --- a/src/routes/safe/components/Settings/ChangeSafeName/index.jsx +++ b/src/routes/safe/components/Settings/ChangeSafeName/index.jsx @@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles' import Block from '~/components/layout/Block' import Col from '~/components/layout/Col' import Field from '~/components/forms/Field' +import Heading from '~/components/layout/Heading' import { composeValidators, required, minMaxLength } from '~/components/forms/validator' import TextField from '~/components/forms/TextField' import GnoForm from '~/components/forms/GnoForm' @@ -49,10 +50,8 @@ const ChangeSafeName = (props: Props) => { {() => ( - - Modify Safe name - - + Modify Safe name + You can change the name of this Safe. This name is only stored locally and never shared with Gnosis or any third parties. diff --git a/src/routes/safe/components/Settings/ChangeSafeName/style.js b/src/routes/safe/components/Settings/ChangeSafeName/style.js index 6971727e..4f0637fa 100644 --- a/src/routes/safe/components/Settings/ChangeSafeName/style.js +++ b/src/routes/safe/components/Settings/ChangeSafeName/style.js @@ -2,12 +2,8 @@ import { lg } from '~/theme/variables' export const styles = () => ({ - title: { - padding: `${lg} 0 20px`, - fontSize: '16px', - }, formContainer: { - padding: '0 20px', + padding: lg, minHeight: '369px', }, root: { diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx new file mode 100644 index 00000000..98ae7d59 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx @@ -0,0 +1,156 @@ +// @flow +import React, { useState, useEffect } from 'react' +import { List } from 'immutable' +import { withStyles } from '@material-ui/core/styles' +import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import Modal from '~/components/Modal' +import { type Owner } from '~/routes/safe/store/models/owner' +import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' +import OwnerForm from './screens/OwnerForm' +import ThresholdForm from './screens/ThresholdForm' +import ReviewAddOwner from './screens/Review' + +const styles = () => ({ + biggerModalWindow: { + width: '775px', + minHeight: '500px', + position: 'static', + }, +}) + +type Props = { + onClose: () => void, + classes: Object, + isOpen: boolean, + safeAddress: string, + safeName: string, + owners: List, + threshold: number, + network: string, + addSafeOwner: Function, + createTransaction: Function, +} +type ActiveScreen = 'selectOwner' | 'selectThreshold' | 'reviewAddOwner' + +export const sendAddOwner = async ( + values: Object, + safeAddress: string, + ownersOld: List, + openSnackbar: Function, + createTransaction: Function, + addSafeOwner: Function, +) => { + const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) + const txData = gnosisSafe.contract.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() + + const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar) + + if (txHash) { + addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress }) + } +} + +const AddOwner = ({ + onClose, + isOpen, + classes, + safeAddress, + safeName, + owners, + threshold, + network, + createTransaction, + addSafeOwner, +}: Props) => { + const [activeScreen, setActiveScreen] = useState('selectOwner') + const [values, setValues] = useState({}) + + useEffect( + () => () => { + setActiveScreen('selectOwner') + setValues({}) + }, + [isOpen], + ) + + const onClickBack = () => { + if (activeScreen === 'reviewAddOwner') { + setActiveScreen('selectThreshold') + } else if (activeScreen === 'selectThreshold') { + setActiveScreen('selectOwner') + } + } + + const ownerSubmitted = (newValues: Object) => { + setValues(stateValues => ({ + ...stateValues, + ownerName: newValues.ownerName, + ownerAddress: newValues.ownerAddress, + })) + setActiveScreen('selectThreshold') + } + + const thresholdSubmitted = (newValues: Object) => { + setValues(stateValues => ({ + ...stateValues, + threshold: newValues.threshold, + })) + setActiveScreen('reviewAddOwner') + } + + return ( + + + {({ openSnackbar }) => { + const onAddOwner = async () => { + onClose() + try { + sendAddOwner(values, safeAddress, owners, openSnackbar, createTransaction, addSafeOwner) + } catch (error) { + // eslint-disable-next-line + console.log('Error while removing an owner ' + error) + } + } + + return ( + + + {activeScreen === 'selectOwner' && ( + + )} + {activeScreen === 'selectThreshold' && ( + + )} + {activeScreen === 'reviewAddOwner' && ( + + )} + + + ) + }} + + + ) +} + +export default withStyles(styles)(AddOwner) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx new file mode 100644 index 00000000..f661110f --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx @@ -0,0 +1,116 @@ +// @flow +import React from 'react' +import { List } from 'immutable' +import { withStyles } from '@material-ui/core/styles' +import Close from '@material-ui/icons/Close' +import IconButton from '@material-ui/core/IconButton' +import Paragraph from '~/components/layout/Paragraph' +import Row from '~/components/layout/Row' +import GnoForm from '~/components/forms/GnoForm' +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 Field from '~/components/forms/Field' +import TextField from '~/components/forms/TextField' +import { type Owner } from '~/routes/safe/store/models/owner' +import { + composeValidators, + required, + mustBeEthereumAddress, + minMaxLength, + uniqueAddress, +} from '~/components/forms/validator' +import { styles } from './style' + +export const ADD_OWNER_NAME_INPUT_TESTID = 'add-owner-name-input' +export const ADD_OWNER_ADDRESS_INPUT_TESTID = 'add-owner-address-testid' +export const ADD_OWNER_NEXT_BTN_TESTID = 'add-owner-next-btn' + +type Props = { + onClose: () => void, + classes: Object, + onSubmit: Function, + owners: List, +} + +const OwnerForm = ({ + classes, onClose, onSubmit, owners, +}: Props) => { + const handleSubmit = (values) => { + onSubmit(values) + } + const ownerDoesntExist = uniqueAddress(owners.map(o => o.address)) + + return ( + + + + Add new owner + + 1 of 3 + + + + + + + {() => ( + + + + Add a new owner to the active Safe + + + + + + + + + + + + + + + + + + + )} + + + ) +} + +export default withStyles(styles)(OwnerForm) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/style.js b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/style.js new file mode 100644 index 00000000..3a5123bd --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/style.js @@ -0,0 +1,32 @@ +// @flow +import { lg, md, sm } from '~/theme/variables' + +export const styles = () => ({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'flex-start', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: '#a2a8ba', + marginRight: 'auto', + marginLeft: '20px', + }, + manage: { + fontSize: '24px', + }, + closeIcon: { + height: '35px', + width: '35px', + }, + formContainer: { + padding: `${md} ${lg}`, + minHeight: '340px', + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, +}) 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 new file mode 100644 index 00000000..8f3a9fda --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx @@ -0,0 +1,176 @@ +// @flow +import React 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 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 type { Owner } from '~/routes/safe/store/models/owner' +import { getEtherScanLink } from '~/logic/wallets/getWeb3' +import { secondary } from '~/theme/variables' +import { styles } from './style' + +export const ADD_OWNER_SUBMIT_BTN_TESTID = 'add-owner-submit-btn' + +const openIconStyle = { + height: '16px', + color: secondary, +} + +type Props = { + onClose: () => void, + classes: Object, + safeName: string, + owners: List, + network: string, + values: Object, + onClickBack: Function, + onSubmit: Function, +} + +const ReviewAddOwner = ({ + classes, onClose, safeName, owners, network, values, onClickBack, onSubmit, +}: Props) => { + const handleSubmit = () => { + onSubmit() + } + return ( + + + + Add new owner + + 3 of 3 + + + + + + + + + + + + Details + + + + + Safe name + + + {safeName} + + + + + Any transaction requires the confirmation of: + + + {values.threshold} + {' '} + out of + {' '} + {owners.size + 1} + {' '} + owner(s) + + + + + + + + {owners.size + 1} + {' '} + Safe owner(s) + + + + {owners.map(owner => ( + + + + + + + + + {owner.name} + + + + {owner.address} + + + + + + + + + + + ))} + + + ADDING NEW OWNER ↓ + + + + + + + + + + + {values.ownerName} + + + + {values.ownerAddress} + + + + + + + + + + + + + + + + + + + ) +} + +export default withStyles(styles)(ReviewAddOwner) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.js b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.js new file mode 100644 index 00000000..129b4a62 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.js @@ -0,0 +1,78 @@ +// @flow +import { + lg, md, sm, border, background, +} from '~/theme/variables' + +export const styles = () => ({ + root: { + height: '372px', + }, + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'flex-start', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: '#a2a8ba', + marginRight: 'auto', + marginLeft: '20px', + }, + manage: { + fontSize: '24px', + }, + closeIcon: { + height: '35px', + width: '35px', + }, + info: { + backgroundColor: background, + padding: sm, + justifyContent: 'center', + textAlign: 'center', + flexDirection: 'column', + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + details: { + padding: lg, + borderRight: `solid 1px ${border}`, + height: '100%', + }, + owners: { + overflow: 'auto', + height: '100%', + }, + ownersTitle: { + padding: lg, + }, + owner: { + padding: sm, + alignItems: 'center', + }, + name: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + userName: { + whiteSpace: 'nowrap', + }, + selectedOwner: { + padding: sm, + alignItems: 'center', + backgroundColor: '#fff3e2', + }, + user: { + justifyContent: 'left', + }, + open: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, +}) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.jsx new file mode 100644 index 00000000..2d15e999 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.jsx @@ -0,0 +1,125 @@ +// @flow +import React from 'react' +import { List } from 'immutable' +import { withStyles } from '@material-ui/core/styles' +import Close from '@material-ui/icons/Close' +import IconButton from '@material-ui/core/IconButton' +import MenuItem from '@material-ui/core/MenuItem' +import SelectField from '~/components/forms/SelectField' +import Paragraph from '~/components/layout/Paragraph' +import Row from '~/components/layout/Row' +import GnoForm from '~/components/forms/GnoForm' +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 Field from '~/components/forms/Field' +import type { Owner } from '~/routes/safe/store/models/owner' +import { + composeValidators, required, minValue, maxValue, mustBeInteger, +} from '~/components/forms/validator' +import { styles } from './style' + +export const ADD_OWNER_THRESHOLD_NEXT_BTN_TESTID = 'add-owner-threshold-next-btn' + +type Props = { + onClose: () => void, + classes: Object, + owners: List, + threshold: number, + onClickBack: Function, + onSubmit: Function, +} + +const ThresholdForm = ({ + classes, onClose, owners, threshold, onClickBack, onSubmit, +}: Props) => { + const handleSubmit = (values) => { + onSubmit(values) + } + + return ( + + + + Add new owner + + 2 of 3 + + + + + + + {() => ( + + + + + Set the required owner confirmations: + + + + + Any transaction over any daily limit requires the confirmation of: + + + + + ( + + + {[...Array(Number(owners.size + 1))].map((x, index) => ( + + {index + 1} + + ))} + + {props.meta.error && props.meta.touched && ( + + {props.meta.error} + + )} + + )} + validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(owners.size + 1))} + data-testid="threshold-select-input" + /> + + + + out of + {' '} + {owners.size + 1} + {' '} +owner(s) + + + + + + + + + + + )} + + + ) +} + +export default withStyles(styles)(ThresholdForm) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/style.js b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/style.js new file mode 100644 index 00000000..dfd2de15 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/style.js @@ -0,0 +1,45 @@ +// @flow +import { lg, md, sm } from '~/theme/variables' + +export const styles = () => ({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'flex-start', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: '#a2a8ba', + marginRight: 'auto', + marginLeft: '20px', + }, + manage: { + fontSize: '24px', + }, + closeIcon: { + height: '35px', + width: '35px', + }, + headingText: { + fontSize: '16px', + }, + formContainer: { + padding: `${md} ${lg}`, + minHeight: '340px', + }, + ownersText: { + marginLeft: sm, + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + inputRow: { + position: 'relative', + }, + errorText: { + position: 'absolute', + bottom: '-25px', + }, +}) diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx new file mode 100644 index 00000000..02a12bd4 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx @@ -0,0 +1,121 @@ +// @flow +import React from 'react' +import { withStyles } from '@material-ui/core/styles' +import Close from '@material-ui/icons/Close' +import OpenInNew from '@material-ui/icons/OpenInNew' +import IconButton from '@material-ui/core/IconButton' +import Row from '~/components/layout/Row' +import Link from '~/components/layout/Link' +import Block from '~/components/layout/Block' +import GnoForm from '~/components/forms/GnoForm' +import Button from '~/components/layout/Button' +import Hairline from '~/components/layout/Hairline' +import Field from '~/components/forms/Field' +import TextField from '~/components/forms/TextField' +import Paragraph from '~/components/layout/Paragraph' +import Identicon from '~/components/Identicon' +import { getEtherScanLink } from '~/logic/wallets/getWeb3' +import { composeValidators, required, minMaxLength } from '~/components/forms/validator' +import Modal from '~/components/Modal' +import { styles } from './style' +import { secondary } from '~/theme/variables' + +export const RENAME_OWNER_INPUT_TESTID = 'rename-owner-input' +export const SAVE_OWNER_CHANGES_BTN_TESTID = 'save-owner-changes-btn' + +const openIconStyle = { + height: '16px', + color: secondary, +} + +type Props = { + onClose: () => void, + classes: Object, + isOpen: boolean, + safeAddress: string, + ownerAddress: string, + network: string, + selectedOwnerName: string, + editSafeOwner: Function, +} + +const EditOwnerComponent = ({ + onClose, + isOpen, + classes, + safeAddress, + ownerAddress, + selectedOwnerName, + editSafeOwner, + network, +}: Props) => { + const handleSubmit = (values) => { + editSafeOwner({ safeAddress, ownerAddress, ownerName: values.ownerName }) + onClose() + } + + return ( + + + + Edit owner name + + + + + + + + {() => ( + + + + + + + + + + {ownerAddress} + + + + + + + + + + + + + + )} + + + ) +} + +const EditOwnerModal = withStyles(styles)(EditOwnerComponent) + +export default EditOwnerModal diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/style.js b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/style.js new file mode 100644 index 00000000..69e9a9a4 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/style.js @@ -0,0 +1,43 @@ +// @flow +import { + lg, md, sm, error, +} from '~/theme/variables' + +export const styles = () => ({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'space-between', + maxHeight: '75px', + boxSizing: 'border-box', + }, + manage: { + fontSize: '24px', + }, + container: { + padding: `${md} ${lg}`, + paddingBottom: '40px', + }, + close: { + height: '35px', + width: '35px', + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + buttonEdit: { + color: '#fff', + backgroundColor: error, + }, + open: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, + smallerModalWindow: { + height: 'auto', + position: 'static', + }, +}) diff --git a/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.jsx b/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.jsx new file mode 100644 index 00000000..5b7b8c9c --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.jsx @@ -0,0 +1,21 @@ +// @flow +import * as React from 'react' +import Block from '~/components/layout/Block' +import Paragraph from '~/components/layout/Paragraph' +import Identicon from '~/components/Identicon' + +type Props = { + address: string, +} + +const OwnerAddressTableCell = (props: Props) => { + const { address } = props + return ( + + + {address} + + ) +} + +export default OwnerAddressTableCell diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx new file mode 100644 index 00000000..244c6b9d --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx @@ -0,0 +1,177 @@ +// @flow +import React, { useState, useEffect } from 'react' +import { List } from 'immutable' +import { withStyles } from '@material-ui/core/styles' +import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import Modal from '~/components/Modal' +import { type Owner } from '~/routes/safe/store/models/owner' +import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts' +import CheckOwner from './screens/CheckOwner' +import ThresholdForm from './screens/ThresholdForm' +import ReviewRemoveOwner from './screens/Review' + +const styles = () => ({ + biggerModalWindow: { + width: '775px', + minHeight: '500px', + position: 'static', + }, +}) + +type Props = { + onClose: () => void, + classes: Object, + isOpen: boolean, + safeAddress: string, + safeName: string, + ownerAddress: string, + ownerName: string, + owners: List, + threshold: number, + network: string, + createTransaction: Function, + removeSafeOwner: Function, +} +type ActiveScreen = 'checkOwner' | 'selectThreshold' | 'reviewRemoveOwner' + +export const sendRemoveOwner = async ( + values: Object, + safeAddress: string, + ownerAddressToRemove: string, + ownerNameToRemove: string, + ownersOld: List, + openSnackbar: Function, + createTransaction: Function, + removeSafeOwner: Function, +) => { + const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) + const safeOwners = await gnosisSafe.getOwners() + const index = safeOwners.findIndex(ownerAddress => ownerAddress.toLowerCase() === ownerAddressToRemove.toLowerCase()) + const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] + const txData = gnosisSafe.contract.methods + .removeOwner(prevAddress, ownerAddressToRemove, values.threshold) + .encodeABI() + + const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar) + + if (txHash) { + removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove }) + } +} + +const RemoveOwner = ({ + onClose, + isOpen, + classes, + safeAddress, + safeName, + ownerAddress, + ownerName, + owners, + threshold, + network, + createTransaction, + removeSafeOwner, +}: Props) => { + const [activeScreen, setActiveScreen] = useState('checkOwner') + const [values, setValues] = useState({}) + + useEffect( + () => () => { + setActiveScreen('checkOwner') + setValues({}) + }, + [isOpen], + ) + + const onClickBack = () => { + if (activeScreen === 'reviewRemoveOwner') { + setActiveScreen('selectThreshold') + } else if (activeScreen === 'selectThreshold') { + setActiveScreen('checkOwner') + } + } + + const ownerSubmitted = () => { + setActiveScreen('selectThreshold') + } + + const thresholdSubmitted = (newValues: Object) => { + values.threshold = newValues.threshold + setValues(values) + setActiveScreen('reviewRemoveOwner') + } + + return ( + + + {({ openSnackbar }) => { + const onRemoveOwner = () => { + onClose() + try { + sendRemoveOwner( + values, + safeAddress, + ownerAddress, + ownerName, + owners, + openSnackbar, + createTransaction, + removeSafeOwner, + ) + } catch (error) { + // eslint-disable-next-line + console.log('Error while removing an owner ' + error) + } + } + + return ( + + + {activeScreen === 'checkOwner' && ( + + )} + {activeScreen === 'selectThreshold' && ( + + )} + {activeScreen === 'reviewRemoveOwner' && ( + + )} + + + ) + }} + + + ) +} + +export default withStyles(styles)(RemoveOwner) 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 new file mode 100644 index 00000000..a059bb08 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.jsx @@ -0,0 +1,107 @@ +// @flow +import React from 'react' +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 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_TESTID = 'remove-owner-next-btn' + +const openIconStyle = { + height: '16px', + color: secondary, +} + +type Props = { + onClose: () => void, + classes: Object, + ownerAddress: string, + ownerName: string, + network: string, + onSubmit: Function, +} + +const CheckOwner = ({ + classes, + onClose, + ownerAddress, + ownerName, + network, + onSubmit, +}: Props) => { + const handleSubmit = (values) => { + onSubmit(values) + } + + return ( + + + + Remove owner + + 1 of 3 + + + + + + + + + Review the owner you want to remove from the active Safe: + + + + + + + + + + {ownerName} + + + + {ownerAddress} + + + + + + + + + + + + + + + + ) +} + +export default withStyles(styles)(CheckOwner) 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 new file mode 100644 index 00000000..d05cc023 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/style.js @@ -0,0 +1,45 @@ +// @flow +import { lg, md, sm } from '~/theme/variables' + +export const styles = () => ({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'flex-start', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: '#a2a8ba', + marginRight: 'auto', + marginLeft: '20px', + }, + manage: { + fontSize: '24px', + }, + closeIcon: { + height: '35px', + width: '35px', + }, + formContainer: { + padding: `${md} ${lg}`, + minHeight: '340px', + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + name: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + userName: { + whiteSpace: 'nowrap', + }, + owner: { + alignItems: 'center', + }, + user: { + justifyContent: 'left', + }, +}) 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 new file mode 100644 index 00000000..f7e2d63e --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx @@ -0,0 +1,194 @@ +// @flow +import React 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 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 type { Owner } from '~/routes/safe/store/models/owner' +import { getEtherScanLink } from '~/logic/wallets/getWeb3' +import { secondary } from '~/theme/variables' +import { styles } from './style' + +export const REMOVE_OWNER_REVIEW_BTN_TESTID = 'remove-owner-review-btn' + +const openIconStyle = { + height: '16px', + color: secondary, +} + +type Props = { + onClose: () => void, + classes: Object, + safeName: string, + owners: List, + network: string, + values: Object, + ownerAddress: string, + ownerName: string, + onClickBack: Function, + onSubmit: Function, +} + +const ReviewRemoveOwner = ({ + classes, + onClose, + safeName, + owners, + network, + values, + ownerAddress, + ownerName, + onClickBack, + onSubmit, +}: Props) => { + const handleSubmit = () => { + onSubmit() + } + + return ( + + + + Remove owner + + 3 of 3 + + + + + + + + + + + + Details + + + + + Safe name + + + {safeName} + + + + + Any transaction requires the confirmation of: + + + {values.threshold} + {' '} + out of + {' '} + {owners.size - 1} + {' '} + owner(s) + + + + + + + + {owners.size - 1} + {' '} + Safe owner(s) + + + + {owners.map( + owner => owner.address !== ownerAddress && ( + + + + + + + + + {owner.name} + + + + {owner.address} + + + + + + + + + + + ), + )} + + + REMOVING OWNER ↓ + + + + + + + + + + + {ownerName} + + + + {ownerAddress} + + + + + + + + + + + + + + + + + + + ) +} + +export default withStyles(styles)(ReviewRemoveOwner) diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/style.js b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/style.js new file mode 100644 index 00000000..bbab6aa3 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/style.js @@ -0,0 +1,78 @@ +// @flow +import { + lg, sm, border, background, +} from '~/theme/variables' + +export const styles = () => ({ + root: { + height: '372px', + }, + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'flex-start', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: '#a2a8ba', + marginRight: 'auto', + marginLeft: '20px', + }, + manage: { + fontSize: '24px', + }, + closeIcon: { + height: '35px', + width: '35px', + }, + info: { + backgroundColor: background, + padding: sm, + justifyContent: 'center', + textAlign: 'center', + flexDirection: 'column', + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + details: { + padding: lg, + borderRight: `solid 1px ${border}`, + height: '100%', + }, + owners: { + overflow: 'auto', + height: '100%', + }, + ownersTitle: { + padding: lg, + }, + owner: { + padding: sm, + alignItems: 'center', + }, + name: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + userName: { + whiteSpace: 'nowrap', + }, + selectedOwner: { + padding: sm, + alignItems: 'center', + backgroundColor: '#ffe6ea', + }, + user: { + justifyContent: 'left', + }, + open: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, +}) diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.jsx new file mode 100644 index 00000000..56f118f3 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.jsx @@ -0,0 +1,130 @@ +// @flow +import React from 'react' +import { List } from 'immutable' +import { withStyles } from '@material-ui/core/styles' +import Close from '@material-ui/icons/Close' +import MenuItem from '@material-ui/core/MenuItem' +import IconButton from '@material-ui/core/IconButton' +import SelectField from '~/components/forms/SelectField' +import Paragraph from '~/components/layout/Paragraph' +import Row from '~/components/layout/Row' +import GnoForm from '~/components/forms/GnoForm' +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 Field from '~/components/forms/Field' +import type { Owner } from '~/routes/safe/store/models/owner' +import { + composeValidators, required, minValue, maxValue, mustBeInteger, +} from '~/components/forms/validator' +import { styles } from './style' + +export const REMOVE_OWNER_THRESHOLD_NEXT_BTN_TESTID = 'remove-owner-threshold-next-btn' + +type Props = { + onClose: () => void, + classes: Object, + owners: List, + threshold: number, + onClickBack: Function, + onSubmit: Function, +} + +const ThresholdForm = ({ + classes, onClose, owners, threshold, onClickBack, onSubmit, +}: Props) => { + const handleSubmit = (values) => { + onSubmit(values) + } + const defaultThreshold = threshold > 1 ? threshold - 1 : threshold + + return ( + + + + Remove owner + + 2 of 3 + + + + + + + {() => { + const numOptions = owners.size > 1 ? owners.size - 1 : 1 + + return ( + + + + + Set the required owner confirmations: + + + + + Any transaction over any daily limit requires the confirmation of: + + + + + ( + + + {[...Array(Number(numOptions))].map((x, index) => ( + + {index + 1} + + ))} + + {props.meta.error && props.meta.touched && ( + + {props.meta.error} + + )} + + )} + validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(numOptions))} + data-testid="threshold-select-input" + /> + + + + out of + {' '} + {owners.size - 1} + {' '} +owner(s) + + + + + + + + + + + ) + }} + + + ) +} + +export default withStyles(styles)(ThresholdForm) diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/style.js b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/style.js new file mode 100644 index 00000000..dfd2de15 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/style.js @@ -0,0 +1,45 @@ +// @flow +import { lg, md, sm } from '~/theme/variables' + +export const styles = () => ({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'flex-start', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: '#a2a8ba', + marginRight: 'auto', + marginLeft: '20px', + }, + manage: { + fontSize: '24px', + }, + closeIcon: { + height: '35px', + width: '35px', + }, + headingText: { + fontSize: '16px', + }, + formContainer: { + padding: `${md} ${lg}`, + minHeight: '340px', + }, + ownersText: { + marginLeft: sm, + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + inputRow: { + position: 'relative', + }, + errorText: { + position: 'absolute', + bottom: '-25px', + }, +}) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx new file mode 100644 index 00000000..74089e60 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx @@ -0,0 +1,164 @@ +// @flow +import React, { useState, useEffect } from 'react' +import { List } from 'immutable' +import { withStyles } from '@material-ui/core/styles' +import { SharedSnackbarConsumer } from '~/components/SharedSnackBar' +import Modal from '~/components/Modal' +import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts' +import OwnerForm from './screens/OwnerForm' +import ReviewReplaceOwner from './screens/Review' + +const styles = () => ({ + biggerModalWindow: { + width: '775px', + minHeight: '500px', + position: 'static', + }, +}) + +type Props = { + onClose: () => void, + classes: Object, + isOpen: boolean, + safeAddress: string, + safeName: string, + ownerAddress: string, + ownerName: string, + owners: List, + network: string, + threshold: string, + createTransaction: Function, + replaceSafeOwner: Function, +} +type ActiveScreen = 'checkOwner' | 'reviewReplaceOwner' + +export const sendReplaceOwner = async ( + values: Object, + safeAddress: string, + ownerAddressToRemove: string, + ownerNameToRemove: string, + ownersOld: List, + openSnackbar: Function, + createTransaction: Function, + replaceSafeOwner: Function, +) => { + const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) + const safeOwners = await gnosisSafe.getOwners() + const index = safeOwners.findIndex(ownerAddress => ownerAddress.toLowerCase() === ownerAddressToRemove.toLowerCase()) + const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] + const txData = gnosisSafe.contract.methods + .swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress) + .encodeABI() + + const txHash = await createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar) + + if (txHash) { + replaceSafeOwner({ + safeAddress, + oldOwnerAddress: ownerAddressToRemove, + ownerAddress: values.ownerAddress, + ownerName: values.ownerName, + }) + } +} + +const ReplaceOwner = ({ + onClose, + isOpen, + classes, + safeAddress, + safeName, + ownerAddress, + ownerName, + owners, + network, + threshold, + createTransaction, + replaceSafeOwner, +}: Props) => { + const [activeScreen, setActiveScreen] = useState('checkOwner') + const [values, setValues] = useState({}) + + useEffect( + () => () => { + setActiveScreen('checkOwner') + setValues({}) + }, + [isOpen], + ) + + const onClickBack = () => setActiveScreen('checkOwner') + + const ownerSubmitted = (newValues: Object) => { + values.ownerName = newValues.ownerName + values.ownerAddress = newValues.ownerAddress + setValues(values) + setActiveScreen('reviewReplaceOwner') + } + + return ( + + + {({ openSnackbar }) => { + const onReplaceOwner = () => { + onClose() + try { + sendReplaceOwner( + values, + safeAddress, + ownerAddress, + ownerName, + owners, + openSnackbar, + createTransaction, + replaceSafeOwner, + ) + } catch (error) { + // eslint-disable-next-line + console.log('Error while removing an owner ' + error) + } + } + + return ( + + + {activeScreen === 'checkOwner' && ( + + )} + {activeScreen === 'reviewReplaceOwner' && ( + + )} + + + ) + }} + + + ) +} + +export default withStyles(styles)(ReplaceOwner) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx new file mode 100644 index 00000000..861ec813 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx @@ -0,0 +1,159 @@ +// @flow +import React from 'react' +import classNames from 'classnames/bind' +import { List } from 'immutable' +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 GnoForm from '~/components/forms/GnoForm' +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 Field from '~/components/forms/Field' +import TextField from '~/components/forms/TextField' +import Identicon from '~/components/Identicon' +import Link from '~/components/layout/Link' +import { getEtherScanLink } from '~/logic/wallets/getWeb3' +import { type Owner } from '~/routes/safe/store/models/owner' +import { + composeValidators, + required, + mustBeEthereumAddress, + minMaxLength, + uniqueAddress, +} from '~/components/forms/validator' +import { styles } from './style' +import { secondary } from '~/theme/variables' + +export const REPLACE_OWNER_NAME_INPUT_TESTID = 'replace-owner-name-input' +export const REPLACE_OWNER_ADDRESS_INPUT_TESTID = 'replace-owner-address-testid' +export const REPLACE_OWNER_NEXT_BTN_TESTID = 'replace-owner-next-btn' + +const openIconStyle = { + height: '16px', + color: secondary, +} + +type Props = { + onClose: () => void, + classes: Object, + ownerAddress: string, + ownerName: string, + network: string, + onSubmit: Function, + owners: List, +} + +const OwnerForm = ({ + classes, onClose, ownerAddress, ownerName, network, onSubmit, owners, +}: Props) => { + const handleSubmit = (values) => { + onSubmit(values) + } + const ownerDoesntExist = uniqueAddress(owners.map(o => o.address)) + + return ( + + + + Replace owner + + 1 of 2 + + + + + + + {() => ( + + + + + Review the owner you want to replace from the active Safe. Then specify the new owner you want to + replace it with: + + + + Current owner + + + + + + + + + {ownerName} + + + + {ownerAddress} + + + + + + + + + + New owner + + + + + + + + + + + + + + + + + + + )} + + + ) +} + +export default withStyles(styles)(OwnerForm) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/style.js b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/style.js new file mode 100644 index 00000000..54952dc1 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/style.js @@ -0,0 +1,44 @@ +// @flow +import { lg, md, sm } from '~/theme/variables' + +export const styles = () => ({ + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'flex-start', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: '#a2a8ba', + marginRight: 'auto', + marginLeft: '20px', + }, + manage: { + fontSize: '24px', + }, + closeIcon: { + height: '35px', + width: '35px', + }, + formContainer: { + padding: `${md} ${lg}`, + minHeight: '340px', + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + owner: { + alignItems: 'center', + }, + user: { + justifyContent: 'left', + }, + userName: { + whiteSpace: 'nowrap', + }, + name: { + marginRight: `${sm}`, + }, +}) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.jsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.jsx new file mode 100644 index 00000000..8c92dd7f --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.jsx @@ -0,0 +1,222 @@ +// @flow +import React 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 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 type { Owner } from '~/routes/safe/store/models/owner' +import { getEtherScanLink } from '~/logic/wallets/getWeb3' +import { secondary } from '~/theme/variables' +import { styles } from './style' + +export const REPLACE_OWNER_SUBMIT_BTN_TESTID = 'replace-owner-submit-btn' + +const openIconStyle = { + height: '16px', + color: secondary, +} + +type Props = { + onClose: () => void, + classes: Object, + safeName: string, + owners: List, + network: string, + values: Object, + ownerAddress: string, + ownerName: string, + onClickBack: Function, + onSubmit: Function, + threshold: string, +} + +const ReviewRemoveOwner = ({ + classes, + onClose, + safeName, + owners, + network, + values, + ownerAddress, + ownerName, + onClickBack, + threshold, + onSubmit, +}: Props) => { + const handleSubmit = () => { + onSubmit() + } + + return ( + + + + Replace owner + + 2 of 2 + + + + + + + + + + + + Details + + + + + Safe name + + + {safeName} + + + + + Any transaction requires the confirmation of: + + + {threshold} + {' '} + out of + {' '} + {owners.size} + {' '} + owner(s) + + + + + + + + {owners.size} + {' '} + Safe owner(s) + + + + {owners.map( + owner => owner.address !== ownerAddress && ( + + + + + + + + + {owner.name} + + + + {owner.address} + + + + + + + + + + + ), + )} + + + REMOVING OWNER ↓ + + + + + + + + + + + {ownerName} + + + + {ownerAddress} + + + + + + + + + + + ADDING NEW OWNER ↓ + + + + + + + + + + + {values.ownerName} + + + + {values.ownerAddress} + + + + + + + + + + + + + + + + + + + ) +} + +export default withStyles(styles)(ReviewRemoveOwner) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.js b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.js new file mode 100644 index 00000000..29241ceb --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.js @@ -0,0 +1,83 @@ +// @flow +import { + lg, md, sm, border, background, +} from '~/theme/variables' + +export const styles = () => ({ + root: { + height: '372px', + }, + heading: { + padding: `${sm} ${lg}`, + justifyContent: 'flex-start', + boxSizing: 'border-box', + maxHeight: '75px', + }, + annotation: { + letterSpacing: '-1px', + color: '#a2a8ba', + marginRight: 'auto', + marginLeft: '20px', + }, + manage: { + fontSize: '24px', + }, + closeIcon: { + height: '35px', + width: '35px', + }, + info: { + backgroundColor: background, + padding: sm, + justifyContent: 'center', + textAlign: 'center', + flexDirection: 'column', + }, + buttonRow: { + height: '84px', + justifyContent: 'center', + }, + details: { + padding: lg, + borderRight: `solid 1px ${border}`, + height: '100%', + }, + owners: { + overflow: 'auto', + height: '100%', + }, + ownersTitle: { + padding: lg, + }, + owner: { + padding: sm, + alignItems: 'center', + }, + name: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + userName: { + whiteSpace: 'nowrap', + }, + selectedOwnerRemoved: { + padding: sm, + alignItems: 'center', + backgroundColor: '#ffe6ea', + }, + selectedOwnerAdded: { + padding: sm, + alignItems: 'center', + backgroundColor: '#fff3e2', + }, + user: { + justifyContent: 'left', + }, + open: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, +}) diff --git a/src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg b/src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg new file mode 100644 index 00000000..b5b1c3b4 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/Settings/ManageOwners/assets/icons/replace-owner.svg b/src/routes/safe/components/Settings/ManageOwners/assets/icons/replace-owner.svg new file mode 100644 index 00000000..1fd14cd9 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/assets/icons/replace-owner.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/Settings/ManageOwners/dataFetcher.js b/src/routes/safe/components/Settings/ManageOwners/dataFetcher.js new file mode 100644 index 00000000..9575d735 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/dataFetcher.js @@ -0,0 +1,56 @@ +// @flow +import { List } from 'immutable' +import type { Owner } from '~/routes/safe/store/models/owner' +import { type SortRow } from '~/components/Table/sorting' +import { type Column } from '~/components/Table/TableHead' + +export const OWNERS_TABLE_NAME_ID = 'name' +export const OWNERS_TABLE_ADDRESS_ID = 'address' +export const OWNERS_TABLE_ACTIONS_ID = 'actions' + +type OwnerData = { + name: string, + address: string, +} + +export type OwnerRow = SortRow + +export const getOwnerData = (owners: List): List => { + const rows = owners.map((owner: Owner) => ({ + [OWNERS_TABLE_NAME_ID]: owner.name, + [OWNERS_TABLE_ADDRESS_ID]: owner.address, + })) + + return rows +} + +export const generateColumns = () => { + const nameColumn: Column = { + id: OWNERS_TABLE_NAME_ID, + order: false, + disablePadding: false, + label: 'Name', + width: 150, + custom: false, + align: 'left', + } + + const addressColumn: Column = { + id: OWNERS_TABLE_ADDRESS_ID, + order: false, + disablePadding: false, + label: 'Address', + custom: false, + align: 'left', + } + + const actionsColumn: Column = { + id: OWNERS_TABLE_ACTIONS_ID, + order: false, + disablePadding: false, + label: '', + custom: true, + } + + return List([nameColumn, addressColumn, actionsColumn]) +} diff --git a/src/routes/safe/components/Settings/ManageOwners/index.jsx b/src/routes/safe/components/Settings/ManageOwners/index.jsx new file mode 100644 index 00000000..a02e815a --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/index.jsx @@ -0,0 +1,265 @@ +// @flow +import React from 'react' +import { List } from 'immutable' +import { withStyles } from '@material-ui/core/styles' +import TableRow from '@material-ui/core/TableRow' +import TableCell from '@material-ui/core/TableCell' +import Block from '~/components/layout/Block' +import Col from '~/components/layout/Col' +import Table from '~/components/Table' +import { type Column, cellWidth } from '~/components/Table/TableHead' +import Row from '~/components/layout/Row' +import Heading from '~/components/layout/Heading' +import Hairline from '~/components/layout/Hairline' +import Button from '~/components/layout/Button' +import Img from '~/components/layout/Img' +import AddOwnerModal from './AddOwnerModal' +import RemoveOwnerModal from './RemoveOwnerModal' +import ReplaceOwnerModal from './ReplaceOwnerModal' +import EditOwnerModal from './EditOwnerModal' +import OwnerAddressTableCell from './OwnerAddressTableCell' +import type { Owner } from '~/routes/safe/store/models/owner' +import { + getOwnerData, generateColumns, OWNERS_TABLE_NAME_ID, OWNERS_TABLE_ADDRESS_ID, type OwnerRow, +} from './dataFetcher' +import { lg, sm, boldFont } from '~/theme/variables' +import { styles } from './style' +import ReplaceOwnerIcon from './assets/icons/replace-owner.svg' +import RenameOwnerIcon from './assets/icons/rename-owner.svg' +import RemoveOwnerIcon from '../assets/icons/bin.svg' + +export const RENAME_OWNER_BTN_TESTID = 'rename-owner-btn' +export const REMOVE_OWNER_BTN_TESTID = 'remove-owner-btn' +export const ADD_OWNER_BTN_TESTID = 'add-owner-btn' +export const REPLACE_OWNER_BTN_TESTID = 'replace-owner-btn' +export const OWNERS_ROW_TESTID = 'owners-row' + +const controlsStyle = { + backgroundColor: 'white', + padding: sm, +} + +const addOwnerButtonStyle = { + marginRight: sm, + fontWeight: boldFont, +} + +const title = { + padding: lg, +} + +type Props = { + classes: Object, + safeAddress: string, + safeName: string, + owners: List, + network: string, + threshold: number, + userAddress: string, + createTransaction: Function, + addSafeOwner: Function, + removeSafeOwner: Function, + replaceSafeOwner: Function, + editSafeOwner: Function, + granted: boolean, +} + +type State = { + selectedOwnerAddress?: string, + selectedOwnerName?: string, + showAddOwner: boolean, + showRemoveOwner: boolean, + showReplaceOwner: boolean, + showEditOwner: boolean, +} + +type Action = 'AddOwner' | 'EditOwner' | 'ReplaceOwner' | 'RemoveOwner' + +class ManageOwners extends React.Component { + state = { + selectedOwnerAddress: undefined, + selectedOwnerName: undefined, + showAddOwner: false, + showRemoveOwner: false, + showReplaceOwner: false, + showEditOwner: false, + } + + onShow = (action: Action, row?: Object) => () => { + this.setState({ + [`show${action}`]: true, + selectedOwnerAddress: row && row.address, + selectedOwnerName: row && row.name, + }) + } + + onHide = (action: Action) => () => { + this.setState({ + [`show${action}`]: false, + selectedOwnerAddress: undefined, + selectedOwnerName: undefined, + }) + } + + render() { + const { + classes, + safeAddress, + safeName, + owners, + threshold, + network, + userAddress, + createTransaction, + addSafeOwner, + removeSafeOwner, + replaceSafeOwner, + editSafeOwner, + granted, + } = this.props + const { + showAddOwner, + showRemoveOwner, + showReplaceOwner, + showEditOwner, + selectedOwnerName, + selectedOwnerAddress, + } = this.state + + const columns = generateColumns() + const autoColumns = columns.filter(c => !c.custom) + const ownerData = getOwnerData(owners) + + return ( + + + Manage Safe Owners + + {(sortedData: Array) => sortedData.map((row: any, index: number) => ( + + {autoColumns.map((column: Column) => ( + + {column.id === OWNERS_TABLE_ADDRESS_ID ? ( + + ) : ( + row[column.id] + )} + + ))} + + {granted && ( + + Edit owner + Replace owner + {ownerData.size > 1 && ( + Remove owner + )} + + )} + + + )) + } +
+
+ {granted && ( + + + + + + + + + )} + + + + +
+ ) + } +} + +export default withStyles(styles)(ManageOwners) diff --git a/src/routes/safe/components/Settings/ManageOwners/style.js b/src/routes/safe/components/Settings/ManageOwners/style.js new file mode 100644 index 00000000..161d71f9 --- /dev/null +++ b/src/routes/safe/components/Settings/ManageOwners/style.js @@ -0,0 +1,31 @@ +// @flow +import { lg } from '~/theme/variables' + +export const styles = () => ({ + formContainer: { + minHeight: '369px', + }, + hide: { + '&:hover': { + backgroundColor: '#fff3e2', + }, + '&:hover $actions': { + visibility: 'initial', + }, + }, + actions: { + justifyContent: 'flex-end', + visibility: 'hidden', + }, + editOwnerIcon: { + cursor: 'pointer', + }, + replaceOwnerIcon: { + marginLeft: lg, + cursor: 'pointer', + }, + removeOwnerIcon: { + marginLeft: lg, + cursor: 'pointer', + }, +}) diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/index.jsx b/src/routes/safe/components/Settings/RemoveSafeModal/index.jsx index a841f826..e4894e06 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/index.jsx +++ b/src/routes/safe/components/Settings/RemoveSafeModal/index.jsx @@ -2,12 +2,12 @@ import React from 'react' import classNames from 'classnames' import { connect } from 'react-redux' -import { history } from '~/store' -import { SAFELIST_ADDRESS } from '~/routes/routes' 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 { SAFELIST_ADDRESS } from '~/routes/routes' +import { history } from '~/store' import Block from '~/components/layout/Block' import Modal from '~/components/Modal' import Identicon from '~/components/Identicon' diff --git a/src/routes/safe/components/Settings/actions.js b/src/routes/safe/components/Settings/actions.js new file mode 100644 index 00000000..0d8e6622 --- /dev/null +++ b/src/routes/safe/components/Settings/actions.js @@ -0,0 +1,19 @@ +// @flow +import addSafeOwner from '~/routes/safe/store/actions/addSafeOwner' +import removeSafeOwner from '~/routes/safe/store/actions/removeSafeOwner' +import replaceSafeOwner from '~/routes/safe/store/actions/replaceSafeOwner' +import editSafeOwner from '~/routes/safe/store/actions/editSafeOwner' + +export type Actions = { + addSafeOwner: Function, + removeSafeOwner: Function, + replaceSafeOwner: Function, + editSafeOwner: Function, +} + +export default { + addSafeOwner, + removeSafeOwner, + replaceSafeOwner, + editSafeOwner, +} diff --git a/src/routes/safe/components/Settings/assets/icons/bin.svg b/src/routes/safe/components/Settings/assets/icons/bin.svg new file mode 100644 index 00000000..f8fe7071 --- /dev/null +++ b/src/routes/safe/components/Settings/assets/icons/bin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/Settings/index.jsx b/src/routes/safe/components/Settings/index.jsx index ab8d38d9..e7bd0de7 100644 --- a/src/routes/safe/components/Settings/index.jsx +++ b/src/routes/safe/components/Settings/index.jsx @@ -2,24 +2,32 @@ import * as React from 'react' import cn from 'classnames' import { List } from 'immutable' +import { connect } from 'react-redux' import { withStyles } from '@material-ui/core/styles' import Block from '~/components/layout/Block' import Col from '~/components/layout/Col' import Row from '~/components/layout/Row' +import Span from '~/components/layout/Span' +import Img from '~/components/layout/Img' import RemoveSafeModal from './RemoveSafeModal' import Paragraph from '~/components/layout/Paragraph' import Hairline from '~/components/layout/Hairline' import { type Owner } from '~/routes/safe/store/models/owner' import ChangeSafeName from './ChangeSafeName' import ThresholdSettings from './ThresholdSettings' +import ManageOwners from './ManageOwners' +import actions, { type Actions } from './actions' import { styles } from './style' +import RemoveSafeIcon from './assets/icons/bin.svg' + +export const OWNERS_SETTINGS_TAB_TESTID = 'owner-settings-tab' type State = { showRemoveSafe: boolean, menuOptionIndex: number, } -type Props = { +type Props = Actions & { classes: Object, granted: boolean, etherScanLink: string, @@ -27,8 +35,13 @@ type Props = { safeName: string, owners: List, threshold: number, + network: string, createTransaction: Function, - updateSafe: Function, + addSafeOwner: Function, + removeSafeOwner: Function, + replaceSafeOwner: Function, + editSafeOwner: Function, + userAddress: string, } type Action = 'RemoveSafe' @@ -59,10 +72,16 @@ class Settings extends React.Component { etherScanLink, safeAddress, safeName, - updateSafe, - owners, threshold, + owners, + network, + userAddress, createTransaction, + updateSafe, + addSafeOwner, + removeSafeOwner, + replaceSafeOwner, + editSafeOwner, } = this.props return ( @@ -74,8 +93,11 @@ class Settings extends React.Component { - - Remove Safe + + + Remove Safe + + Trash Icon { Safe name + + Owners ( + {owners.size} +) + + {granted && ( - - Owners - - { Required confirmations - - Modules - - )} @@ -128,7 +146,22 @@ class Settings extends React.Component { {menuOptionIndex === 1 && ( )} - {granted && menuOptionIndex === 2 &&

To be done

} + {menuOptionIndex === 2 && ( + + )} {granted && menuOptionIndex === 3 && ( { safeAddress={safeAddress} /> )} - {granted && menuOptionIndex === 4 &&

To be done

} @@ -146,4 +178,9 @@ class Settings extends React.Component { } } -export default withStyles(styles)(Settings) +const settingsComponent = withStyles(styles)(Settings) + +export default connect( + undefined, + actions, +)(settingsComponent) diff --git a/src/routes/safe/components/Settings/style.js b/src/routes/safe/components/Settings/style.js index c6cc3304..c7b24c88 100644 --- a/src/routes/safe/components/Settings/style.js +++ b/src/routes/safe/components/Settings/style.js @@ -39,4 +39,14 @@ export const styles = () => ({ cursor: 'pointer', }, }, + removeSafeText: { + height: '16px', + lineHeight: '16px', + paddingRight: sm, + float: 'left', + }, + removeSafeIcon: { + height: '16px', + cursor: 'pointer', + }, }) diff --git a/src/routes/safe/container/selector.js b/src/routes/safe/container/selector.js index 14db2918..fc3bcf0e 100644 --- a/src/routes/safe/container/selector.js +++ b/src/routes/safe/container/selector.js @@ -46,7 +46,7 @@ export const grantedSelector: Selector = crea return false } - return owners.find((owner: Owner) => sameAddress(owner.get('address'), userAccount)) !== undefined + return owners.find((owner: Owner) => sameAddress(owner.address, userAccount)) !== undefined }, ) diff --git a/src/routes/safe/store/actions/addSafeOwner.js b/src/routes/safe/store/actions/addSafeOwner.js new file mode 100644 index 00000000..bb9a4f0c --- /dev/null +++ b/src/routes/safe/store/actions/addSafeOwner.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const ADD_SAFE_OWNER = 'ADD_SAFE_OWNER' + +const addSafeOwner = createAction(ADD_SAFE_OWNER) + +export default addSafeOwner diff --git a/src/routes/safe/store/actions/createTransaction.js b/src/routes/safe/store/actions/createTransaction.js index c4b6ff99..4970332f 100644 --- a/src/routes/safe/store/actions/createTransaction.js +++ b/src/routes/safe/store/actions/createTransaction.js @@ -31,6 +31,7 @@ const createTransaction = ( txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from) openSnackbar('Transaction has been confirmed', 'success') } else { + console.log('Temporal error: threshold != 1') // txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce) } // dispatch(addTransactions(txHash)) diff --git a/src/routes/safe/store/actions/editSafeOwner.js b/src/routes/safe/store/actions/editSafeOwner.js new file mode 100644 index 00000000..1edf0606 --- /dev/null +++ b/src/routes/safe/store/actions/editSafeOwner.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const EDIT_SAFE_OWNER = 'EDIT_SAFE_OWNER' + +const editSafeOwner = createAction(EDIT_SAFE_OWNER) + +export default editSafeOwner diff --git a/src/routes/safe/store/actions/removeSafeOwner.js b/src/routes/safe/store/actions/removeSafeOwner.js new file mode 100644 index 00000000..b83312c0 --- /dev/null +++ b/src/routes/safe/store/actions/removeSafeOwner.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const REMOVE_SAFE_OWNER = 'REMOVE_SAFE_OWNER' + +const removeSafeOwner = createAction(REMOVE_SAFE_OWNER) + +export default removeSafeOwner diff --git a/src/routes/safe/store/actions/replaceSafeOwner.js b/src/routes/safe/store/actions/replaceSafeOwner.js new file mode 100644 index 00000000..f6e75418 --- /dev/null +++ b/src/routes/safe/store/actions/replaceSafeOwner.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const REPLACE_SAFE_OWNER = 'REPLACE_SAFE_OWNER' + +const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER) + +export default replaceSafeOwner diff --git a/src/routes/safe/store/middleware/safeStorage.js b/src/routes/safe/store/middleware/safeStorage.js index d49b1a50..afdfdb4f 100644 --- a/src/routes/safe/store/middleware/safeStorage.js +++ b/src/routes/safe/store/middleware/safeStorage.js @@ -1,18 +1,33 @@ // @flow +import type { Store, AnyAction } from 'redux' +import { List } from 'immutable' import { ADD_SAFE } from '~/routes/safe/store/actions/addSafe' import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe' import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe' -import type { Store, AnyAction } from 'redux' +import { ADD_SAFE_OWNER } from '~/routes/safe/store/actions/addSafeOwner' +import { REMOVE_SAFE_OWNER } from '~/routes/safe/store/actions/removeSafeOwner' +import { REPLACE_SAFE_OWNER } from '~/routes/safe/store/actions/replaceSafeOwner' +import { EDIT_SAFE_OWNER } from '~/routes/safe/store/actions/editSafeOwner' import { type GlobalState } from '~/store/' import { saveSafes, setOwners, removeOwners } from '~/logic/safe/utils' import { safesMapSelector } from '~/routes/safeList/store/selectors' import { getActiveTokensAddressesForAllSafes } from '~/routes/safe/store/selectors' import { tokensSelector } from '~/logic/tokens/store/selectors' import type { Token } from '~/logic/tokens/store/model/token' +import { makeOwner } from '~/routes/safe/store/models/owner' import { saveActiveTokens } from '~/logic/tokens/utils/tokensStorage' import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes' -const watchedActions = [ADD_SAFE, UPDATE_SAFE, REMOVE_SAFE, ACTIVATE_TOKEN_FOR_ALL_SAFES] +const watchedActions = [ + ADD_SAFE, + UPDATE_SAFE, + REMOVE_SAFE, + ADD_SAFE_OWNER, + REMOVE_SAFE_OWNER, + REPLACE_SAFE_OWNER, + EDIT_SAFE_OWNER, + ACTIVATE_TOKEN_FOR_ALL_SAFES, +] const safeStorageMware = (store: Store) => (next: Function) => async (action: AnyAction) => { const handledAction = next(action) @@ -20,30 +35,78 @@ const safeStorageMware = (store: Store) => (next: Function) => asyn if (watchedActions.includes(action.type)) { const state: GlobalState = store.getState() const safes = safesMapSelector(state) - saveSafes(safes.toJSON()) + await saveSafes(safes.toJSON()) - // recalculate active tokens - if (action.payload.activeTokens || action.type === ACTIVATE_TOKEN_FOR_ALL_SAFES) { - const tokens = tokensSelector(state) - const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state) + switch (action.type) { + case ACTIVATE_TOKEN_FOR_ALL_SAFES: { + let { activeTokens } = action.payload + if (activeTokens) { + const tokens = tokensSelector(state) + const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state) - const activeTokens = tokens.withMutations((map) => { - map.forEach((token: Token) => { - if (!activeTokenAddresses.has(token.address)) { - map.remove(token.address) - } + activeTokens = tokens.withMutations((map) => { + map.forEach((token: Token) => { + if (!activeTokenAddresses.has(token.address)) { + map.remove(token.address) + } + }) }) - }) - saveActiveTokens(activeTokens) + saveActiveTokens(activeTokens) + } + break } - - if (action.type === ADD_SAFE) { + case ADD_SAFE: { const { safe } = action.payload setOwners(safe.address, safe.owners) - } else if (action.type === REMOVE_SAFE) { - const safeAddress = action.payload - removeOwners(safeAddress) + break + } + case UPDATE_SAFE: { + const { safeAddress, owners } = action.payload + if (safeAddress && owners) { + setOwners(safeAddress, owners) + } + break + } + case REMOVE_SAFE: { + const { safeAddress } = action.payload + await removeOwners(safeAddress) + break + } + case ADD_SAFE_OWNER: { + const { safeAddress, ownerAddress, ownerName } = action.payload + const owners = List(safes.get(safeAddress).owners) + setOwners(safeAddress, owners.push(makeOwner({ address: ownerAddress, name: ownerName }))) + break + } + case REMOVE_SAFE_OWNER: { + const { safeAddress, ownerAddress } = action.payload + const owners = List(safes.get(safeAddress).owners) + setOwners(safeAddress, owners.filter(o => o.address.toLowerCase() !== ownerAddress.toLowerCase())) + break + } + case REPLACE_SAFE_OWNER: { + const { + safeAddress, ownerAddress, ownerName, oldOwnerAddress, + } = action.payload + const owners = List(safes.get(safeAddress).owners) + setOwners( + safeAddress, + owners + .filter(o => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase()) + .push(makeOwner({ address: ownerAddress, name: ownerName })), + ) + break + } + case EDIT_SAFE_OWNER: { + const { safeAddress, ownerAddress, ownerName } = action.payload + const owners = List(safes.get(safeAddress).owners) + const ownerToUpdateIndex = owners.findIndex(o => o.address.toLowerCase() === ownerAddress.toLowerCase()) + setOwners(safeAddress, owners.update(ownerToUpdateIndex, owner => owner.set('name', ownerName))) + break + } + default: + break } } diff --git a/src/routes/safe/store/reducer/safe.js b/src/routes/safe/store/reducer/safe.js index c3aff170..088ff9a6 100644 --- a/src/routes/safe/store/reducer/safe.js +++ b/src/routes/safe/store/reducer/safe.js @@ -4,12 +4,16 @@ import { handleActions, type ActionType } from 'redux-actions' import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe' import SafeRecord, { type Safe, type SafeProps } from '~/routes/safe/store/models/safe' import TokenBalance from '~/routes/safe/store/models/tokenBalance' -import { type OwnerProps } from '~/routes/safe/store/models/owner' +import { makeOwner, type OwnerProps } from '~/routes/safe/store/models/owner' import { loadFromStorage } from '~/utils/storage' import { SAFES_KEY } from '~/logic/safe/utils' import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe' import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes' import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe' +import { ADD_SAFE_OWNER } from '~/routes/safe/store/actions/addSafeOwner' +import { REMOVE_SAFE_OWNER } from '~/routes/safe/store/actions/removeSafeOwner' +import { REPLACE_SAFE_OWNER } from '~/routes/safe/store/actions/replaceSafeOwner' +import { EDIT_SAFE_OWNER } from '~/routes/safe/store/actions/editSafeOwner' export const SAFE_REDUCER_ID = 'safes' @@ -97,6 +101,42 @@ export default handleActions( return state.delete(safeAddress) }, + [ADD_SAFE_OWNER]: (state: State, action: ActionType): State => { + const { safeAddress, ownerName, ownerAddress } = action.payload + + return state.update(safeAddress, prevSafe => prevSafe.merge({ + owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })), + })) + }, + [REMOVE_SAFE_OWNER]: (state: State, action: ActionType): State => { + const { safeAddress, ownerAddress } = action.payload + + return state.update(safeAddress, prevSafe => prevSafe.merge({ + owners: prevSafe.owners.filter(o => o.address.toLowerCase() !== ownerAddress.toLowerCase()), + })) + }, + [REPLACE_SAFE_OWNER]: (state: State, action: ActionType): State => { + const { + safeAddress, oldOwnerAddress, ownerName, ownerAddress, + } = action.payload + + return state.update(safeAddress, prevSafe => prevSafe.merge({ + owners: prevSafe.owners + .filter(o => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase()) + .push(makeOwner({ address: ownerAddress, name: ownerName })), + })) + }, + [EDIT_SAFE_OWNER]: (state: State, action: ActionType): State => { + const { safeAddress, ownerAddress, ownerName } = action.payload + + return state.update(safeAddress, (prevSafe) => { + const ownerToUpdateIndex = prevSafe.owners.findIndex( + o => o.address.toLowerCase() === ownerAddress.toLowerCase(), + ) + const updatedOwners = prevSafe.owners.update(ownerToUpdateIndex, owner => owner.set('name', ownerName)) + return prevSafe.merge({ owners: updatedOwners }) + }) + }, }, Map(), ) diff --git a/src/test/builder/safe.dom.utils.js b/src/test/builder/safe.dom.utils.js index 3234e3c5..72100de5 100644 --- a/src/test/builder/safe.dom.utils.js +++ b/src/test/builder/safe.dom.utils.js @@ -3,11 +3,10 @@ import * as React from 'react' import TestUtils from 'react-dom/test-utils' import { type Store } from 'redux' import { Provider } from 'react-redux' +import { render } from '@testing-library/react' import { ConnectedRouter } from 'connected-react-router' import PageFrame from '~/components/layout/PageFrame' -import { render } from '@testing-library/react' import ListItemText from '~/components/List/ListItemText/index' -import { SEE_MULTISIG_BUTTON_TEXT } from '~/routes/safe/components/Safe/MultisigTx' import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' import { sleep } from '~/utils/timer' import { history } from '~/store' @@ -23,16 +22,6 @@ export const EDIT_INDEX = 4 export const WITHDRAW_INDEX = 5 export const LIST_TXS_INDEX = 6 -export const listTxsClickingOn = async (store: Store, seeTxsButton: Element, safeAddress: string) => { - await store.dispatch(fetchTransactions(safeAddress)) - await sleep(1200) - expect(seeTxsButton.getElementsByTagName('span')[0].innerHTML).toEqual(SEE_MULTISIG_BUTTON_TEXT) - TestUtils.Simulate.click(seeTxsButton) - - // give some time to expand the transactions - await sleep(800) -} - export const checkMinedTx = (Transaction: React.Component, name: string) => { const paragraphs = TestUtils.scryRenderedDOMComponentsWithTag(Transaction, 'p') diff --git a/src/test/safe.dom.settings.test.js b/src/test/safe.dom.settings.name.test.js similarity index 96% rename from src/test/safe.dom.settings.test.js rename to src/test/safe.dom.settings.name.test.js index c635e9d0..2cfd40b5 100644 --- a/src/test/safe.dom.settings.test.js +++ b/src/test/safe.dom.settings.name.test.js @@ -10,7 +10,7 @@ import { SAFE_NAME_INPUT_TESTID, SAFE_NAME_SUBMIT_BTN_TESTID } from '~/routes/sa afterEach(cleanup) -describe('DOM > Feature > Settings', () => { +describe('DOM > Feature > Settings - Name', () => { let store let safeAddress beforeEach(async () => { diff --git a/src/test/safe.dom.settings.owners.test.js b/src/test/safe.dom.settings.owners.test.js new file mode 100644 index 00000000..654b78e7 --- /dev/null +++ b/src/test/safe.dom.settings.owners.test.js @@ -0,0 +1,228 @@ +// @flow +import { fireEvent, cleanup } from '@testing-library/react' +import { aNewStore } from '~/store' +import { aMinedSafe } from '~/test/builder/safe.redux.builder' +import { renderSafeView } from '~/test/builder/safe.dom.utils' +import { sleep } from '~/utils/timer' +import 'jest-dom/extend-expect' +import { SETTINGS_TAB_BTN_TESTID } from '~/routes/safe/components/Layout' +import { OWNERS_SETTINGS_TAB_TESTID } from '~/routes/safe/components/Settings' +import { + RENAME_OWNER_BTN_TESTID, + OWNERS_ROW_TESTID, + REMOVE_OWNER_BTN_TESTID, + ADD_OWNER_BTN_TESTID, + REPLACE_OWNER_BTN_TESTID, +} from '~/routes/safe/components/Settings/ManageOwners' +import { + RENAME_OWNER_INPUT_TESTID, + SAVE_OWNER_CHANGES_BTN_TESTID, +} from '~/routes/safe/components/Settings/ManageOwners/EditOwnerModal' +import { REMOVE_OWNER_MODAL_NEXT_BTN_TESTID } from '~/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner' +import { REMOVE_OWNER_THRESHOLD_NEXT_BTN_TESTID } from '~/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm' +import { REMOVE_OWNER_REVIEW_BTN_TESTID } from '~/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review' +import { ADD_OWNER_THRESHOLD_NEXT_BTN_TESTID } from '~/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm' +import { + ADD_OWNER_NAME_INPUT_TESTID, + ADD_OWNER_ADDRESS_INPUT_TESTID, + ADD_OWNER_NEXT_BTN_TESTID, +} from '~/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm' +import { ADD_OWNER_SUBMIT_BTN_TESTID } from '~/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review' +import { + REPLACE_OWNER_NEXT_BTN_TESTID, + REPLACE_OWNER_NAME_INPUT_TESTID, + REPLACE_OWNER_ADDRESS_INPUT_TESTID, +} from '~/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm' +import { REPLACE_OWNER_SUBMIT_BTN_TESTID } from '~/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review' + +afterEach(cleanup) + +describe('DOM > Feature > Settings - Manage owners', () => { + let store + let safeAddress + beforeEach(async () => { + store = aNewStore() + safeAddress = await aMinedSafe(store) + }) + + it("Changes owner's name", async () => { + const NEW_OWNER_NAME = 'NEW OWNER NAME' + + const SafeDom = renderSafeView(store, safeAddress) + await sleep(1300) + + // Travel to settings + const settingsBtn = SafeDom.getByTestId(SETTINGS_TAB_BTN_TESTID) + fireEvent.click(settingsBtn) + await sleep(200) + + // click on owners settings + const ownersSettingsBtn = SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TESTID) + fireEvent.click(ownersSettingsBtn) + await sleep(200) + + // open rename owner modal + const renameOwnerBtn = SafeDom.getByTestId(RENAME_OWNER_BTN_TESTID) + fireEvent.click(renameOwnerBtn) + + // rename owner + const ownerNameInput = SafeDom.getByTestId(RENAME_OWNER_INPUT_TESTID) + const saveOwnerChangesBtn = SafeDom.getByTestId(SAVE_OWNER_CHANGES_BTN_TESTID) + fireEvent.change(ownerNameInput, { target: { value: NEW_OWNER_NAME } }) + fireEvent.click(saveOwnerChangesBtn) + await sleep(200) + + // check if the name updated + const ownerRow = SafeDom.getByTestId(OWNERS_ROW_TESTID) + expect(ownerRow).toHaveTextContent(NEW_OWNER_NAME) + }) + + it('Removes an owner', async () => { + const twoOwnersSafeAddress = await aMinedSafe(store, 2) + + const SafeDom = renderSafeView(store, twoOwnersSafeAddress) + await sleep(1300) + + // Travel to settings + const settingsBtn = SafeDom.getByTestId(SETTINGS_TAB_BTN_TESTID) + fireEvent.click(settingsBtn) + await sleep(200) + + // click on owners settings + const ownersSettingsBtn = SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TESTID) + fireEvent.click(ownersSettingsBtn) + await sleep(200) + + // check if there are 2 owners + let ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID) + expect(ownerRows.length).toBe(2) + expect(ownerRows[0]).toHaveTextContent('Adol 1 Eth Account') + expect(ownerRows[0]).toHaveTextContent('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1') + expect(ownerRows[1]).toHaveTextContent('Adol 2 Eth Account') + expect(ownerRows[1]).toHaveTextContent('0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0') + + // click remove owner btn which opens the modal + const removeOwnerBtn = SafeDom.getAllByTestId(REMOVE_OWNER_BTN_TESTID)[1] + fireEvent.click(removeOwnerBtn) + + // modal navigation + const nextBtnStep1 = SafeDom.getByTestId(REMOVE_OWNER_MODAL_NEXT_BTN_TESTID) + fireEvent.click(nextBtnStep1) + + const nextBtnStep2 = SafeDom.getByTestId(REMOVE_OWNER_THRESHOLD_NEXT_BTN_TESTID) + fireEvent.click(nextBtnStep2) + + const nextBtnStep3 = SafeDom.getByTestId(REMOVE_OWNER_REVIEW_BTN_TESTID) + fireEvent.click(nextBtnStep3) + await sleep(1300) + + // check if owner was removed + ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID) + expect(ownerRows.length).toBe(1) + expect(ownerRows[0]).toHaveTextContent('Adol 1 Eth Account') + expect(ownerRows[0]).toHaveTextContent('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1') + }) + + it('Adds a new owner', async () => { + const NEW_OWNER_NAME = 'I am a new owner' + const NEW_OWNER_ADDRESS = '0x0E329Fa8d6fCd1BA0cDA495431F1F7ca24F442c3' + + const SafeDom = renderSafeView(store, safeAddress) + await sleep(1300) + + // Travel to settings + const settingsBtn = SafeDom.getByTestId(SETTINGS_TAB_BTN_TESTID) + fireEvent.click(settingsBtn) + await sleep(200) + + // click on owners settings + const ownersSettingsBtn = SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TESTID) + fireEvent.click(ownersSettingsBtn) + await sleep(200) + + // check if there is 1 owner + let ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID) + expect(ownerRows.length).toBe(1) + expect(ownerRows[0]).toHaveTextContent('Adol 1 Eth Account') + expect(ownerRows[0]).toHaveTextContent('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1') + + // click add owner btn + fireEvent.click(SafeDom.getByTestId(ADD_OWNER_BTN_TESTID)) + await sleep(200) + + // fill and travel add owner modal + const ownerNameInput = SafeDom.getByTestId(ADD_OWNER_NAME_INPUT_TESTID) + const ownerAddressInput = SafeDom.getByTestId(ADD_OWNER_ADDRESS_INPUT_TESTID) + const nextBtn = SafeDom.getByTestId(ADD_OWNER_NEXT_BTN_TESTID) + fireEvent.change(ownerNameInput, { target: { value: NEW_OWNER_NAME } }) + fireEvent.change(ownerAddressInput, { target: { value: NEW_OWNER_ADDRESS } }) + fireEvent.click(nextBtn) + await sleep(200) + + fireEvent.click(SafeDom.getByTestId(ADD_OWNER_THRESHOLD_NEXT_BTN_TESTID)) + await sleep(200) + fireEvent.click(SafeDom.getByTestId(ADD_OWNER_SUBMIT_BTN_TESTID)) + await sleep(1000) + + // check if owner was added + ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID) + 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) + }) + + it('Replaces an owner', async () => { + const NEW_OWNER_NAME = 'I replaced an old owner' + const NEW_OWNER_ADDRESS = '0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e' + + const twoOwnersSafeAddress = await aMinedSafe(store, 2) + + const SafeDom = renderSafeView(store, twoOwnersSafeAddress) + await sleep(1300) + + // Travel to settings + const settingsBtn = SafeDom.getByTestId(SETTINGS_TAB_BTN_TESTID) + fireEvent.click(settingsBtn) + await sleep(200) + + // click on owners settings + const ownersSettingsBtn = SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TESTID) + fireEvent.click(ownersSettingsBtn) + await sleep(200) + + // check if there are 2 owners + let ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID) + expect(ownerRows.length).toBe(2) + expect(ownerRows[0]).toHaveTextContent('Adol 1 Eth Account') + expect(ownerRows[0]).toHaveTextContent('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1') + expect(ownerRows[1]).toHaveTextContent('Adol 2 Eth Account') + expect(ownerRows[1]).toHaveTextContent('0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0') + + // click replace owner btn which opens the modal + const replaceOwnerBtn = SafeDom.getAllByTestId(REPLACE_OWNER_BTN_TESTID)[1] + fireEvent.click(replaceOwnerBtn) + + // fill and travel add owner modal + const ownerNameInput = SafeDom.getByTestId(REPLACE_OWNER_NAME_INPUT_TESTID) + const ownerAddressInput = SafeDom.getByTestId(REPLACE_OWNER_ADDRESS_INPUT_TESTID) + const nextBtn = SafeDom.getByTestId(REPLACE_OWNER_NEXT_BTN_TESTID) + fireEvent.change(ownerNameInput, { target: { value: NEW_OWNER_NAME } }) + fireEvent.change(ownerAddressInput, { target: { value: NEW_OWNER_ADDRESS } }) + fireEvent.click(nextBtn) + await sleep(200) + + const replaceSubmitBtn = SafeDom.getByTestId(REPLACE_OWNER_SUBMIT_BTN_TESTID) + fireEvent.click(replaceSubmitBtn) + await sleep(1000) + + // check if the owner was replaced + ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID) + 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) + }) +}) diff --git a/src/test/safe.dom.transactions.test.js b/src/test/safe.dom.transactions.test.js index ba93243c..cf6474da 100644 --- a/src/test/safe.dom.transactions.test.js +++ b/src/test/safe.dom.transactions.test.js @@ -3,15 +3,9 @@ // TBD describe('DOM > Feature > SAFE MULTISIG Transactions', () => { - it.only('mines correctly all multisig txs in a 1 owner & 1 threshold safe', async () => { + it.only('mines correctly all multisig txs in a 1 owner & 1 threshold safe', async () => {}) - }) + it.only('mines withdraw process correctly all multisig txs in a 2 owner & 2 threshold safe', async () => {}) - it.only('mines withdraw process correctly all multisig txs in a 2 owner & 2 threshold safe', async () => { - - }) - - it.only('approves and executes pending transactions', async () => { - - }) + it.only('approves and executes pending transactions', async () => {}) }) diff --git a/src/test/tokens.dom.adding.test.js b/src/test/tokens.dom.adding.test.js index a77c734a..0daf9b76 100644 --- a/src/test/tokens.dom.adding.test.js +++ b/src/test/tokens.dom.adding.test.js @@ -1,6 +1,6 @@ // @flow -import { getWeb3 } from '~/logic/wallets/getWeb3' import { fireEvent } from '@testing-library/react' +import { getWeb3 } from '~/logic/wallets/getWeb3' import { getFirstTokenContract } from '~/test/utils/tokenMovements' import { aNewStore } from '~/store' import { aMinedSafe } from '~/test/builder/safe.redux.builder' diff --git a/src/test/utils/DOMNavigation/tokens.js b/src/test/utils/DOMNavigation/tokens.js index 7ed1a422..698933fd 100644 --- a/src/test/utils/DOMNavigation/tokens.js +++ b/src/test/utils/DOMNavigation/tokens.js @@ -1,7 +1,10 @@ // @flow import { fireEvent } from '@testing-library/react' import { MANAGE_TOKENS_BUTTON_TEST_ID } from '~/routes/safe/components/Balances' -import { ADD_CUSTOM_TOKEN_BUTTON_TEST_ID, TOGGLE_TOKEN_TEST_ID } from '~/routes/safe/components/Balances/Tokens/screens/TokenList' +import { + ADD_CUSTOM_TOKEN_BUTTON_TEST_ID, + TOGGLE_TOKEN_TEST_ID, +} from '~/routes/safe/components/Balances/Tokens/screens/TokenList' import { MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID } from '~/routes/safe/components/Balances/Tokens' export const clickOnManageTokens = (dom: any): void => { diff --git a/src/test/utils/ethereumErrors.js b/src/test/utils/ethereumErrors.js index f4f0cd43..73bf6c02 100644 --- a/src/test/utils/ethereumErrors.js +++ b/src/test/utils/ethereumErrors.js @@ -1,6 +1,6 @@ // @flow -import { getWeb3 } from '~/logic/wallets/getWeb3' import abi from 'ethereumjs-abi' +import { getWeb3 } from '~/logic/wallets/getWeb3' /* console.log(`to[${to}] \n\n valieInWei[${valueInWei}] \n\n diff --git a/src/test/utils/logTransactions.js b/src/test/utils/logTransactions.js index e76f197e..f73c4d2a 100644 --- a/src/test/utils/logTransactions.js +++ b/src/test/utils/logTransactions.js @@ -1,9 +1,9 @@ // @flow import React from 'react' -import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' -import GnoStepper from '~/components/Stepper' import Stepper from '@material-ui/core/Stepper' import TestUtils from 'react-dom/test-utils' +import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' +import GnoStepper from '~/components/Stepper' export const printOutApprove = async ( subject: string, @@ -38,7 +38,10 @@ type FinsihedTx = { finishedTransaction: boolean, } -export const whenExecuted = (SafeDom: React.Component, ParentComponent: React.ElementType): Promise => new Promise((resolve, reject) => { +export const whenExecuted = ( + SafeDom: React.Component, + ParentComponent: React.ElementType, +): Promise => new Promise((resolve, reject) => { let times = 0 const interval = setInterval(() => { if (times >= MAX_TIMES_EXECUTED) { diff --git a/src/test/utils/safeHelper.js b/src/test/utils/safeHelper.js index 6449ff9d..2e7c2b5b 100644 --- a/src/test/utils/safeHelper.js +++ b/src/test/utils/safeHelper.js @@ -1,7 +1,7 @@ // @flow +import { type Match } from 'react-router-dom' import { buildMatchPropsFrom } from '~/test/utils/buildReactRouterProps' import { safeSelector } from '~/routes/safe/store/selectors/index' -import { type Match } from 'react-router-dom' import { type GlobalState } from '~/store' import { type Safe } from '~/routes/safe/store/models/safe' diff --git a/src/test/utils/transactions/addOwner.helper.js b/src/test/utils/transactions/addOwner.helper.js deleted file mode 100644 index fe038f3d..00000000 --- a/src/test/utils/transactions/addOwner.helper.js +++ /dev/null @@ -1,53 +0,0 @@ -// @flow -import TestUtils from 'react-dom/test-utils' -import { sleep } from '~/utils/timer' -import { checkMinedTx, checkPendingTx } from '~/test/builder/safe.dom.utils' -import { whenExecuted } from '~/test/utils/logTransactions' -import AddOwner from '~/routes/safe/components/AddOwner' - -export const sendAddOwnerForm = async ( - SafeDom: React.Component, - addOwner: React.Component, - ownerName: string, - ownerAddress: string, - increase: boolean = false, -) => { - // load add multisig form component - TestUtils.Simulate.click(addOwner) - // give time to re-render it - await sleep(400) - - // fill the form - const inputs = TestUtils.scryRenderedDOMComponentsWithTag(SafeDom, 'input') - const nameInput = inputs[0] - const addressInput = inputs[1] - TestUtils.Simulate.change(nameInput, { target: { value: ownerName } }) - TestUtils.Simulate.change(addressInput, { target: { value: ownerAddress } }) - - if (increase) { - const increaseInput = inputs[2] - TestUtils.Simulate.change(increaseInput, { target: { value: 'true' } }) - } - - // $FlowFixMe - const form = TestUtils.findRenderedDOMComponentWithTag(SafeDom, 'form') - - // submit it - TestUtils.Simulate.submit(form) - TestUtils.Simulate.submit(form) - - return whenExecuted(SafeDom, AddOwner) -} - -export const checkMinedAddOwnerTx = (Transaction: React.Component, name: string) => { - checkMinedTx(Transaction, name) -} - -export const checkPendingAddOwnerTx = async ( - Transaction: React.Component, - safeThreshold: number, - name: string, - statusses: string[], -) => { - await checkPendingTx(Transaction, safeThreshold, name, statusses) -} diff --git a/src/test/utils/transactions/removeOwner.helper.js b/src/test/utils/transactions/removeOwner.helper.js deleted file mode 100644 index fd492ae9..00000000 --- a/src/test/utils/transactions/removeOwner.helper.js +++ /dev/null @@ -1,49 +0,0 @@ -// @flow -import * as React from 'react' -import TestUtils from 'react-dom/test-utils' -import { sleep } from '~/utils/timer' -import { checkMinedTx, EXPAND_OWNERS_INDEX, checkPendingTx } from '~/test/builder/safe.dom.utils' -import { filterMoveButtonsFrom } from '~/test/builder/safe.dom.builder' -import { whenExecuted } from '~/test/utils/logTransactions' -import RemoveOwner from '~/routes/safe/components/RemoveOwner' - -export const sendRemoveOwnerForm = async ( - SafeDom: React.Component, - expandOwners: React.Component, -) => { - // Expand owners - TestUtils.Simulate.click(expandOwners) - await sleep(400) - - // Get delete button user - const allButtons = TestUtils.scryRenderedDOMComponentsWithTag(SafeDom, 'button') - const buttons = filterMoveButtonsFrom(allButtons) - const removeUserButton = buttons[EXPAND_OWNERS_INDEX + 2] // + 2 one the Add and the next delete - expect(removeUserButton.getAttribute('aria-label')).toBe('Delete') - - // render form for deleting the user - TestUtils.Simulate.click(removeUserButton) - await sleep(400) - - // $FlowFixMe - const form = TestUtils.findRenderedDOMComponentWithTag(SafeDom, 'form') - - // submit it - TestUtils.Simulate.submit(form) - TestUtils.Simulate.submit(form) - - return whenExecuted(SafeDom, RemoveOwner) -} - -export const checkMinedRemoveOwnerTx = (Transaction: React.Component, name: string) => { - checkMinedTx(Transaction, name) -} - -export const checkPendingRemoveOwnerTx = async ( - Transaction: React.Component, - safeThreshold: number, - name: string, - statusses: string[], -) => { - await checkPendingTx(Transaction, safeThreshold, name, statusses) -} diff --git a/yarn.lock b/yarn.lock index 39959f66..27566875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@babel/cli@7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.4.tgz#5454bb7112f29026a4069d8e6f0e1794e651966c" - integrity sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ== +"@babel/cli@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.5.0.tgz#f403c930692e28ecfa3bf02a9e7562b474f38271" + integrity sha512-qNH55fWbKrEsCwID+Qc/3JDPnsSGpIIiMDbppnR8Z6PxLAqMQCFNqBctkIkBrMH49Nx+qqVTrHRWUR+ho2k+qQ== dependencies: commander "^2.8.1" convert-source-map "^1.1.0" @@ -46,7 +46,27 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@7.4.5", "@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.4.3": +"@babel/core@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.0.tgz#6ed6a2881ad48a732c5433096d96d1b0ee5eb734" + integrity sha512-6Isr4X98pwXqHvtigw71CKgmhL1etZjPs5A67jL/w0TkLM9eqmFR40YrnJvEc1WnMZFsskjsmid8bHZyxKEAnw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.5.0" + "@babel/helpers" "^7.5.0" + "@babel/parser" "^7.5.0" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.5.0" + "@babel/types" "^7.5.0" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.11" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.4.3": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a" integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA== @@ -77,6 +97,17 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.0.tgz#f20e4b7a91750ee8b63656073d843d2a736dca4a" + integrity sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA== + dependencies: + "@babel/types" "^7.5.0" + jsesc "^2.5.1" + lodash "^4.17.11" + source-map "^0.5.0" + trim-right "^1.0.1" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -121,6 +152,18 @@ "@babel/helper-replace-supers" "^7.4.4" "@babel/helper-split-export-declaration" "^7.4.4" +"@babel/helper-create-class-features-plugin@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.0.tgz#02edb97f512d44ba23b3227f1bf2ed43454edac5" + integrity sha512-EAoMc3hE5vE5LNhMqDOwB1usHvmRjCDAnH8CD4PVkX9/Yr3W/tcz8xE8QvdZxfsFBDICwZnF2UTHIqslRpvxmA== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-member-expression-to-functions" "^7.0.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.4.4" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/helper-define-map@^7.4.0", "@babel/helper-define-map@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a" @@ -261,6 +304,15 @@ "@babel/traverse" "^7.4.4" "@babel/types" "^7.4.4" +"@babel/helpers@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.0.tgz#7f0c17666e7ed8355ed6eff643dde12fb681ddb4" + integrity sha512-EgCUEa8cNwuMrwo87l2d7i2oShi8m2Q58H7h3t4TWtqATZalJYFwfL9DulRe02f3KdqM9xmMCw3v/7Ll+EiaWg== + dependencies: + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.5.0" + "@babel/types" "^7.5.0" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -275,6 +327,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== +"@babel/parser@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7" + integrity sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA== + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -292,7 +349,15 @@ "@babel/helper-create-class-features-plugin" "^7.4.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-proposal-class-properties@7.4.4", "@babel/plugin-proposal-class-properties@^7.3.3": +"@babel/plugin-proposal-class-properties@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.0.tgz#5bc6a0537d286fcb4fd4e89975adbca334987007" + integrity sha512-9L/JfPCT+kShiiTTzcnBJ8cOwdKVmlC1RcCf9F0F9tERVrM4iWtWnXtjWCRqNm2la2BxO1MPArWNsU9zsSJWSQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.5.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-proposal-class-properties@^7.3.3": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce" integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg== @@ -326,6 +391,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-do-expressions" "^7.2.0" +"@babel/plugin-proposal-dynamic-import@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506" + integrity sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-dynamic-import" "^7.2.0" + "@babel/plugin-proposal-export-default-from@^7.0.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.2.0.tgz#737b0da44b9254b6152fe29bb99c64e5691f6f68" @@ -407,6 +480,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" +"@babel/plugin-proposal-object-rest-spread@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.0.tgz#4838ce3cbc9a84dd00bce7a17e9e9c36119f83a0" + integrity sha512-G1qy5EdcO3vYhbxlXjRSR2SXB8GsxYv9hoRKT1Jdn3qy/NUnFqUUnqymKZ00Pbj+3FXNh06B+BUZzecrp3sxNw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" @@ -611,6 +692,15 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-remap-async-to-generator" "^7.1.0" +"@babel/plugin-transform-async-to-generator@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e" + integrity sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-remap-async-to-generator" "^7.1.0" + "@babel/plugin-transform-block-scoped-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" @@ -675,6 +765,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-destructuring@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a" + integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-dotall-regex@^7.4.3", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" @@ -691,6 +788,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-duplicate-keys@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853" + integrity sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-exponentiation-operator@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" @@ -752,6 +856,15 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-modules-amd@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91" + integrity sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg== + dependencies: + "@babel/helper-module-transforms" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-dynamic-import-node "^2.3.0" + "@babel/plugin-transform-modules-commonjs@^7.4.3", "@babel/plugin-transform-modules-commonjs@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e" @@ -761,6 +874,16 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" +"@babel/plugin-transform-modules-commonjs@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74" + integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ== + dependencies: + "@babel/helper-module-transforms" "^7.4.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-simple-access" "^7.1.0" + babel-plugin-dynamic-import-node "^2.3.0" + "@babel/plugin-transform-modules-systemjs@^7.4.0", "@babel/plugin-transform-modules-systemjs@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405" @@ -769,6 +892,15 @@ "@babel/helper-hoist-variables" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-modules-systemjs@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249" + integrity sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg== + dependencies: + "@babel/helper-hoist-variables" "^7.4.4" + "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-dynamic-import-node "^2.3.0" + "@babel/plugin-transform-modules-umd@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" @@ -995,7 +1127,63 @@ js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/preset-env@7.4.5", "@babel/preset-env@^7.4.3", "@babel/preset-env@^7.4.5": +"@babel/preset-env@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.0.tgz#1122a751e864850b4dbce38bd9b4497840ee6f01" + integrity sha512-/5oQ7cYg+6sH9Dt9yx5IiylnLPiUdyMHl5y+K0mKVNiW2wJ7FpU5bg8jKcT8PcCbxdYzfv6OuC63jLEtMuRSmQ== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-async-generator-functions" "^7.2.0" + "@babel/plugin-proposal-dynamic-import" "^7.5.0" + "@babel/plugin-proposal-json-strings" "^7.2.0" + "@babel/plugin-proposal-object-rest-spread" "^7.5.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-syntax-async-generators" "^7.2.0" + "@babel/plugin-syntax-dynamic-import" "^7.2.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" + "@babel/plugin-transform-arrow-functions" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.5.0" + "@babel/plugin-transform-block-scoped-functions" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.4.4" + "@babel/plugin-transform-classes" "^7.4.4" + "@babel/plugin-transform-computed-properties" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.5.0" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/plugin-transform-duplicate-keys" "^7.5.0" + "@babel/plugin-transform-exponentiation-operator" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.4.4" + "@babel/plugin-transform-function-name" "^7.4.4" + "@babel/plugin-transform-literals" "^7.2.0" + "@babel/plugin-transform-member-expression-literals" "^7.2.0" + "@babel/plugin-transform-modules-amd" "^7.5.0" + "@babel/plugin-transform-modules-commonjs" "^7.5.0" + "@babel/plugin-transform-modules-systemjs" "^7.5.0" + "@babel/plugin-transform-modules-umd" "^7.2.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5" + "@babel/plugin-transform-new-target" "^7.4.4" + "@babel/plugin-transform-object-super" "^7.2.0" + "@babel/plugin-transform-parameters" "^7.4.4" + "@babel/plugin-transform-property-literals" "^7.2.0" + "@babel/plugin-transform-regenerator" "^7.4.5" + "@babel/plugin-transform-reserved-words" "^7.2.0" + "@babel/plugin-transform-shorthand-properties" "^7.2.0" + "@babel/plugin-transform-spread" "^7.2.0" + "@babel/plugin-transform-sticky-regex" "^7.2.0" + "@babel/plugin-transform-template-literals" "^7.4.4" + "@babel/plugin-transform-typeof-symbol" "^7.2.0" + "@babel/plugin-transform-unicode-regex" "^7.4.4" + "@babel/types" "^7.5.0" + browserslist "^4.6.0" + core-js-compat "^3.1.1" + invariant "^2.2.2" + js-levenshtein "^1.1.3" + semver "^5.5.0" + +"@babel/preset-env@^7.4.3", "@babel/preset-env@^7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58" integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w== @@ -1129,6 +1317,21 @@ globals "^11.1.0" lodash "^4.17.11" +"@babel/traverse@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485" + integrity sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.5.0" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.5.0" + "@babel/types" "^7.5.0" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.11" + "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" @@ -1138,6 +1341,15 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" +"@babel/types@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab" + integrity sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -3316,7 +3528,7 @@ babel-plugin-dynamic-import-node@2.2.0: dependencies: object.assign "^4.1.0" -babel-plugin-dynamic-import-node@^2.2.0: +babel-plugin-dynamic-import-node@^2.2.0, babel-plugin-dynamic-import-node@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== @@ -6535,10 +6747,10 @@ eslint-plugin-import@2.18.0: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jest@22.7.1: - version "22.7.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.1.tgz#5dcdf8f7a285f98040378220d6beca581f0ab2a1" - integrity sha512-CrT3AzA738neimv8G8iK2HCkrCwHnAJeeo7k5TEHK86VMItKl6zdJT/tHBDImfnVVAYsVs4Y6BUdBZQCCgfiyw== +eslint-plugin-jest@22.7.2: + version "22.7.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.2.tgz#7ab118a66a34e46ae5e16a128b5d24fd28b43dca" + integrity sha512-Aecqe3ulBVI7amgOycVI8ZPL8o0SnGHOf3zn2/Ciu8TXyXDHcjtwD3hOs3ss/Qh/VAwlW/DMcuiXg5btgF+XMA== eslint-plugin-jsx-a11y@^6.0.3: version "6.2.1" @@ -7768,12 +7980,12 @@ fs-extra@6.0.1, fs-extra@^6.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@8.0.1, fs-extra@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.0.1.tgz#90294081f978b1f182f347a440a209154344285b" - integrity sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A== +fs-extra@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: - graceful-fs "^4.1.2" + graceful-fs "^4.2.0" jsonfile "^4.0.0" universalify "^0.1.0" @@ -7824,6 +8036,15 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.0.1.tgz#90294081f978b1f182f347a440a209154344285b" + integrity sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" @@ -8258,6 +8479,11 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1. resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== +graceful-fs@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" + integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== + "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" @@ -13320,10 +13546,10 @@ react-helmet-async@^1.0.2: react-fast-compare "2.0.4" shallowequal "1.1.0" -react-hot-loader@4.11.1: - version "4.11.1" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.11.1.tgz#2cabbd0f1c8a44c28837b86d6ce28521e6d9a8ac" - integrity sha512-HAC0UedYzM3mD+ZaQHesntFO0yi2ftOV4ZMMRTj43E4GvW5sQqYTPvur+6J7EaH3MDr/RqjDKXyCqKepV8+y7w== +react-hot-loader@4.12.3: + version "4.12.3" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.3.tgz#0972255cd110a00860902e82bb2b789a262cfe01" + integrity sha512-XBhxogFOxEh8L4Ykdk2mp704Xc/eoy+bwadEYMvmBhjAz3wg+DfMpINMkA+kLTRDinqwjssDfA9DhUJznRjvuA== dependencies: fast-levenshtein "^2.0.6" global "^4.3.0" @@ -13987,7 +14213,7 @@ request-promise-core@1.1.2: dependencies: lodash "^4.17.11" -request-promise-native@^1.0.5: +request-promise-native@^1.0.5, request-promise-native@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== @@ -15729,10 +15955,10 @@ truffle-artifactor@^2.1.2: truffle-contract "^2.0.3" truffle-contract-schema "^0.0.5" -truffle-artifactor@^4.0.20: - version "4.0.20" - resolved "https://registry.yarnpkg.com/truffle-artifactor/-/truffle-artifactor-4.0.20.tgz#1baba681f407485fda7c408976ce0ad31a13d5ac" - integrity sha512-IX+KvGNPkDQlMW1+9txYYn1+6IkvLPFiIh+hIjj2E7yYg9XS3zYxYbWg/wTcGyZsdtHwRVLOks7EvA0k+5LD1w== +truffle-artifactor@^4.0.22: + version "4.0.22" + resolved "https://registry.yarnpkg.com/truffle-artifactor/-/truffle-artifactor-4.0.22.tgz#3ca2061842ac0803b83538a11126fd2d8480d4e6" + integrity sha512-jqgLahO/29TnWvBZAj9Vo58LUs82fR5FcPvof5sy8q+b4y/nRc+v4cOWFYIZPkLo8sRRotN/U0T17S7CyrR9wQ== dependencies: fs-extra "6.0.1" lodash "4.17.11" @@ -15755,15 +15981,16 @@ truffle-blockchain-utils@^0.0.5: resolved "https://registry.yarnpkg.com/truffle-blockchain-utils/-/truffle-blockchain-utils-0.0.5.tgz#a4e5c064dadd69f782a137f3d276d21095da7a47" integrity sha1-pOXAZNrdafeCoTfz0nbSEJXaekc= -truffle-box@^1.0.28: - version "1.0.28" - resolved "https://registry.yarnpkg.com/truffle-box/-/truffle-box-1.0.28.tgz#da117d387db4048e2e556080739a60e446f71dda" - integrity sha512-mOmo2EdB0gUcV84pB6xPPhTsycLjjjz90kSd4V47IqJhx6dDSx6jEs/wBDNcA0gcEVf54x1IcuBgvnPZhH0wXw== +truffle-box@^1.0.29: + version "1.0.29" + resolved "https://registry.yarnpkg.com/truffle-box/-/truffle-box-1.0.29.tgz#dd785925541f2bb024e0fe34276d1f53af6546e6" + integrity sha512-QdhEIAae+Jdf/QvheOFrz0gVaR09Sy9Q36FG2lCJVmrA0hxTT+JWJxQ7EXn3CyZD03KGy8ORUHyDAJX8bX/QUA== dependencies: fs-extra "6.0.1" github-download "^0.5.0" ora "^3.0.0" request "^2.85.0" + request-promise-native "^1.0.7" tmp "0.0.33" truffle-config "^1.1.13" vcsurl "^0.1.1" @@ -15773,25 +16000,25 @@ truffle-code-utils@^1.2.4: resolved "https://registry.yarnpkg.com/truffle-code-utils/-/truffle-code-utils-1.2.4.tgz#19acbc225a5c99081c7aa4bbda122f6fbf2d27e9" integrity sha512-MtIusUMxJeJlOqoqda4DYJ86gqSGDVMGiqhVEVgHL2J5L4plci/uePgYROajklXE9H/g6u7yotnAKOhCdTB9/A== -truffle-compile-vyper@^1.0.18: - version "1.0.18" - resolved "https://registry.yarnpkg.com/truffle-compile-vyper/-/truffle-compile-vyper-1.0.18.tgz#f9ed4c4141a76edfd1b32f6fd87eb95cd96495d3" - integrity sha512-MAsmYqY0eFklYtG3IBtBqlT0sibyq2yox2T356R1XGh/KpsFC1AbjyxsSTBL+pzpSb0ro3Fd2qZkluS1sSoijg== +truffle-compile-vyper@^1.0.20: + version "1.0.20" + resolved "https://registry.yarnpkg.com/truffle-compile-vyper/-/truffle-compile-vyper-1.0.20.tgz#3984ffb17ca2bc9a4f207e1d930d0a27e22ff9f6" + integrity sha512-LkF+puWaSg+1AjMl/hWrQS2DlbrB3rDuN3Ap974gXTwagLUDc4yvmN6luQ3Wd5Irn89ugX6lYrzXaETfrEsD+Q== dependencies: async "2.6.1" colors "^1.1.2" eslint "^5.5.0" minimatch "^3.0.4" - truffle-compile "^4.1.1" + truffle-compile "^4.1.3" -truffle-compile@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/truffle-compile/-/truffle-compile-4.1.1.tgz#581a3a6b851282b17ba010b141b8c1f5c9667420" - integrity sha512-b4XTcK2L/nb11ekeZy3Q4cx0DRaoqrZ4UQfd0cp6CXq3IVqMuEF+lNXjHnu+tIbzhMN2Zov1xV5DtoJC3vOJmw== +truffle-compile@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/truffle-compile/-/truffle-compile-4.1.3.tgz#698a650d27b961b12e0594c078ec870a931eafde" + integrity sha512-pCMhw3r5+M9RNe37/Hvzjk8FrOvTXm3qpuGufynQCMWYcsOwLPyLrMRmgtTWL5sABP42JuNpM4xrfBDqatmv6Q== dependencies: - async "2.6.1" colors "^1.1.2" debug "^4.1.0" + fs-extra "^8.0.1" ora "^3.0.0" original-require "^1.0.1" request "^2.85.0" @@ -15800,7 +16027,7 @@ truffle-compile@^4.1.1: semver "^5.6.0" solc "^0.5.0" truffle-config "^1.1.13" - truffle-contract-sources "^0.1.4" + truffle-contract-sources "^0.1.5" truffle-error "^0.0.5" truffle-expect "^0.0.9" @@ -15841,10 +16068,10 @@ truffle-contract-schema@^3.0.11: crypto-js "^3.1.9-1" debug "^4.1.0" -truffle-contract-sources@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/truffle-contract-sources/-/truffle-contract-sources-0.1.4.tgz#bc6dacfe8a3744e00dc9238e11e70d8951c4c7b1" - integrity sha512-Evi9UEaOYz99XVkl11rrlAR6lho0rZzByEoqpdbPPku+z0ONeyqXls3fKoTtwp7Zrus4ihJKS/AIiQOSm93ccA== +truffle-contract-sources@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/truffle-contract-sources/-/truffle-contract-sources-0.1.5.tgz#cacd788b7904d53747fa88d04412b77a1826b46f" + integrity sha512-skhZ8s3sLM4PyRFqn8zbBWG2z1P5zX7ndQPYokr2aiez8o44x5hmUoPeiXgKlMYIyR1Y1zOyNpaVEZyBY1+4Cw== dependencies: debug "^4.1.0" glob "^7.1.2" @@ -15865,10 +16092,10 @@ truffle-contract@4.0.0-next.0: web3-eth-abi "1.0.0-beta.35" web3-utils "1.0.0-beta.35" -truffle-contract@4.0.21, truffle-contract@^4.0.21: - version "4.0.21" - resolved "https://registry.yarnpkg.com/truffle-contract/-/truffle-contract-4.0.21.tgz#aa336764d9712346ee1914b383b43a57f6ff3962" - integrity sha512-nFoEYDCyDihw3eDC7aHSJcNVP7WQbKl0N8umxXS5gAb3AujlNeV0OCV5lbAMMY4KwyfwaZp+uVjLnK/AnuSbww== +truffle-contract@4.0.23, truffle-contract@^4.0.23: + version "4.0.23" + resolved "https://registry.yarnpkg.com/truffle-contract/-/truffle-contract-4.0.23.tgz#a1e5a83972552bf81e879323eb1990970496db9f" + integrity sha512-aTiKSRoum01n9G77ESAqRvCoK8S5FBjNdN1NKkibAUfcdZTIm2VmFcmW702nKGX8hMAU+pSY3cXNaI8VhIdAfQ== dependencies: bignumber.js "^7.2.1" ethers "^4.0.0-beta.1" @@ -15891,10 +16118,10 @@ truffle-contract@^2.0.3: truffle-contract-schema "^0.0.5" web3 "^0.20.1" -truffle-core@^5.0.24: - version "5.0.24" - resolved "https://registry.yarnpkg.com/truffle-core/-/truffle-core-5.0.24.tgz#80e6694032411aca6d98c0f4e38d49143df25a5c" - integrity sha512-rAjOUZnfGBivlwvkM2EyzL3tQJAqyDq/5IVgwR4LkM/dlYi5ZeIjA7Jj8LiUwtlmUiuYJi0R6bpk/cTjtJFaJQ== +truffle-core@^5.0.26: + version "5.0.26" + resolved "https://registry.yarnpkg.com/truffle-core/-/truffle-core-5.0.26.tgz#d9ee01bf62dca5d8c1c48825547de93ef5637e44" + integrity sha512-ylli1AcILbCFwuzXCjEdDyQCaGURKcQmYXNwuQqDhccFM2MbB+vohNXyoXHI5rT4SzIkaLfn7a4MaJEsGZXcbA== dependencies: app-module-path "^2.2.0" async "2.6.1" @@ -15923,26 +16150,26 @@ truffle-core@^5.0.24: source-map-support "^0.5.3" spawn-args "^0.1.0" temp "^0.8.3" - truffle-artifactor "^4.0.20" - truffle-box "^1.0.28" - truffle-compile "^4.1.1" + truffle-artifactor "^4.0.22" + truffle-box "^1.0.29" + truffle-compile "^4.1.3" truffle-config "^1.1.13" - truffle-contract "^4.0.21" - truffle-contract-sources "^0.1.4" + truffle-contract "^4.0.23" + truffle-contract-sources "^0.1.5" truffle-debug-utils "^1.0.18" - truffle-debugger "^5.0.16" - truffle-deployer "^3.0.22" + truffle-debugger "^5.0.18" + truffle-deployer "^3.0.24" truffle-error "^0.0.5" truffle-expect "^0.0.9" truffle-init "^1.0.7" truffle-interface-adapter "^0.1.6" - truffle-migrate "^3.0.22" + truffle-migrate "^3.0.24" truffle-provider "^0.1.10" truffle-provisioner "^0.1.5" truffle-require "^2.0.13" truffle-resolver "^5.0.14" truffle-solidity-utils "^1.2.3" - truffle-workflow-compile "^2.0.20" + truffle-workflow-compile "^2.0.22" universal-analytics "^0.4.17" web3 "1.0.0-beta.37" xregexp "^4.2.4" @@ -15957,10 +16184,10 @@ truffle-debug-utils@^1.0.18: debug "^4.1.0" node-dir "0.1.17" -truffle-debugger@^5.0.16: - version "5.0.16" - resolved "https://registry.yarnpkg.com/truffle-debugger/-/truffle-debugger-5.0.16.tgz#b2b42609357c127479b617be6c1fb410374d8789" - integrity sha512-aeCRZPmowbFm8D0s8/e+FQPhStxcGpsEnZ2B446GHhSB+Y04fcqohOQqyPDXyua2P1nkLfuxhWBxFW2qgr23ig== +truffle-debugger@^5.0.18: + version "5.0.18" + resolved "https://registry.yarnpkg.com/truffle-debugger/-/truffle-debugger-5.0.18.tgz#0b7162ffc983188eabac046c595cfead0cad7fb5" + integrity sha512-5gtnUpx0HlY5J2W8BS9M4kO9KI392uk//cGBgKWsX6BuevS1Q8f4tiKwOr5yIIkPNt7Ua1ksU0O9N9cz29TpDw== dependencies: bn.js "^4.11.8" debug "^4.1.0" @@ -15973,7 +16200,7 @@ truffle-debugger@^5.0.16: reselect-tree "^1.3.1" truffle-code-utils "^1.2.4" truffle-decode-utils "^1.0.14" - truffle-decoder "^3.0.5" + truffle-decoder "^3.0.6" truffle-expect "^0.0.9" truffle-solidity-utils "^1.2.3" web3 "1.0.0-beta.37" @@ -15990,10 +16217,10 @@ truffle-decode-utils@^1.0.14: web3 "1.0.0-beta.37" web3-eth-abi "1.0.0-beta.52" -truffle-decoder@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/truffle-decoder/-/truffle-decoder-3.0.5.tgz#78d106eb50a3f032da963e0059a802bebafe618a" - integrity sha512-lY1ls6Hht9NK/UAzNYh9bfx5fDPBe7DvJGTwYm4TehwpaCsz67xTMrYN/Fr+6qFSxXXQ0lSW3X0VRQcbD5CDlA== +truffle-decoder@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/truffle-decoder/-/truffle-decoder-3.0.6.tgz#81f2a79b3e9b7d8f481aa624a848fedfa465548f" + integrity sha512-+oTTnymkCs/CClbk7xZCE2Im/2a26p0NRLFIhGghU3W25KxS6h810eeL+PHgtkNTCnbRbd/KNJaVS68j8u1IJg== dependencies: abi-decoder "^1.2.0" async-eventemitter "^0.2.4" @@ -16003,15 +16230,16 @@ truffle-decoder@^3.0.5: lodash.isequal "^4.5.0" lodash.merge "^4.6.1" truffle-decode-utils "^1.0.14" + utf8 "^3.0.0" web3 "1.0.0-beta.37" -truffle-deployer@^3.0.22: - version "3.0.22" - resolved "https://registry.yarnpkg.com/truffle-deployer/-/truffle-deployer-3.0.22.tgz#6b779241208a6a8180041e9c0b0e6cc408803e17" - integrity sha512-5XmwBU/53xnVzOMAS3gjhdQAxRKRcy/rwUiqg0Fr5YMncqKj/gRfQvaJxav8k1GWPz7Cxjh7Yc7dC4HPd9sZjA== +truffle-deployer@^3.0.24: + version "3.0.24" + resolved "https://registry.yarnpkg.com/truffle-deployer/-/truffle-deployer-3.0.24.tgz#7c8fb6110018babee86911093d5949d45bf51704" + integrity sha512-1CoUsG0J8eVGS+aG6+c/r4Bf9YV4G+jVcKINDA438mJMNoDW7QgKCRU4qRGki1v4KlfnlhRVp0JfRuRVQvqQCQ== dependencies: emittery "^0.4.0" - truffle-contract "^4.0.21" + truffle-contract "^4.0.23" truffle-expect "^0.0.9" truffle-error@^0.0.3: @@ -16070,16 +16298,16 @@ truffle-interface-adapter@^0.1.6: bn.js "^4.11.8" web3 "1.0.0-beta.37" -truffle-migrate@^3.0.22: - version "3.0.22" - resolved "https://registry.yarnpkg.com/truffle-migrate/-/truffle-migrate-3.0.22.tgz#6f66f9b79c57d69f09cf17ee39bedb1506479c1e" - integrity sha512-saTphMwl8oG95N3HqYettzkh7jjLfBR/Z40Pp087OL84YbqGpLxGXzfW6vF37IRs0UZfE0Ip0Py5VFIXRdgvmw== +truffle-migrate@^3.0.24: + version "3.0.24" + resolved "https://registry.yarnpkg.com/truffle-migrate/-/truffle-migrate-3.0.24.tgz#205e16aea91eca597e84d2f952c1dbda6367c685" + integrity sha512-0NekMBRC058Lk6PpJx7kCaBGVgOgM997tThRTL5brBb/oQg6wDOgPucChQEsKol07eqZ6BAfkXVyL9RkkFW+ow== dependencies: async "2.6.1" emittery "^0.4.0" node-dir "0.1.17" truffle-config "^1.1.13" - truffle-deployer "^3.0.22" + truffle-deployer "^3.0.24" truffle-expect "^0.0.9" truffle-interface-adapter "^0.1.6" truffle-reporters "^1.0.10" @@ -16131,41 +16359,41 @@ truffle-resolver@^5.0.14: truffle-expect "^0.0.9" truffle-provisioner "^0.1.5" -truffle-solidity-loader@0.1.23: - version "0.1.23" - resolved "https://registry.yarnpkg.com/truffle-solidity-loader/-/truffle-solidity-loader-0.1.23.tgz#b94ced7b412f8760468fc5e8ea6a6c6c86628a2c" - integrity sha512-AT83a1pqu8XSVa9NttTOVjDqCuwGFEVEUSjKKMxeKSorlK8tZqlipAe5GHs2lTZ1ffnRgGBFgPUrfiSr9tQp+w== +truffle-solidity-loader@0.1.25: + version "0.1.25" + resolved "https://registry.yarnpkg.com/truffle-solidity-loader/-/truffle-solidity-loader-0.1.25.tgz#458c5219e0ba5099a9a99f85a73607e7557ad6f3" + integrity sha512-5TgUk5w7fo+MKGDFyFmIt1icQxCXbqwRk0WeYDB5Xizr/PUbVW37scTfdv38cSgI2FJ7eZyvLsKE6zAa78hFCw== dependencies: chalk "^1.1.3" find-up "^1.1.2" loader-utils "^1.1.0" schema-utils "^1.0.0" truffle-config "^1.1.13" - truffle-core "^5.0.24" + truffle-core "^5.0.26" truffle-solidity-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/truffle-solidity-utils/-/truffle-solidity-utils-1.2.3.tgz#9e83c80fe5eeac1b9587f227af57e3feee5e183c" integrity sha512-Rf9KLx8BFTX6/1jxKuzWC5AegSMTN9uxLIKWP38oBAxHq/ilD64W+W5eyEqBxAXUYlAABj9jpOg4Pn5NRYtxOg== -truffle-workflow-compile@^2.0.20: - version "2.0.20" - resolved "https://registry.yarnpkg.com/truffle-workflow-compile/-/truffle-workflow-compile-2.0.20.tgz#2faa3c3e2d0873beebce2c5fc265027e36c12fd8" - integrity sha512-XyUW5DTmnsCF9149/0kgUkRPe5gBzCb7W8FfjqXsUuGMKbSqucb+yMOU7qidSsl7NGiDBekWoR4G5Li6Z5k21g== +truffle-workflow-compile@^2.0.22: + version "2.0.22" + resolved "https://registry.yarnpkg.com/truffle-workflow-compile/-/truffle-workflow-compile-2.0.22.tgz#9ff49d5ddb0547bfed0ca2771923d18a07792210" + integrity sha512-RVcwWj61SwrAZFwoKMGm1lwWtJgNX6Yor+m5mxeVwJVoBH1d++mKUYTtf8eeujJQSfNHCo6OJe5RKtzrePOVfg== dependencies: mkdirp "^0.5.1" - truffle-artifactor "^4.0.20" - truffle-compile "^4.1.1" - truffle-compile-vyper "^1.0.18" + truffle-artifactor "^4.0.22" + truffle-compile "^4.1.3" + truffle-compile-vyper "^1.0.20" truffle-config "^1.1.13" truffle-expect "^0.0.9" truffle-external-compile "^1.0.11" truffle-resolver "^5.0.14" -truffle@5.0.24: - version "5.0.24" - resolved "https://registry.yarnpkg.com/truffle/-/truffle-5.0.24.tgz#5cbba0bb280a2907529e9b71b9444f064c649262" - integrity sha512-a7sAi7S3s82+qQBstGJzEUE09QjJLRlY00ITrK4N4Qfzgje3thUA2nIGl2uExdD07w6bUZBOLWW9eoCVwps1Eg== +truffle@5.0.26: + version "5.0.26" + resolved "https://registry.yarnpkg.com/truffle/-/truffle-5.0.26.tgz#cb9a6dcd77501821ba6e8ba51b2d4eef58da7458" + integrity sha512-gf3Khot59ZM2Tegqb2md0lpr1SXGUHKJkSajCyXsiFCiiHUbX6NpXn/mgJgk1SXt7FAHpL0zYdLI1pYfqzvBFw== dependencies: app-module-path "^2.2.0" mocha "5.2.0" @@ -17725,7 +17953,37 @@ webpack-sources@^1.1.0, webpack-sources@^1.3.0: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@4.35.0, webpack@^4.33.0: +webpack@4.35.2: + version "4.35.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.2.tgz#5c8b8a66602cbbd6ec65c6e6747914a61c1449b1" + integrity sha512-TZAmorNymV4q66gAM/h90cEjG+N3627Q2MnkSgKlX/z3DlNVKUtqy57lz1WmZU2+FUZwzM+qm7cGaO95PyrX5A== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + acorn "^6.0.5" + acorn-dynamic-import "^4.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" + chrome-trace-event "^1.0.0" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.0" + json-parse-better-errors "^1.0.2" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + micromatch "^3.1.8" + mkdirp "~0.5.0" + neo-async "^2.5.0" + node-libs-browser "^2.0.0" + schema-utils "^1.0.0" + tapable "^1.1.0" + terser-webpack-plugin "^1.1.0" + watchpack "^1.5.0" + webpack-sources "^1.3.0" + +webpack@^4.33.0: version "4.35.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.0.tgz#ad3f0f8190876328806ccb7a36f3ce6e764b8378" integrity sha512-M5hL3qpVvtr8d4YaJANbAQBc4uT01G33eDpl/psRTBCfjxFTihdhin1NtAKB1ruDwzeVdcsHHV3NX+QsAgOosw==