commit
84567a59f4
22
package.json
22
package.json
|
@ -49,7 +49,7 @@
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-final-form": "6.3.0",
|
"react-final-form": "6.3.0",
|
||||||
"react-final-form-listeners": "^1.0.2",
|
"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-infinite-scroll-component": "^4.5.2",
|
||||||
"react-redux": "7.1.0",
|
"react-redux": "7.1.0",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
|
@ -61,9 +61,9 @@
|
||||||
"web3": "1.0.0-beta.37"
|
"web3": "1.0.0-beta.37"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "7.4.4",
|
"@babel/cli": "7.5.0",
|
||||||
"@babel/core": "7.4.5",
|
"@babel/core": "7.5.0",
|
||||||
"@babel/plugin-proposal-class-properties": "7.4.4",
|
"@babel/plugin-proposal-class-properties": "7.5.0",
|
||||||
"@babel/plugin-proposal-decorators": "7.4.4",
|
"@babel/plugin-proposal-decorators": "7.4.4",
|
||||||
"@babel/plugin-proposal-do-expressions": "^7.0.0",
|
"@babel/plugin-proposal-do-expressions": "^7.0.0",
|
||||||
"@babel/plugin-proposal-export-default-from": "^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-member-expression-literals": "^7.2.0",
|
||||||
"@babel/plugin-transform-property-literals": "^7.2.0",
|
"@babel/plugin-transform-property-literals": "^7.2.0",
|
||||||
"@babel/polyfill": "7.4.4",
|
"@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-flow": "^7.0.0-beta.40",
|
||||||
"@babel/preset-react": "^7.0.0-beta.40",
|
"@babel/preset-react": "^7.0.0-beta.40",
|
||||||
"@sambego/storybook-state": "^1.0.7",
|
"@sambego/storybook-state": "^1.0.7",
|
||||||
|
@ -106,14 +106,14 @@
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
"eslint-config-airbnb": "^17.1.0",
|
||||||
"eslint-plugin-flowtype": "3.11.1",
|
"eslint-plugin-flowtype": "3.11.1",
|
||||||
"eslint-plugin-import": "2.18.0",
|
"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-jsx-a11y": "^6.0.3",
|
||||||
"eslint-plugin-react": "7.14.2",
|
"eslint-plugin-react": "7.14.2",
|
||||||
"ethereumjs-abi": "^0.6.7",
|
"ethereumjs-abi": "^0.6.7",
|
||||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||||
"file-loader": "4.0.0",
|
"file-loader": "4.0.0",
|
||||||
"flow-bin": "0.102.0",
|
"flow-bin": "0.102.0",
|
||||||
"fs-extra": "8.0.1",
|
"fs-extra": "8.1.0",
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^3.0.4",
|
"html-webpack-plugin": "^3.0.4",
|
||||||
"jest": "24.8.0",
|
"jest": "24.8.0",
|
||||||
|
@ -129,11 +129,11 @@
|
||||||
"storybook-host": "^5.0.3",
|
"storybook-host": "^5.0.3",
|
||||||
"storybook-router": "^0.3.3",
|
"storybook-router": "^0.3.3",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"truffle": "5.0.24",
|
"truffle": "5.0.26",
|
||||||
"truffle-contract": "4.0.21",
|
"truffle-contract": "4.0.23",
|
||||||
"truffle-solidity-loader": "0.1.23",
|
"truffle-solidity-loader": "0.1.25",
|
||||||
"uglifyjs-webpack-plugin": "2.1.3",
|
"uglifyjs-webpack-plugin": "2.1.3",
|
||||||
"webpack": "4.35.0",
|
"webpack": "4.35.2",
|
||||||
"webpack-bundle-analyzer": "3.3.2",
|
"webpack-bundle-analyzer": "3.3.2",
|
||||||
"webpack-cli": "3.3.5",
|
"webpack-cli": "3.3.5",
|
||||||
"webpack-dev-server": "3.7.2",
|
"webpack-dev-server": "3.7.2",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: solid 0.5px $border;
|
border: solid 0.5px $border;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $(screenXs)px) {
|
@media only screen and (max-width: $(screenXs)px) {
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import Row from '~/components/layout/Row'
|
|
||||||
import Table from '@material-ui/core/Table'
|
import Table from '@material-ui/core/Table'
|
||||||
import TableBody from '@material-ui/core/TableBody'
|
import TableBody from '@material-ui/core/TableBody'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
import TablePagination from '@material-ui/core/TablePagination'
|
import TablePagination from '@material-ui/core/TablePagination'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
import { type Order, stableSort, getSorting } from '~/components/Table/sorting'
|
import { type Order, stableSort, getSorting } from '~/components/Table/sorting'
|
||||||
import TableHead, { type Column } from '~/components/Table/TableHead'
|
import TableHead, { type Column } from '~/components/Table/TableHead'
|
||||||
import { xl } from '~/theme/variables'
|
import { xl } from '~/theme/variables'
|
||||||
|
@ -21,6 +21,7 @@ type Props<K> = {
|
||||||
children: Function,
|
children: Function,
|
||||||
size: number,
|
size: number,
|
||||||
defaultFixed?: boolean,
|
defaultFixed?: boolean,
|
||||||
|
noBorder: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -99,7 +100,7 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
data, label, columns, classes, children, size, defaultOrderBy, defaultFixed,
|
data, label, columns, classes, children, size, defaultOrderBy, defaultFixed, noBorder,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {
|
const {
|
||||||
order, orderBy, page, orderProp, rowsPerPage, fixed,
|
order, orderBy, page, orderProp, rowsPerPage, fixed,
|
||||||
|
@ -117,7 +118,7 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
|
|
||||||
const paginationClasses = {
|
const paginationClasses = {
|
||||||
selectRoot: classes.selectRoot,
|
selectRoot: classes.selectRoot,
|
||||||
root: classes.paginationRoot,
|
root: !noBorder && classes.paginationRoot,
|
||||||
input: classes.white,
|
input: classes.white,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,13 +133,16 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!isEmpty && (
|
{!isEmpty && (
|
||||||
<Table aria-labelledby={label} className={classes.root}>
|
<Table aria-labelledby={label} className={noBorder ? '' : classes.root}>
|
||||||
<TableHead columns={columns} order={order} orderBy={orderByParam} onSort={this.onSort} />
|
<TableHead columns={columns} order={order} orderBy={orderByParam} onSort={this.onSort} />
|
||||||
<TableBody>{children(sortedData)}</TableBody>
|
<TableBody>{children(sortedData)}</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
{isEmpty && (
|
{isEmpty && (
|
||||||
<Row className={classNames(classes.loader, classes.root)} style={this.getEmptyStyle(emptyRows + 1)}>
|
<Row
|
||||||
|
className={classNames(classes.loader, !noBorder && classes.root)}
|
||||||
|
style={this.getEmptyStyle(emptyRows + 1)}
|
||||||
|
>
|
||||||
<CircularProgress size={60} />
|
<CircularProgress size={60} />
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,9 +2,21 @@
|
||||||
import { type FieldValidator } from 'final-form'
|
import { type FieldValidator } from 'final-form'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
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
|
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)
|
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 ok = () => undefined
|
||||||
|
|
||||||
export const mustBeEthereumAddress = (address: Field) => {
|
export const mustBeEthereumAddress = simpleMemoize((address: Field) => {
|
||||||
const isAddress: boolean = getWeb3().utils.isAddress(address)
|
const isAddress: boolean = getWeb3().utils.isAddress(address)
|
||||||
|
|
||||||
return isAddress ? undefined : 'Address should be a valid Ethereum 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 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 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)
|
export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field) => validators.reduce((error, validator) => error || validator(value), undefined)
|
||||||
|
|
||||||
|
|
|
@ -10,15 +10,16 @@ type Props = {
|
||||||
fullwidth?: boolean,
|
fullwidth?: boolean,
|
||||||
bordered?: boolean,
|
bordered?: boolean,
|
||||||
className?: string,
|
className?: string,
|
||||||
style?: React.Node,
|
style?: Object,
|
||||||
|
testId?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Img = ({
|
const Img = ({
|
||||||
fullwidth, alt, bordered, className, style, ...props
|
fullwidth, alt, bordered, className, style, testId = '', ...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const classes = cx(styles.img, { fullwidth, bordered }, className)
|
const classes = cx(styles.img, { fullwidth, bordered }, className)
|
||||||
|
|
||||||
return <img alt={alt} style={style} className={classes} {...props} />
|
return <img alt={alt} style={style} className={classes} data-testid={testId} {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Img
|
export default Img
|
||||||
|
|
|
@ -12,10 +12,11 @@ type Props = {
|
||||||
margin?: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
|
margin?: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
|
||||||
align?: 'center' | 'end' | 'start',
|
align?: 'center' | 'end' | 'start',
|
||||||
grow?: boolean,
|
grow?: boolean,
|
||||||
|
testId?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row = ({
|
const Row = ({
|
||||||
children, className, margin, align, grow, ...props
|
children, className, margin, align, grow, testId = '', ...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const rowClassNames = cx(
|
const rowClassNames = cx(
|
||||||
styles.row,
|
styles.row,
|
||||||
|
@ -26,7 +27,7 @@ const Row = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={rowClassNames} {...props}>
|
<div className={rowClassNames} data-testid={testId} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,7 @@ const buildWidthFrom = (size: number) => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const overflowStyle = {
|
const overflowStyle = {
|
||||||
overflowX: 'scroll',
|
overflowX: 'auto',
|
||||||
}
|
}
|
||||||
|
|
||||||
// see: https://css-tricks.com/responsive-data-tables/
|
// see: https://css-tricks.com/responsive-data-tables/
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import contract from 'truffle-contract'
|
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 GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||||
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/ProxyFactory.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 { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions'
|
||||||
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
|
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
|
||||||
|
|
||||||
let proxyFactoryMaster
|
let proxyFactoryMaster
|
||||||
let safeMaster
|
let safeMaster
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import { loadFromStorage, saveToStorage, removeFromStorage } from '~/utils/storage'
|
import { loadFromStorage, saveToStorage, removeFromStorage } from '~/utils/storage'
|
||||||
|
|
||||||
export const SAFES_KEY = 'SAFES'
|
export const SAFES_KEY = 'SAFES'
|
||||||
|
@ -28,7 +28,7 @@ export const saveSafes = async (safes: Object) => {
|
||||||
|
|
||||||
export const setOwners = async (safeAddress: string, owners: List<Owner>) => {
|
export const setOwners = async (safeAddress: string, owners: List<Owner>) => {
|
||||||
try {
|
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)
|
await saveToStorage(`${OWNERS_KEY}-${safeAddress}`, ownersAsMap)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
|
@ -42,7 +42,7 @@ export const getOwners = async (safeAddress: string): Map<string, string> => {
|
||||||
return data ? Map(data) : Map()
|
return data ? Map(data) : Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeOwners = async (safeAddress: string): Map<string, string> => {
|
export const removeOwners = async (safeAddress: string) => {
|
||||||
try {
|
try {
|
||||||
await removeFromStorage(`${OWNERS_KEY}-${safeAddress}`)
|
await removeFromStorage(`${OWNERS_KEY}-${safeAddress}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { createAction } from 'redux-actions'
|
import { createAction } from 'redux-actions'
|
||||||
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import { removeTokenFromStorage, removeFromActiveTokens } from '~/logic/tokens/utils/tokensStorage'
|
import { removeTokenFromStorage, removeFromActiveTokens } from '~/logic/tokens/utils/tokensStorage'
|
||||||
import { type GlobalState } from '~/store/index'
|
import { type GlobalState } from '~/store/index'
|
||||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
|
||||||
|
|
||||||
export const REMOVE_TOKEN = 'REMOVE_TOKEN'
|
export const REMOVE_TOKEN = 'REMOVE_TOKEN'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
|
||||||
import { BigNumber } from 'bignumber.js'
|
import { BigNumber } from 'bignumber.js'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
export const toNative = (amt: string | number | BigNumber, decimal: number): BigNumber => {
|
export const toNative = (amt: string | number | BigNumber, decimal: number): BigNumber => {
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ChevronLeft from '@material-ui/icons/ChevronLeft'
|
import ChevronLeft from '@material-ui/icons/ChevronLeft'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import Stepper from '~/components/Stepper'
|
import Stepper from '~/components/Stepper'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Heading from '~/components/layout/Heading'
|
import Heading from '~/components/layout/Heading'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
|
||||||
import ReviewInformation from '~/routes/load/components/ReviewInformation'
|
import ReviewInformation from '~/routes/load/components/ReviewInformation'
|
||||||
import OwnerList from '~/routes/load/components/OwnerList'
|
import OwnerList from '~/routes/load/components/OwnerList'
|
||||||
import DetailsForm, { safeFieldsValidation } from '~/routes/load/components/DetailsForm'
|
import DetailsForm, { safeFieldsValidation } from '~/routes/load/components/DetailsForm'
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
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 { withStyles } from '@material-ui/core/styles'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
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 Identicon from '~/components/Identicon'
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
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 Field from '~/components/forms/Field'
|
||||||
import TextField from '~/components/forms/TextField'
|
import TextField from '~/components/forms/TextField'
|
||||||
import {
|
import {
|
||||||
|
@ -15,8 +17,6 @@ import Button from '~/components/layout/Button'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import Col from '~/components/layout/Col'
|
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 { getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
|
@ -74,8 +74,11 @@ const styles = () => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const getAddressValidators = (addresses: string[], position: number) => {
|
const getAddressValidators = (addresses: string[], position: number) => {
|
||||||
|
// thanks Rich Harris
|
||||||
|
// https://twitter.com/Rich_Harris/status/1125850391155965952
|
||||||
const copy = addresses.slice()
|
const copy = addresses.slice()
|
||||||
copy.splice(position, 1)
|
copy[position] = copy[copy.length - 1]
|
||||||
|
copy.pop()
|
||||||
|
|
||||||
return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy))
|
return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy))
|
||||||
}
|
}
|
||||||
|
@ -97,7 +100,7 @@ export const calculateValuesAfterRemoving = (index: number, notRemovedOwners: nu
|
||||||
return initialValues
|
return initialValues
|
||||||
}
|
}
|
||||||
|
|
||||||
class SafeOwners extends React.Component<Props, State> {
|
class SafeOwners extends React.PureComponent<Props, State> {
|
||||||
state = {
|
state = {
|
||||||
numOwners: 1,
|
numOwners: 1,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => (
|
|
||||||
<OpenPaper controls={controls}>
|
|
||||||
<Heading tag="h2" margin="lg">
|
|
||||||
Add Owner
|
|
||||||
</Heading>
|
|
||||||
<Heading tag="h4" margin="lg">
|
|
||||||
{`Actual number of owners: ${numOwners}, with threshold: ${threshold}`}
|
|
||||||
</Heading>
|
|
||||||
<Block margin="md">
|
|
||||||
<Field
|
|
||||||
name={NAME_PARAM}
|
|
||||||
component={TextField}
|
|
||||||
type="text"
|
|
||||||
validate={required}
|
|
||||||
placeholder="Owner Name*"
|
|
||||||
text="Owner Name*"
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
<Block margin="md">
|
|
||||||
<Field
|
|
||||||
name={OWNER_ADDRESS_PARAM}
|
|
||||||
component={TextField}
|
|
||||||
type="text"
|
|
||||||
validate={composeValidators(required, mustBeEthereumAddress, uniqueAddress(addresses))}
|
|
||||||
placeholder="Owner address*"
|
|
||||||
text="Owner address*"
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
<Block margin="md">
|
|
||||||
<Field name={INCREASE_PARAM} component={Checkbox} type="checkbox" />
|
|
||||||
<Block>Increase threshold?</Block>
|
|
||||||
</Block>
|
|
||||||
</OpenPaper>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default AddOwnerForm
|
|
|
@ -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 (
|
|
||||||
<OpenPaper controls={controls}>
|
|
||||||
<Heading tag="h2">Review the Add Owner operation</Heading>
|
|
||||||
<Paragraph align="left">
|
|
||||||
<Bold>Owner Name: </Bold>
|
|
||||||
{' '}
|
|
||||||
{values[NAME_PARAM]}
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph align="left">
|
|
||||||
<Bold>Owner Address: </Bold>
|
|
||||||
{' '}
|
|
||||||
{values[OWNER_ADDRESS_PARAM]}
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph align="left">
|
|
||||||
<Bold>{text}</Bold>
|
|
||||||
</Paragraph>
|
|
||||||
<Block style={spinnerStyle}>
|
|
||||||
{ submitting && <CircularProgress size={50} /> }
|
|
||||||
</Block>
|
|
||||||
</OpenPaper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Review
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -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<Owner>) => {
|
|
||||||
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<Props, State> {
|
|
||||||
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 = <Stepper.FinishButton title={ADD_OWNER_RESET_BUTTON_TEXT} />
|
|
||||||
const addresses = getOwnerAddressesFrom(safe.get('owners'))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Stepper
|
|
||||||
finishedTransaction={done}
|
|
||||||
finishedButton={finishedButton}
|
|
||||||
onSubmit={this.onAddOwner}
|
|
||||||
steps={steps}
|
|
||||||
onReset={this.onReset}
|
|
||||||
>
|
|
||||||
<Stepper.Page numOwners={safe.get('owners').count()} threshold={safe.get('threshold')} addresses={addresses}>
|
|
||||||
{AddOwnerForm}
|
|
||||||
</Stepper.Page>
|
|
||||||
<Stepper.Page>{Review}</Stepper.Page>
|
|
||||||
</Stepper>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
selector,
|
|
||||||
actions,
|
|
||||||
)(AddOwner)
|
|
|
@ -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,
|
|
||||||
})
|
|
|
@ -3,20 +3,9 @@ import { List } from 'immutable'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||||
|
import { simpleMemoize } from '~/components/forms/validator'
|
||||||
// import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
// 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
|
// eslint-disable-next-line
|
||||||
export const addressIsTokenContract = simpleMemoize(async (tokenAddress: string) => {
|
export const addressIsTokenContract = simpleMemoize(async (tokenAddress: string) => {
|
||||||
// SECOND APPROACH:
|
// SECOND APPROACH:
|
||||||
|
|
|
@ -103,6 +103,7 @@ class Layout extends React.Component<Props, State> {
|
||||||
activeTokens,
|
activeTokens,
|
||||||
createTransaction,
|
createTransaction,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
|
userAddress,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { tabIndex } = this.state
|
const { tabIndex } = this.state
|
||||||
|
|
||||||
|
@ -163,6 +164,8 @@ class Layout extends React.Component<Props, State> {
|
||||||
updateSafe={updateSafe}
|
updateSafe={updateSafe}
|
||||||
threshold={safe.threshold}
|
threshold={safe.threshold}
|
||||||
owners={safe.owners}
|
owners={safe.owners}
|
||||||
|
network={network}
|
||||||
|
userAddress={userAddress}
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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,
|
|
||||||
) => (
|
|
||||||
<OpenPaper controls={controls}>
|
|
||||||
<Heading tag="h2" margin="lg">
|
|
||||||
Remove Owner
|
|
||||||
{' '}
|
|
||||||
{!!name && name}
|
|
||||||
</Heading>
|
|
||||||
<Heading tag="h4" margin="lg">
|
|
||||||
{`Actual number of owners: ${numOwners}, threhsold of safe: ${threshold}`}
|
|
||||||
</Heading>
|
|
||||||
{pendingTransactions && (
|
|
||||||
<SnackbarContent
|
|
||||||
variant="warning"
|
|
||||||
message="Be careful removing an owner might incur in some of the pending transactions could never be executed"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Block margin="md">
|
|
||||||
<Field name={DECREASE_PARAM} component={Checkbox} type="checkbox" disabled={disabled} />
|
|
||||||
<Block>
|
|
||||||
{disabled && '(disabled) '}
|
|
||||||
Decrease threshold?
|
|
||||||
</Block>
|
|
||||||
</Block>
|
|
||||||
</OpenPaper>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default RemoveOwnerForm
|
|
|
@ -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 (
|
|
||||||
<OpenPaper controls={controls}>
|
|
||||||
<Heading tag="h2">Review the Remove Owner operation</Heading>
|
|
||||||
<Paragraph align="left">
|
|
||||||
<Bold>Owner Name: </Bold>
|
|
||||||
{' '}
|
|
||||||
{name}
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph align="left">
|
|
||||||
<Bold>{text}</Bold>
|
|
||||||
</Paragraph>
|
|
||||||
<Block style={spinnerStyle}>
|
|
||||||
{ submitting && <CircularProgress size={50} /> }
|
|
||||||
</Block>
|
|
||||||
</OpenPaper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Review
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -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<Props, State> {
|
|
||||||
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 = <Stepper.FinishButton title={REMOVE_OWNER_RESET_BUTTON_TEXT} />
|
|
||||||
const decrease = shouldDecrease(numOwners, threshold)
|
|
||||||
const initialValues = initialValuesFrom(decrease)
|
|
||||||
const disabled = decrease || threshold === 1
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Stepper
|
|
||||||
finishedTransaction={done}
|
|
||||||
finishedButton={finishedButton}
|
|
||||||
onSubmit={this.onRemoveOwner}
|
|
||||||
steps={steps}
|
|
||||||
onReset={this.onReset}
|
|
||||||
initialValues={initialValues}
|
|
||||||
>
|
|
||||||
<Stepper.Page
|
|
||||||
numOwners={numOwners}
|
|
||||||
threshold={threshold}
|
|
||||||
name={name}
|
|
||||||
disabled={disabled}
|
|
||||||
pendingTransactions={pendingTransactions}
|
|
||||||
>
|
|
||||||
{ RemoveOwnerForm }
|
|
||||||
</Stepper.Page>
|
|
||||||
<Stepper.Page name={name}>
|
|
||||||
{ Review }
|
|
||||||
</Stepper.Page>
|
|
||||||
</Stepper>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(selector, actions)(RemoveOwner)
|
|
|
@ -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<Transaction>) => transactions.findEntry((tx: Transaction) => tx.get('isExecuted')),
|
|
||||||
)
|
|
||||||
|
|
||||||
export type SelectorProps = {
|
|
||||||
executor: typeof userAccountSelector,
|
|
||||||
pendingTransactions: typeof pendingTransactionsSelector,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createStructuredSelector({
|
|
||||||
executor: userAccountSelector,
|
|
||||||
pendingTransactions: pendingTransactionsSelector,
|
|
||||||
})
|
|
|
@ -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) => (
|
|
||||||
<ListItem>
|
|
||||||
<Avatar>
|
|
||||||
<Mail />
|
|
||||||
</Avatar>
|
|
||||||
<ListItemText primary="Safe Address" secondary={address} cut />
|
|
||||||
</ListItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default Address
|
|
|
@ -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<string, Token>,
|
|
||||||
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 (
|
|
||||||
<React.Fragment>
|
|
||||||
<ListItem onClick={hasBalances ? toggle : undefined}>
|
|
||||||
<Avatar>
|
|
||||||
<AccountBalance />
|
|
||||||
</Avatar>
|
|
||||||
<ListItemText primary="Balance" secondary="List of different token balances" />
|
|
||||||
<ListItemIcon>
|
|
||||||
{open
|
|
||||||
? <IconButton disableRipple><ExpandLess /></IconButton>
|
|
||||||
: <IconButton disabled={!hasBalances} disableRipple><ExpandMore /></IconButton>
|
|
||||||
}
|
|
||||||
</ListItemIcon>
|
|
||||||
</ListItem>
|
|
||||||
<Collapse in={open} timeout="auto">
|
|
||||||
<List component="div" disablePadding>
|
|
||||||
{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 (
|
|
||||||
<ListItem key={symbol} className={classNames(classes.nested, symbol)}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Img src={token.get('logoUri')} height={30} alt={name} />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={name} secondary={`${token.get('funds')} ${symbol}`} />
|
|
||||||
<Button variant="contained" color="primary" onClick={onMoveFundsClick} disabled={disabled}>
|
|
||||||
{MOVE_FUNDS_BUTTON_TEXT}
|
|
||||||
</Button>
|
|
||||||
</ListItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default withStyles(styles)(BalanceComponent)
|
|
|
@ -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) => (
|
|
||||||
<ListItem>
|
|
||||||
<Avatar>
|
|
||||||
<DoneAll />
|
|
||||||
</Avatar>
|
|
||||||
<ListItemText
|
|
||||||
primary="Confirmations"
|
|
||||||
secondary={`${confirmations} required confirmations per transaction`}
|
|
||||||
cut
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={onEditThreshold}
|
|
||||||
>
|
|
||||||
{EDIT_THRESHOLD_BUTTON_TEXT}
|
|
||||||
</Button>
|
|
||||||
</ListItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default Confirmations
|
|
|
@ -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 (
|
|
||||||
<ListItem>
|
|
||||||
<Avatar>
|
|
||||||
<AcoountBalanceWallet />
|
|
||||||
</Avatar>
|
|
||||||
<ListItemText primary="Safe's Multisig Transaction" secondary={text} />
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={onSeeTxs}
|
|
||||||
>
|
|
||||||
{SEE_MULTISIG_BUTTON_TEXT}
|
|
||||||
</Button>
|
|
||||||
</ListItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MultisigTransactionsComponent
|
|
|
@ -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<OwnerProps>,
|
|
||||||
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) => (
|
|
||||||
<React.Fragment>
|
|
||||||
<ListItem onClick={toggle}>
|
|
||||||
<Avatar>
|
|
||||||
<Group />
|
|
||||||
</Avatar>
|
|
||||||
<ListItemText primary="Owners" secondary={`${owners.size} owners`} />
|
|
||||||
<ListItemIcon>
|
|
||||||
{open
|
|
||||||
? <IconButton disableRipple><ExpandLess /></IconButton>
|
|
||||||
: <IconButton disableRipple><ExpandMore /></IconButton>
|
|
||||||
}
|
|
||||||
</ListItemIcon>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={onAddOwner}
|
|
||||||
>
|
|
||||||
{ADD_OWNER_BUTTON_TEXT}
|
|
||||||
</Button>
|
|
||||||
</ListItem>
|
|
||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
|
||||||
<List component="div" disablePadding>
|
|
||||||
{owners.map((owner) => {
|
|
||||||
const onRemoveIconClick = () => onRemoveOwner(owner.name, owner.address)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItem key={owner.address} className={classes.nested}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Person />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
cut
|
|
||||||
primary={owner.name}
|
|
||||||
secondary={owner.address}
|
|
||||||
/>
|
|
||||||
{ !sameAddress(userAddress, owner.address)
|
|
||||||
&& (
|
|
||||||
<IconButton aria-label="Delete" onClick={onRemoveIconClick}>
|
|
||||||
<Delete />
|
|
||||||
</IconButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ListItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
|
||||||
</React.Fragment>
|
|
||||||
))
|
|
||||||
|
|
||||||
export default withStyles(styles)(Owners)
|
|
|
@ -1,17 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="a" x1="0%" y1="50.001%" y2="50.001%">
|
|
||||||
<stop offset="0%" stop-color="#00B3CE"/>
|
|
||||||
<stop offset="100%" stop-color="#00C8DD"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g fill="none" fill-rule="nonzero">
|
|
||||||
<circle cx="250" cy="250" r="250" fill="url(#a)"/>
|
|
||||||
<g fill="#FFF">
|
|
||||||
<path d="M189.93 245.903l-49.95-49.674c-4.508 6.049-7.237 13.563-7.237 21.674 0 19.583 15.972 35.52 35.528 35.52 8.118 0 15.66-2.715 21.66-7.52z"/>
|
|
||||||
<path d="M248.639 71.028c-52.077 0-100.542 21.097-136.063 58.694l-5.708 6.042 143.84 144.48v25.923l-.555.562-37.007-37.326c-17.18 11.423-39.153 14.757-59.903 7.528-34.924-12.653-53-50.882-40.361-85.5 1.826-5.41 4.52-10.23 7.52-14.743l-15.937-15.959-3.014 5.118c-16.555 27.104-25.597 58.403-25.597 90.59-.285 96.042 77.965 174.577 173.965 174.577h29.396V71.056l-30.576-.028zm-118.292 64.729c31.945-31.02 73.743-47.854 118.611-47.854h.285c.5 0 .993 0 1.465.02v168.5l-120.36-120.666z"/>
|
|
||||||
<path d="M248.639 71.028c-52.077 0-100.542 21.097-136.063 58.694l-5.708 6.042 143.84 144.48v25.923l-.555.562-37.007-37.326c-17.18 11.423-39.153 14.757-59.903 7.528-34.924-12.653-53-50.882-40.361-85.5 1.826-5.41 4.52-10.23 7.52-14.743l-15.937-15.959-3.014 5.118c-16.555 27.104-25.597 58.403-25.597 90.59-.285 96.042 77.965 174.577 173.965 174.577h29.396V71.056l-30.576-.028zm-118.292 64.729c31.945-31.02 73.743-47.854 118.611-47.854h.285c.5 0 .993 0 1.465.02v168.5l-120.36-120.666zM423.611 250.41c0-79.618-64.764-144.417-144.389-144.473v17.3c70.104.027 127.125 57.069 127.125 127.166 0 70.11-57.02 127.166-127.125 127.194v17.278c79.625-.035 144.39-64.813 144.39-144.465z"/>
|
|
||||||
<path d="M314.549 250.486l-6.855 42.98h33.577l-6.854-42.98c7.646-3.743 12.944-11.59 12.944-20.708 0-12.702-10.236-23-22.868-23-12.632 0-22.875 10.298-22.875 23-.014 9.132 5.285 16.965 12.93 20.708zM303.25 152.986c9.028 2.333 12.507-11.201 3.48-13.528-9.015-2.312-12.508 11.223-3.48 13.528M342.868 173.549c7.139 5.972 16.111-4.73 8.972-10.702-7.11-6-16.104 4.715-8.972 10.702M368.333 206.944c4.105 8.362 16.646 2.223 12.549-6.138-4.09-8.369-16.646-2.209-12.549 6.138M303.25 347.618c9.028-2.326 12.507 11.23 3.48 13.549-9.015 2.305-12.508-11.23-3.48-13.549M342.868 327.07c7.139-5.98 16.111 4.729 8.972 10.687-7.11 6.014-16.104-4.701-8.972-10.688M368.333 293.66c4.105-8.361 16.646-2.202 12.549 6.166-4.09 8.34-16.646 2.202-12.549-6.166M378.424 250.826c0 9.334 13.958 9.334 13.958 0 0-9.312-13.958-9.312-13.958 0"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -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<Token>,
|
|
||||||
userAddress: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
component?: React.Node,
|
|
||||||
}
|
|
||||||
|
|
||||||
const listStyle = {
|
|
||||||
width: '100%',
|
|
||||||
}
|
|
||||||
|
|
||||||
class GnoSafe extends React.PureComponent<SafeProps, State> {
|
|
||||||
state = {
|
|
||||||
component: undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
onListTransactions = () => {
|
|
||||||
const { safe } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
component: (
|
|
||||||
<Transactions threshold={safe.get('threshold')} safeName={safe.get('name')} safeAddress={safe.get('address')} />
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditThreshold = () => {
|
|
||||||
const { safe } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
component: <Threshold numOwners={safe.get('owners').count()} safe={safe} onReset={this.onListTransactions} />,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddOwner = (e: SyntheticEvent<HTMLButtonElement>) => {
|
|
||||||
const { safe } = this.props
|
|
||||||
e.stopPropagation()
|
|
||||||
this.setState({ component: <AddOwner threshold={safe.get('threshold')} safe={safe} /> })
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemoveOwner = (name: string, address: string) => {
|
|
||||||
const { safe } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
component: (
|
|
||||||
<RemoveOwner
|
|
||||||
safeAddress={safe.get('address')}
|
|
||||||
threshold={safe.get('threshold')}
|
|
||||||
safe={safe}
|
|
||||||
name={name}
|
|
||||||
userToRemove={address}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMoveTokens = (ercToken: Token) => {
|
|
||||||
const { safe } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
component: (
|
|
||||||
<SendToken safe={safe} token={ercToken} key={ercToken.get('address')} onReset={this.onListTransactions} />
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { safe, tokens, userAddress } = this.props
|
|
||||||
const { component } = this.state
|
|
||||||
const address = safe.get('address')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row grow>
|
|
||||||
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
|
||||||
<ListComponent style={listStyle}>
|
|
||||||
<BalanceInfo tokens={tokens} onMoveFunds={this.onMoveTokens} safeAddress={address} />
|
|
||||||
<Owners
|
|
||||||
owners={safe.owners}
|
|
||||||
onAddOwner={this.onAddOwner}
|
|
||||||
userAddress={userAddress}
|
|
||||||
onRemoveOwner={this.onRemoveOwner}
|
|
||||||
/>
|
|
||||||
<Confirmations confirmations={safe.get('threshold')} onEditThreshold={this.onEditThreshold} />
|
|
||||||
<Address address={address} />
|
|
||||||
<MultisigTx onSeeTxs={this.onListTransactions} />
|
|
||||||
</ListComponent>
|
|
||||||
</Col>
|
|
||||||
<Col sm={12} center="xs" md={7} margin="xl" layout="column">
|
|
||||||
<Block margin="xl">
|
|
||||||
<Paragraph size="lg" noMargin align="right">
|
|
||||||
<Bold>{safe.name.toUpperCase()}</Bold>
|
|
||||||
</Paragraph>
|
|
||||||
</Block>
|
|
||||||
<Row grow>
|
|
||||||
<Col sm={12} center={component ? undefined : 'sm'} middle={component ? undefined : 'sm'} layout="column">
|
|
||||||
{component || <Img alt="Safe Icon" src={safeIcon} height={330} />}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GnoSafe
|
|
|
@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Field from '~/components/forms/Field'
|
import Field from '~/components/forms/Field'
|
||||||
|
import Heading from '~/components/layout/Heading'
|
||||||
import { composeValidators, required, minMaxLength } from '~/components/forms/validator'
|
import { composeValidators, required, minMaxLength } from '~/components/forms/validator'
|
||||||
import TextField from '~/components/forms/TextField'
|
import TextField from '~/components/forms/TextField'
|
||||||
import GnoForm from '~/components/forms/GnoForm'
|
import GnoForm from '~/components/forms/GnoForm'
|
||||||
|
@ -49,10 +50,8 @@ const ChangeSafeName = (props: Props) => {
|
||||||
{() => (
|
{() => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Block className={classes.formContainer}>
|
<Block className={classes.formContainer}>
|
||||||
<Paragraph noMargin className={classes.title} size="lg" weight="bolder">
|
<Heading tag="h3">Modify Safe name</Heading>
|
||||||
Modify Safe name
|
<Paragraph>
|
||||||
</Paragraph>
|
|
||||||
<Paragraph size="sm">
|
|
||||||
You can change the name of this Safe. This name is only stored locally and never shared with Gnosis or
|
You can change the name of this Safe. This name is only stored locally and never shared with Gnosis or
|
||||||
any third parties.
|
any third parties.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
|
@ -2,12 +2,8 @@
|
||||||
import { lg } from '~/theme/variables'
|
import { lg } from '~/theme/variables'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = () => ({
|
||||||
title: {
|
|
||||||
padding: `${lg} 0 20px`,
|
|
||||||
fontSize: '16px',
|
|
||||||
},
|
|
||||||
formContainer: {
|
formContainer: {
|
||||||
padding: '0 20px',
|
padding: lg,
|
||||||
minHeight: '369px',
|
minHeight: '369px',
|
||||||
},
|
},
|
||||||
root: {
|
root: {
|
||||||
|
|
|
@ -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<Owner>,
|
||||||
|
threshold: number,
|
||||||
|
network: string,
|
||||||
|
addSafeOwner: Function,
|
||||||
|
createTransaction: Function,
|
||||||
|
}
|
||||||
|
type ActiveScreen = 'selectOwner' | 'selectThreshold' | 'reviewAddOwner'
|
||||||
|
|
||||||
|
export const sendAddOwner = async (
|
||||||
|
values: Object,
|
||||||
|
safeAddress: string,
|
||||||
|
ownersOld: List<Owner>,
|
||||||
|
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<ActiveScreen>('selectOwner')
|
||||||
|
const [values, setValues] = useState<Object>({})
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<SharedSnackbarConsumer>
|
||||||
|
{({ openSnackbar }) => {
|
||||||
|
const onAddOwner = async () => {
|
||||||
|
onClose()
|
||||||
|
try {
|
||||||
|
sendAddOwner(values, safeAddress, owners, openSnackbar, createTransaction, addSafeOwner)
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error while removing an owner ' + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Add owner to Safe"
|
||||||
|
description="Add owner to Safe"
|
||||||
|
handleClose={onClose}
|
||||||
|
open={isOpen}
|
||||||
|
paperClassName={classes.biggerModalWindow}
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
{activeScreen === 'selectOwner' && (
|
||||||
|
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} owners={owners} />
|
||||||
|
)}
|
||||||
|
{activeScreen === 'selectThreshold' && (
|
||||||
|
<ThresholdForm
|
||||||
|
onClose={onClose}
|
||||||
|
owners={owners}
|
||||||
|
threshold={threshold}
|
||||||
|
onClickBack={onClickBack}
|
||||||
|
onSubmit={thresholdSubmitted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeScreen === 'reviewAddOwner' && (
|
||||||
|
<ReviewAddOwner
|
||||||
|
onClose={onClose}
|
||||||
|
safeName={safeName}
|
||||||
|
owners={owners}
|
||||||
|
network={network}
|
||||||
|
values={values}
|
||||||
|
onClickBack={onClickBack}
|
||||||
|
onSubmit={onAddOwner}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</SharedSnackbarConsumer>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(AddOwner)
|
|
@ -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<Owner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const OwnerForm = ({
|
||||||
|
classes, onClose, onSubmit, owners,
|
||||||
|
}: Props) => {
|
||||||
|
const handleSubmit = (values) => {
|
||||||
|
onSubmit(values)
|
||||||
|
}
|
||||||
|
const ownerDoesntExist = uniqueAddress(owners.map(o => o.address))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Add new owner
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>1 of 3</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit}>
|
||||||
|
{() => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<Row margin="md">
|
||||||
|
<Paragraph>Add a new owner to the active Safe</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={8}>
|
||||||
|
<Field
|
||||||
|
name="ownerName"
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||||
|
placeholder="Owner name*"
|
||||||
|
text="Owner name*"
|
||||||
|
className={classes.addressInput}
|
||||||
|
testId={ADD_OWNER_NAME_INPUT_TESTID}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={8}>
|
||||||
|
<Field
|
||||||
|
name="ownerAddress"
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, mustBeEthereumAddress, ownerDoesntExist)}
|
||||||
|
placeholder="Owner address*"
|
||||||
|
text="Owner address*"
|
||||||
|
className={classes.addressInput}
|
||||||
|
testId={ADD_OWNER_ADDRESS_INPUT_TESTID}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
testId={ADD_OWNER_NEXT_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</GnoForm>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(OwnerForm)
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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<Owner>,
|
||||||
|
network: string,
|
||||||
|
values: Object,
|
||||||
|
onClickBack: Function,
|
||||||
|
onSubmit: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReviewAddOwner = ({
|
||||||
|
classes, onClose, safeName, owners, network, values, onClickBack, onSubmit,
|
||||||
|
}: Props) => {
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onSubmit()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Add new owner
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>3 of 3</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<Row className={classes.root}>
|
||||||
|
<Col xs={4} layout="column">
|
||||||
|
<Block className={classes.details}>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="lg" color="primary" noMargin>
|
||||||
|
Details
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="sm" color="disabled" noMargin>
|
||||||
|
Safe name
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
|
||||||
|
{safeName}
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="sm" color="disabled" noMargin>
|
||||||
|
Any transaction requires the confirmation of:
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
|
||||||
|
{values.threshold}
|
||||||
|
{' '}
|
||||||
|
out of
|
||||||
|
{' '}
|
||||||
|
{owners.size + 1}
|
||||||
|
{' '}
|
||||||
|
owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
<Col xs={8} layout="column" className={classes.owners}>
|
||||||
|
<Row className={classes.ownersTitle}>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin>
|
||||||
|
{owners.size + 1}
|
||||||
|
{' '}
|
||||||
|
Safe owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
{owners.map(owner => (
|
||||||
|
<React.Fragment key={owner.address}>
|
||||||
|
<Row className={classes.owner}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={owner.address} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph weight="bolder" size="lg" noMargin>
|
||||||
|
{owner.name}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{owner.address}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={getEtherScanLink(owner.address, network)} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
<Row className={classes.info} align="center">
|
||||||
|
<Paragraph weight="bolder" noMargin color="primary" size="md">
|
||||||
|
ADDING NEW OWNER ↓
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row className={classes.selectedOwner}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={values.ownerAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph weight="bolder" size="lg" noMargin>
|
||||||
|
{values.ownerName}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{values.ownerAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={getEtherScanLink(values.ownerAddress, network)} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
testId={ADD_OWNER_SUBMIT_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ReviewAddOwner)
|
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -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<Owner>,
|
||||||
|
threshold: number,
|
||||||
|
onClickBack: Function,
|
||||||
|
onSubmit: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThresholdForm = ({
|
||||||
|
classes, onClose, owners, threshold, onClickBack, onSubmit,
|
||||||
|
}: Props) => {
|
||||||
|
const handleSubmit = (values) => {
|
||||||
|
onSubmit(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Add new owner
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>2 of 3</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit} initialValues={{ threshold: threshold.toString() }}>
|
||||||
|
{() => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<Row>
|
||||||
|
<Paragraph weight="bolder" className={classes.headingText}>
|
||||||
|
Set the required owner confirmations:
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Paragraph weight="bolder">
|
||||||
|
Any transaction over any daily limit requires the confirmation of:
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xl" align="center" className={classes.inputRow}>
|
||||||
|
<Col xs={2}>
|
||||||
|
<Field
|
||||||
|
name="threshold"
|
||||||
|
render={props => (
|
||||||
|
<React.Fragment>
|
||||||
|
<SelectField {...props} disableError>
|
||||||
|
{[...Array(Number(owners.size + 1))].map((x, index) => (
|
||||||
|
<MenuItem key={index} value={`${index + 1}`}>
|
||||||
|
{index + 1}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</SelectField>
|
||||||
|
{props.meta.error && props.meta.touched && (
|
||||||
|
<Paragraph className={classes.errorText} noMargin color="error">
|
||||||
|
{props.meta.error}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(owners.size + 1))}
|
||||||
|
data-testid="threshold-select-input"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={10}>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin className={classes.ownersText}>
|
||||||
|
out of
|
||||||
|
{' '}
|
||||||
|
{owners.size + 1}
|
||||||
|
{' '}
|
||||||
|
owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
testId={ADD_OWNER_THRESHOLD_NEXT_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Review
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</GnoForm>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ThresholdForm)
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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 (
|
||||||
|
<Modal
|
||||||
|
title="Edit owner from Safe"
|
||||||
|
description="Edit owner from Safe"
|
||||||
|
handleClose={onClose}
|
||||||
|
open={isOpen}
|
||||||
|
paperClassName={classes.smallerModalWindow}
|
||||||
|
>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||||
|
Edit owner name
|
||||||
|
</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.close} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit}>
|
||||||
|
{() => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block className={classes.container}>
|
||||||
|
<Row margin="md">
|
||||||
|
<Field
|
||||||
|
name="ownerName"
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||||
|
placeholder="Owner name*"
|
||||||
|
text="Owner name*"
|
||||||
|
initialValue={selectedOwnerName}
|
||||||
|
className={classes.addressInput}
|
||||||
|
testId={RENAME_OWNER_INPUT_TESTID}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Identicon address={ownerAddress} diameter={32} />
|
||||||
|
<Paragraph style={{ marginLeft: 10 }} size="md" color="disabled" noMargin>
|
||||||
|
{ownerAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className={classes.button} variant="contained" minWidth={140} color="primary" testId={SAVE_OWNER_CHANGES_BTN_TESTID}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</GnoForm>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditOwnerModal = withStyles(styles)(EditOwnerComponent)
|
||||||
|
|
||||||
|
export default EditOwnerModal
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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 (
|
||||||
|
<Block align="left">
|
||||||
|
<Identicon address={address} diameter={32} />
|
||||||
|
<Paragraph style={{ marginLeft: 10 }}>{address}</Paragraph>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OwnerAddressTableCell
|
|
@ -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<Owner>,
|
||||||
|
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<Owner>,
|
||||||
|
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<ActiveScreen>('checkOwner')
|
||||||
|
const [values, setValues] = useState<Object>({})
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<SharedSnackbarConsumer>
|
||||||
|
{({ 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 (
|
||||||
|
<Modal
|
||||||
|
title="Remove owner from Safe"
|
||||||
|
description="Remove owner from Safe"
|
||||||
|
handleClose={onClose}
|
||||||
|
open={isOpen}
|
||||||
|
paperClassName={classes.biggerModalWindow}
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
{activeScreen === 'checkOwner' && (
|
||||||
|
<CheckOwner
|
||||||
|
onClose={onClose}
|
||||||
|
ownerAddress={ownerAddress}
|
||||||
|
ownerName={ownerName}
|
||||||
|
network={network}
|
||||||
|
onSubmit={ownerSubmitted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeScreen === 'selectThreshold' && (
|
||||||
|
<ThresholdForm
|
||||||
|
onClose={onClose}
|
||||||
|
owners={owners}
|
||||||
|
threshold={threshold}
|
||||||
|
onClickBack={onClickBack}
|
||||||
|
onSubmit={thresholdSubmitted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeScreen === 'reviewRemoveOwner' && (
|
||||||
|
<ReviewRemoveOwner
|
||||||
|
onClose={onClose}
|
||||||
|
safeName={safeName}
|
||||||
|
owners={owners}
|
||||||
|
network={network}
|
||||||
|
values={values}
|
||||||
|
ownerAddress={ownerAddress}
|
||||||
|
ownerName={ownerName}
|
||||||
|
onClickBack={onClickBack}
|
||||||
|
onSubmit={onRemoveOwner}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</SharedSnackbarConsumer>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(RemoveOwner)
|
|
@ -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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Remove owner
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>1 of 3</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<Row margin="md">
|
||||||
|
<Paragraph>
|
||||||
|
Review the owner you want to remove from the active Safe:
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row className={classes.owner}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={ownerAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={7}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph size="lg" noMargin weight="bolder">
|
||||||
|
{ownerName}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{ownerAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
testId={REMOVE_OWNER_MODAL_NEXT_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(CheckOwner)
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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<Owner>,
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Remove owner
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>3 of 3</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block>
|
||||||
|
<Row className={classes.root}>
|
||||||
|
<Col xs={4} layout="column">
|
||||||
|
<Block className={classes.details}>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="lg" color="primary" noMargin>
|
||||||
|
Details
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="sm" color="disabled" noMargin>
|
||||||
|
Safe name
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
|
||||||
|
{safeName}
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="sm" color="disabled" noMargin>
|
||||||
|
Any transaction requires the confirmation of:
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
|
||||||
|
{values.threshold}
|
||||||
|
{' '}
|
||||||
|
out of
|
||||||
|
{' '}
|
||||||
|
{owners.size - 1}
|
||||||
|
{' '}
|
||||||
|
owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
<Col xs={8} layout="column" className={classes.owners}>
|
||||||
|
<Row className={classes.ownersTitle}>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin>
|
||||||
|
{owners.size - 1}
|
||||||
|
{' '}
|
||||||
|
Safe owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
{owners.map(
|
||||||
|
owner => owner.address !== ownerAddress && (
|
||||||
|
<React.Fragment key={owner.address}>
|
||||||
|
<Row className={classes.owner}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={owner.address} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph weight="bolder" size="lg" noMargin>
|
||||||
|
{owner.name}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{owner.address}
|
||||||
|
</Paragraph>
|
||||||
|
<Link
|
||||||
|
className={classes.open}
|
||||||
|
to={getEtherScanLink(owner.address, network)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
</React.Fragment>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<Row className={classes.info} align="center">
|
||||||
|
<Paragraph weight="bolder" noMargin color="primary" size="md">
|
||||||
|
REMOVING OWNER ↓
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row className={classes.selectedOwner}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={ownerAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph weight="bolder" size="lg" noMargin>
|
||||||
|
{ownerName}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{ownerAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
testId={REMOVE_OWNER_REVIEW_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ReviewRemoveOwner)
|
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -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<Owner>,
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Remove owner
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>2 of 3</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit} initialValues={{ threshold: defaultThreshold.toString() }}>
|
||||||
|
{() => {
|
||||||
|
const numOptions = owners.size > 1 ? owners.size - 1 : 1
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<Row>
|
||||||
|
<Paragraph weight="bolder" className={classes.headingText}>
|
||||||
|
Set the required owner confirmations:
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Paragraph weight="bolder">
|
||||||
|
Any transaction over any daily limit requires the confirmation of:
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xl" align="center" className={classes.inputRow}>
|
||||||
|
<Col xs={2}>
|
||||||
|
<Field
|
||||||
|
name="threshold"
|
||||||
|
render={props => (
|
||||||
|
<React.Fragment>
|
||||||
|
<SelectField {...props} disableError>
|
||||||
|
{[...Array(Number(numOptions))].map((x, index) => (
|
||||||
|
<MenuItem key={index} value={`${index + 1}`}>
|
||||||
|
{index + 1}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</SelectField>
|
||||||
|
{props.meta.error && props.meta.touched && (
|
||||||
|
<Paragraph className={classes.errorText} noMargin color="error">
|
||||||
|
{props.meta.error}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(numOptions))}
|
||||||
|
data-testid="threshold-select-input"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={10}>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin className={classes.ownersText}>
|
||||||
|
out of
|
||||||
|
{' '}
|
||||||
|
{owners.size - 1}
|
||||||
|
{' '}
|
||||||
|
owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
data-testid={REMOVE_OWNER_THRESHOLD_NEXT_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Review
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</GnoForm>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ThresholdForm)
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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<Owner>,
|
||||||
|
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<Owner>,
|
||||||
|
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<ActiveScreen>('checkOwner')
|
||||||
|
const [values, setValues] = useState<Object>({})
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<SharedSnackbarConsumer>
|
||||||
|
{({ 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 (
|
||||||
|
<Modal
|
||||||
|
title="Replace owner from Safe"
|
||||||
|
description="Replace owner from Safe"
|
||||||
|
handleClose={onClose}
|
||||||
|
open={isOpen}
|
||||||
|
paperClassName={classes.biggerModalWindow}
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
{activeScreen === 'checkOwner' && (
|
||||||
|
<OwnerForm
|
||||||
|
onClose={onClose}
|
||||||
|
ownerAddress={ownerAddress}
|
||||||
|
ownerName={ownerName}
|
||||||
|
owners={owners}
|
||||||
|
network={network}
|
||||||
|
onSubmit={ownerSubmitted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeScreen === 'reviewReplaceOwner' && (
|
||||||
|
<ReviewReplaceOwner
|
||||||
|
onClose={onClose}
|
||||||
|
safeName={safeName}
|
||||||
|
owners={owners}
|
||||||
|
network={network}
|
||||||
|
values={values}
|
||||||
|
ownerAddress={ownerAddress}
|
||||||
|
ownerName={ownerName}
|
||||||
|
onClickBack={onClickBack}
|
||||||
|
onSubmit={onReplaceOwner}
|
||||||
|
threshold={threshold}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</SharedSnackbarConsumer>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ReplaceOwner)
|
|
@ -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<Owner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const OwnerForm = ({
|
||||||
|
classes, onClose, ownerAddress, ownerName, network, onSubmit, owners,
|
||||||
|
}: Props) => {
|
||||||
|
const handleSubmit = (values) => {
|
||||||
|
onSubmit(values)
|
||||||
|
}
|
||||||
|
const ownerDoesntExist = uniqueAddress(owners.map(o => o.address))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Replace owner
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit}>
|
||||||
|
{() => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<Row>
|
||||||
|
<Paragraph>
|
||||||
|
Review the owner you want to replace from the active Safe. Then specify the new owner you want to
|
||||||
|
replace it with:
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Paragraph>Current owner</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row className={classes.owner}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={ownerAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={7}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph size="lg" noMargin weight="bolder">
|
||||||
|
{ownerName}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{ownerAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Paragraph>New owner</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={8}>
|
||||||
|
<Field
|
||||||
|
name="ownerName"
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||||
|
placeholder="Owner name*"
|
||||||
|
text="Owner name*"
|
||||||
|
className={classes.addressInput}
|
||||||
|
testId={REPLACE_OWNER_NAME_INPUT_TESTID}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={8}>
|
||||||
|
<Field
|
||||||
|
name="ownerAddress"
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, mustBeEthereumAddress, ownerDoesntExist)}
|
||||||
|
placeholder="Owner address*"
|
||||||
|
text="Owner address*"
|
||||||
|
className={classes.addressInput}
|
||||||
|
testId={REPLACE_OWNER_ADDRESS_INPUT_TESTID}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
testId={REPLACE_OWNER_NEXT_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</GnoForm>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(OwnerForm)
|
|
@ -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}`,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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<Owner>,
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Replace owner
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<Row className={classes.root}>
|
||||||
|
<Col xs={4} layout="column">
|
||||||
|
<Block className={classes.details}>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="lg" color="primary" noMargin>
|
||||||
|
Details
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="sm" color="disabled" noMargin>
|
||||||
|
Safe name
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
|
||||||
|
{safeName}
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="sm" color="disabled" noMargin>
|
||||||
|
Any transaction requires the confirmation of:
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin weight="bolder" className={classes.name}>
|
||||||
|
{threshold}
|
||||||
|
{' '}
|
||||||
|
out of
|
||||||
|
{' '}
|
||||||
|
{owners.size}
|
||||||
|
{' '}
|
||||||
|
owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
<Col xs={8} layout="column" className={classes.owners}>
|
||||||
|
<Row className={classes.ownersTitle}>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin>
|
||||||
|
{owners.size}
|
||||||
|
{' '}
|
||||||
|
Safe owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
{owners.map(
|
||||||
|
owner => owner.address !== ownerAddress && (
|
||||||
|
<React.Fragment key={owner.address}>
|
||||||
|
<Row className={classes.owner}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={owner.address} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph weight="bolder" size="lg" noMargin>
|
||||||
|
{owner.name}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{owner.address}
|
||||||
|
</Paragraph>
|
||||||
|
<Link
|
||||||
|
className={classes.open}
|
||||||
|
to={getEtherScanLink(owner.address, network)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
</React.Fragment>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<Row className={classes.info} align="center">
|
||||||
|
<Paragraph weight="bolder" noMargin color="primary" size="md">
|
||||||
|
REMOVING OWNER ↓
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row className={classes.selectedOwnerRemoved}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={ownerAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph weight="bolder" size="lg" noMargin>
|
||||||
|
{ownerName}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{ownerAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row className={classes.info} align="center">
|
||||||
|
<Paragraph weight="bolder" noMargin color="primary" size="md">
|
||||||
|
ADDING NEW OWNER ↓
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row className={classes.selectedOwnerAdded}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={values.ownerAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph weight="bolder" size="lg" noMargin>
|
||||||
|
{values.ownerName}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{values.ownerAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={getEtherScanLink(values.ownerAddress, network)} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
testId={REPLACE_OWNER_SUBMIT_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ReviewRemoveOwner)
|
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
|
||||||
|
<path fill="#4A5579" fill-rule="nonzero" d="M11.684 2.695a.669.669 0 0 0 0-.94L10.141.195a.652.652 0 0 0-.93 0l-1.215 1.22 2.475 2.5 1.213-1.22zM0 9.502v2.5h2.474l7.297-7.38-2.474-2.5L0 9.502z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 291 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="12" viewBox="0 0 15 12">
|
||||||
|
<path fill="#4A5579" fill-rule="nonzero" d="M12.1 12v-1.412H9.278V9.176h2.824V7.765l2.117 2.117L12.101 12zM5.749 0a2.824 2.824 0 1 1 0 5.647 2.824 2.824 0 0 1 0-5.647zm0 7.059c.812 0 1.588.085 2.287.24a4.265 4.265 0 0 0-.635 3.995H.1V9.882c0-1.56 2.528-2.823 5.648-2.823z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 370 B |
|
@ -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<OwnerData>
|
||||||
|
|
||||||
|
export const getOwnerData = (owners: List<Owner>): List<OwnerRow> => {
|
||||||
|
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])
|
||||||
|
}
|
|
@ -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<Owner>,
|
||||||
|
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<Props, State> {
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<Heading tag="h3" style={title}>Manage Safe Owners</Heading>
|
||||||
|
<Table
|
||||||
|
label="Owners"
|
||||||
|
defaultOrderBy={OWNERS_TABLE_NAME_ID}
|
||||||
|
columns={columns}
|
||||||
|
data={ownerData}
|
||||||
|
size={ownerData.size}
|
||||||
|
defaultFixed
|
||||||
|
noBorder
|
||||||
|
>
|
||||||
|
{(sortedData: Array<OwnerRow>) => sortedData.map((row: any, index: number) => (
|
||||||
|
<TableRow tabIndex={-1} key={index} className={classes.hide} data-testid={OWNERS_ROW_TESTID}>
|
||||||
|
{autoColumns.map((column: Column) => (
|
||||||
|
<TableCell key={column.id} style={cellWidth(column.width)} align={column.align} component="td">
|
||||||
|
{column.id === OWNERS_TABLE_ADDRESS_ID ? (
|
||||||
|
<OwnerAddressTableCell address={row[column.id]} />
|
||||||
|
) : (
|
||||||
|
row[column.id]
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
<TableCell component="td">
|
||||||
|
{granted && (
|
||||||
|
<Row align="end" className={classes.actions}>
|
||||||
|
<Img
|
||||||
|
alt="Edit owner"
|
||||||
|
className={classes.editOwnerIcon}
|
||||||
|
src={RenameOwnerIcon}
|
||||||
|
onClick={this.onShow('EditOwner', row)}
|
||||||
|
testId={RENAME_OWNER_BTN_TESTID}
|
||||||
|
/>
|
||||||
|
<Img
|
||||||
|
alt="Replace owner"
|
||||||
|
className={classes.replaceOwnerIcon}
|
||||||
|
src={ReplaceOwnerIcon}
|
||||||
|
onClick={this.onShow('ReplaceOwner', row)}
|
||||||
|
testId={REPLACE_OWNER_BTN_TESTID}
|
||||||
|
/>
|
||||||
|
{ownerData.size > 1 && (
|
||||||
|
<Img
|
||||||
|
alt="Remove owner"
|
||||||
|
className={classes.removeOwnerIcon}
|
||||||
|
src={RemoveOwnerIcon}
|
||||||
|
onClick={this.onShow('RemoveOwner', row)}
|
||||||
|
testId={REMOVE_OWNER_BTN_TESTID}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Table>
|
||||||
|
</Block>
|
||||||
|
{granted && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Hairline />
|
||||||
|
<Row style={controlsStyle} align="end" grow>
|
||||||
|
<Col end="xs">
|
||||||
|
<Button
|
||||||
|
style={addOwnerButtonStyle}
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={this.onShow('AddOwner')}
|
||||||
|
testId={ADD_OWNER_BTN_TESTID}
|
||||||
|
>
|
||||||
|
Add new owner
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
<AddOwnerModal
|
||||||
|
onClose={this.onHide('AddOwner')}
|
||||||
|
isOpen={showAddOwner}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
safeName={safeName}
|
||||||
|
owners={owners}
|
||||||
|
threshold={threshold}
|
||||||
|
network={network}
|
||||||
|
userAddress={userAddress}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
addSafeOwner={addSafeOwner}
|
||||||
|
/>
|
||||||
|
<RemoveOwnerModal
|
||||||
|
onClose={this.onHide('RemoveOwner')}
|
||||||
|
isOpen={showRemoveOwner}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
safeName={safeName}
|
||||||
|
ownerAddress={selectedOwnerAddress}
|
||||||
|
ownerName={selectedOwnerName}
|
||||||
|
owners={owners}
|
||||||
|
threshold={threshold}
|
||||||
|
network={network}
|
||||||
|
userAddress={userAddress}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
removeSafeOwner={removeSafeOwner}
|
||||||
|
/>
|
||||||
|
<ReplaceOwnerModal
|
||||||
|
onClose={this.onHide('ReplaceOwner')}
|
||||||
|
isOpen={showReplaceOwner}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
safeName={safeName}
|
||||||
|
ownerAddress={selectedOwnerAddress}
|
||||||
|
ownerName={selectedOwnerName}
|
||||||
|
owners={owners}
|
||||||
|
network={network}
|
||||||
|
threshold={threshold}
|
||||||
|
userAddress={userAddress}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
replaceSafeOwner={replaceSafeOwner}
|
||||||
|
/>
|
||||||
|
<EditOwnerModal
|
||||||
|
onClose={this.onHide('EditOwner')}
|
||||||
|
isOpen={showEditOwner}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
ownerAddress={selectedOwnerAddress}
|
||||||
|
selectedOwnerName={selectedOwnerName}
|
||||||
|
owners={owners}
|
||||||
|
network={network}
|
||||||
|
editSafeOwner={editSafeOwner}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ManageOwners)
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -2,12 +2,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { history } from '~/store'
|
|
||||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||||
|
import { history } from '~/store'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
import Identicon from '~/components/Identicon'
|
import Identicon from '~/components/Identicon'
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" viewBox="0 0 10 12">
|
||||||
|
<path fill="#FB4F62" fill-rule="nonzero" d="M.442 2.953l.375 7.51c0 .849.702 1.537 1.57 1.537H7.57c.867 0 1.57-.688 1.57-1.538l.36-7.509H.442zm9.498-.532l.003-.36c0-.825-.489-1.278-1.145-1.278L6.972.785C6.972.35 6.55 0 6.116 0H3.843c-.434 0-.87.351-.87.785L1.144.783C.42.783 0 1.335 0 2.062l.003.359H9.94zM6.542 4.927a.433.433 0 1 1 .867 0v4.965a.433.433 0 1 1-.867 0V4.927zm-2.004 0a.433.433 0 1 1 .867 0v4.965a.433.433 0 1 1-.867 0V4.927zm-2.003 0a.434.434 0 0 1 .867 0v4.965a.433.433 0 1 1-.867 0V4.927z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 605 B |
|
@ -2,24 +2,32 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
|
import Span from '~/components/layout/Span'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
import RemoveSafeModal from './RemoveSafeModal'
|
import RemoveSafeModal from './RemoveSafeModal'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import ChangeSafeName from './ChangeSafeName'
|
import ChangeSafeName from './ChangeSafeName'
|
||||||
import ThresholdSettings from './ThresholdSettings'
|
import ThresholdSettings from './ThresholdSettings'
|
||||||
|
import ManageOwners from './ManageOwners'
|
||||||
|
import actions, { type Actions } from './actions'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
import RemoveSafeIcon from './assets/icons/bin.svg'
|
||||||
|
|
||||||
|
export const OWNERS_SETTINGS_TAB_TESTID = 'owner-settings-tab'
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
showRemoveSafe: boolean,
|
showRemoveSafe: boolean,
|
||||||
menuOptionIndex: number,
|
menuOptionIndex: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = Actions & {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
etherScanLink: string,
|
etherScanLink: string,
|
||||||
|
@ -27,8 +35,13 @@ type Props = {
|
||||||
safeName: string,
|
safeName: string,
|
||||||
owners: List<Owner>,
|
owners: List<Owner>,
|
||||||
threshold: number,
|
threshold: number,
|
||||||
|
network: string,
|
||||||
createTransaction: Function,
|
createTransaction: Function,
|
||||||
updateSafe: Function,
|
addSafeOwner: Function,
|
||||||
|
removeSafeOwner: Function,
|
||||||
|
replaceSafeOwner: Function,
|
||||||
|
editSafeOwner: Function,
|
||||||
|
userAddress: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = 'RemoveSafe'
|
type Action = 'RemoveSafe'
|
||||||
|
@ -59,10 +72,16 @@ class Settings extends React.Component<Props, State> {
|
||||||
etherScanLink,
|
etherScanLink,
|
||||||
safeAddress,
|
safeAddress,
|
||||||
safeName,
|
safeName,
|
||||||
updateSafe,
|
|
||||||
owners,
|
|
||||||
threshold,
|
threshold,
|
||||||
|
owners,
|
||||||
|
network,
|
||||||
|
userAddress,
|
||||||
createTransaction,
|
createTransaction,
|
||||||
|
updateSafe,
|
||||||
|
addSafeOwner,
|
||||||
|
removeSafeOwner,
|
||||||
|
replaceSafeOwner,
|
||||||
|
editSafeOwner,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -74,8 +93,11 @@ class Settings extends React.Component<Props, State> {
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={6} end="sm">
|
<Col xs={6} end="sm">
|
||||||
<Paragraph noMargin size="md" color="error" className={classes.links} onClick={this.onShow('RemoveSafe')}>
|
<Paragraph noMargin size="md" color="error" onClick={this.onShow('RemoveSafe')}>
|
||||||
Remove Safe
|
<Span className={cn(classes.links, classes.removeSafeText)}>
|
||||||
|
Remove Safe
|
||||||
|
</Span>
|
||||||
|
<Img alt="Trash Icon" className={classes.removeSafeIcon} src={RemoveSafeIcon} />
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<RemoveSafeModal
|
<RemoveSafeModal
|
||||||
onClose={this.onHide('RemoveSafe')}
|
onClose={this.onHide('RemoveSafe')}
|
||||||
|
@ -96,15 +118,18 @@ class Settings extends React.Component<Props, State> {
|
||||||
Safe name
|
Safe name
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
<Row
|
||||||
|
className={cn(classes.menuOption, menuOptionIndex === 2 && classes.active)}
|
||||||
|
onClick={this.handleChange(2)}
|
||||||
|
testId={OWNERS_SETTINGS_TAB_TESTID}
|
||||||
|
>
|
||||||
|
Owners (
|
||||||
|
{owners.size}
|
||||||
|
)
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
{granted && (
|
{granted && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Row
|
|
||||||
className={cn(classes.menuOption, menuOptionIndex === 2 && classes.active)}
|
|
||||||
onClick={this.handleChange(2)}
|
|
||||||
>
|
|
||||||
Owners
|
|
||||||
</Row>
|
|
||||||
<Hairline />
|
|
||||||
<Row
|
<Row
|
||||||
className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)}
|
className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)}
|
||||||
onClick={this.handleChange(3)}
|
onClick={this.handleChange(3)}
|
||||||
|
@ -112,13 +137,6 @@ class Settings extends React.Component<Props, State> {
|
||||||
Required confirmations
|
Required confirmations
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row
|
|
||||||
className={cn(classes.menuOption, menuOptionIndex === 4 && classes.active)}
|
|
||||||
onClick={this.handleChange(4)}
|
|
||||||
>
|
|
||||||
Modules
|
|
||||||
</Row>
|
|
||||||
<Hairline />
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -128,7 +146,22 @@ class Settings extends React.Component<Props, State> {
|
||||||
{menuOptionIndex === 1 && (
|
{menuOptionIndex === 1 && (
|
||||||
<ChangeSafeName safeAddress={safeAddress} safeName={safeName} updateSafe={updateSafe} />
|
<ChangeSafeName safeAddress={safeAddress} safeName={safeName} updateSafe={updateSafe} />
|
||||||
)}
|
)}
|
||||||
{granted && menuOptionIndex === 2 && <p>To be done</p>}
|
{menuOptionIndex === 2 && (
|
||||||
|
<ManageOwners
|
||||||
|
owners={owners}
|
||||||
|
threshold={threshold}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
safeName={safeName}
|
||||||
|
network={network}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
userAddress={userAddress}
|
||||||
|
addSafeOwner={addSafeOwner}
|
||||||
|
removeSafeOwner={removeSafeOwner}
|
||||||
|
replaceSafeOwner={replaceSafeOwner}
|
||||||
|
editSafeOwner={editSafeOwner}
|
||||||
|
granted={granted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{granted && menuOptionIndex === 3 && (
|
{granted && menuOptionIndex === 3 && (
|
||||||
<ThresholdSettings
|
<ThresholdSettings
|
||||||
owners={owners}
|
owners={owners}
|
||||||
|
@ -137,7 +170,6 @@ class Settings extends React.Component<Props, State> {
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{granted && menuOptionIndex === 4 && <p>To be done</p>}
|
|
||||||
</Block>
|
</Block>
|
||||||
</Col>
|
</Col>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -146,4 +178,9 @@ class Settings extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(Settings)
|
const settingsComponent = withStyles(styles)(Settings)
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
undefined,
|
||||||
|
actions,
|
||||||
|
)(settingsComponent)
|
||||||
|
|
|
@ -39,4 +39,14 @@ export const styles = () => ({
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
removeSafeText: {
|
||||||
|
height: '16px',
|
||||||
|
lineHeight: '16px',
|
||||||
|
paddingRight: sm,
|
||||||
|
float: 'left',
|
||||||
|
},
|
||||||
|
removeSafeIcon: {
|
||||||
|
height: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = crea
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return owners.find((owner: Owner) => sameAddress(owner.get('address'), userAccount)) !== undefined
|
return owners.find((owner: Owner) => sameAddress(owner.address, userAccount)) !== undefined
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const ADD_SAFE_OWNER = 'ADD_SAFE_OWNER'
|
||||||
|
|
||||||
|
const addSafeOwner = createAction<string, *>(ADD_SAFE_OWNER)
|
||||||
|
|
||||||
|
export default addSafeOwner
|
|
@ -31,6 +31,7 @@ const createTransaction = (
|
||||||
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
||||||
openSnackbar('Transaction has been confirmed', 'success')
|
openSnackbar('Transaction has been confirmed', 'success')
|
||||||
} else {
|
} else {
|
||||||
|
console.log('Temporal error: threshold != 1')
|
||||||
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
||||||
}
|
}
|
||||||
// dispatch(addTransactions(txHash))
|
// dispatch(addTransactions(txHash))
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const EDIT_SAFE_OWNER = 'EDIT_SAFE_OWNER'
|
||||||
|
|
||||||
|
const editSafeOwner = createAction<string, *>(EDIT_SAFE_OWNER)
|
||||||
|
|
||||||
|
export default editSafeOwner
|
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const REMOVE_SAFE_OWNER = 'REMOVE_SAFE_OWNER'
|
||||||
|
|
||||||
|
const removeSafeOwner = createAction<string, *>(REMOVE_SAFE_OWNER)
|
||||||
|
|
||||||
|
export default removeSafeOwner
|
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const REPLACE_SAFE_OWNER = 'REPLACE_SAFE_OWNER'
|
||||||
|
|
||||||
|
const replaceSafeOwner = createAction<string, *>(REPLACE_SAFE_OWNER)
|
||||||
|
|
||||||
|
export default replaceSafeOwner
|
|
@ -1,18 +1,33 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import type { Store, AnyAction } from 'redux'
|
||||||
|
import { List } from 'immutable'
|
||||||
import { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
import { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
||||||
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
||||||
import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe'
|
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 { type GlobalState } from '~/store/'
|
||||||
import { saveSafes, setOwners, removeOwners } from '~/logic/safe/utils'
|
import { saveSafes, setOwners, removeOwners } from '~/logic/safe/utils'
|
||||||
import { safesMapSelector } from '~/routes/safeList/store/selectors'
|
import { safesMapSelector } from '~/routes/safeList/store/selectors'
|
||||||
import { getActiveTokensAddressesForAllSafes } from '~/routes/safe/store/selectors'
|
import { getActiveTokensAddressesForAllSafes } from '~/routes/safe/store/selectors'
|
||||||
import { tokensSelector } from '~/logic/tokens/store/selectors'
|
import { tokensSelector } from '~/logic/tokens/store/selectors'
|
||||||
import type { Token } from '~/logic/tokens/store/model/token'
|
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 { saveActiveTokens } from '~/logic/tokens/utils/tokensStorage'
|
||||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes'
|
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<GlobalState>) => (next: Function) => async (action: AnyAction) => {
|
const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => async (action: AnyAction) => {
|
||||||
const handledAction = next(action)
|
const handledAction = next(action)
|
||||||
|
@ -20,30 +35,78 @@ const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => asyn
|
||||||
if (watchedActions.includes(action.type)) {
|
if (watchedActions.includes(action.type)) {
|
||||||
const state: GlobalState = store.getState()
|
const state: GlobalState = store.getState()
|
||||||
const safes = safesMapSelector(state)
|
const safes = safesMapSelector(state)
|
||||||
saveSafes(safes.toJSON())
|
await saveSafes(safes.toJSON())
|
||||||
|
|
||||||
// recalculate active tokens
|
switch (action.type) {
|
||||||
if (action.payload.activeTokens || action.type === ACTIVATE_TOKEN_FOR_ALL_SAFES) {
|
case ACTIVATE_TOKEN_FOR_ALL_SAFES: {
|
||||||
const tokens = tokensSelector(state)
|
let { activeTokens } = action.payload
|
||||||
const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state)
|
if (activeTokens) {
|
||||||
|
const tokens = tokensSelector(state)
|
||||||
|
const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state)
|
||||||
|
|
||||||
const activeTokens = tokens.withMutations((map) => {
|
activeTokens = tokens.withMutations((map) => {
|
||||||
map.forEach((token: Token) => {
|
map.forEach((token: Token) => {
|
||||||
if (!activeTokenAddresses.has(token.address)) {
|
if (!activeTokenAddresses.has(token.address)) {
|
||||||
map.remove(token.address)
|
map.remove(token.address)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
saveActiveTokens(activeTokens)
|
saveActiveTokens(activeTokens)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
case ADD_SAFE: {
|
||||||
if (action.type === ADD_SAFE) {
|
|
||||||
const { safe } = action.payload
|
const { safe } = action.payload
|
||||||
setOwners(safe.address, safe.owners)
|
setOwners(safe.address, safe.owners)
|
||||||
} else if (action.type === REMOVE_SAFE) {
|
break
|
||||||
const safeAddress = action.payload
|
}
|
||||||
removeOwners(safeAddress)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,16 @@ import { handleActions, type ActionType } from 'redux-actions'
|
||||||
import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
|
import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
|
||||||
import SafeRecord, { type Safe, type SafeProps } from '~/routes/safe/store/models/safe'
|
import SafeRecord, { type Safe, type SafeProps } from '~/routes/safe/store/models/safe'
|
||||||
import TokenBalance from '~/routes/safe/store/models/tokenBalance'
|
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 { loadFromStorage } from '~/utils/storage'
|
||||||
import { SAFES_KEY } from '~/logic/safe/utils'
|
import { SAFES_KEY } from '~/logic/safe/utils'
|
||||||
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
||||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes'
|
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes'
|
||||||
import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe'
|
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'
|
export const SAFE_REDUCER_ID = 'safes'
|
||||||
|
|
||||||
|
@ -97,6 +101,42 @@ export default handleActions<State, *>(
|
||||||
|
|
||||||
return state.delete(safeAddress)
|
return state.delete(safeAddress)
|
||||||
},
|
},
|
||||||
|
[ADD_SAFE_OWNER]: (state: State, action: ActionType<Function>): 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<Function>): 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<Function>): 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<Function>): 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(),
|
Map(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,11 +3,10 @@ import * as React from 'react'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import { type Store } from 'redux'
|
import { type Store } from 'redux'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
import { ConnectedRouter } from 'connected-react-router'
|
import { ConnectedRouter } from 'connected-react-router'
|
||||||
import PageFrame from '~/components/layout/PageFrame'
|
import PageFrame from '~/components/layout/PageFrame'
|
||||||
import { render } from '@testing-library/react'
|
|
||||||
import ListItemText from '~/components/List/ListItemText/index'
|
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 fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
import { sleep } from '~/utils/timer'
|
import { sleep } from '~/utils/timer'
|
||||||
import { history } from '~/store'
|
import { history } from '~/store'
|
||||||
|
@ -23,16 +22,6 @@ export const EDIT_INDEX = 4
|
||||||
export const WITHDRAW_INDEX = 5
|
export const WITHDRAW_INDEX = 5
|
||||||
export const LIST_TXS_INDEX = 6
|
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<any, any>, name: string) => {
|
export const checkMinedTx = (Transaction: React.Component<any, any>, name: string) => {
|
||||||
const paragraphs = TestUtils.scryRenderedDOMComponentsWithTag(Transaction, 'p')
|
const paragraphs = TestUtils.scryRenderedDOMComponentsWithTag(Transaction, 'p')
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { SAFE_NAME_INPUT_TESTID, SAFE_NAME_SUBMIT_BTN_TESTID } from '~/routes/sa
|
||||||
|
|
||||||
afterEach(cleanup)
|
afterEach(cleanup)
|
||||||
|
|
||||||
describe('DOM > Feature > Settings', () => {
|
describe('DOM > Feature > Settings - Name', () => {
|
||||||
let store
|
let store
|
||||||
let safeAddress
|
let safeAddress
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -3,15 +3,9 @@
|
||||||
// TBD
|
// TBD
|
||||||
|
|
||||||
describe('DOM > Feature > SAFE MULTISIG Transactions', () => {
|
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 () => {
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
|
||||||
import { fireEvent } from '@testing-library/react'
|
import { fireEvent } from '@testing-library/react'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { getFirstTokenContract } from '~/test/utils/tokenMovements'
|
import { getFirstTokenContract } from '~/test/utils/tokenMovements'
|
||||||
import { aNewStore } from '~/store'
|
import { aNewStore } from '~/store'
|
||||||
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { fireEvent } from '@testing-library/react'
|
import { fireEvent } from '@testing-library/react'
|
||||||
import { MANAGE_TOKENS_BUTTON_TEST_ID } from '~/routes/safe/components/Balances'
|
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'
|
import { MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID } from '~/routes/safe/components/Balances/Tokens'
|
||||||
|
|
||||||
export const clickOnManageTokens = (dom: any): void => {
|
export const clickOnManageTokens = (dom: any): void => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
|
||||||
import abi from 'ethereumjs-abi'
|
import abi from 'ethereumjs-abi'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
console.log(`to[${to}] \n\n valieInWei[${valueInWei}] \n\n
|
console.log(`to[${to}] \n\n valieInWei[${valueInWei}] \n\n
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
|
||||||
import GnoStepper from '~/components/Stepper'
|
|
||||||
import Stepper from '@material-ui/core/Stepper'
|
import Stepper from '@material-ui/core/Stepper'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
|
import GnoStepper from '~/components/Stepper'
|
||||||
|
|
||||||
export const printOutApprove = async (
|
export const printOutApprove = async (
|
||||||
subject: string,
|
subject: string,
|
||||||
|
@ -38,7 +38,10 @@ type FinsihedTx = {
|
||||||
finishedTransaction: boolean,
|
finishedTransaction: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const whenExecuted = (SafeDom: React.Component<any, any>, ParentComponent: React.ElementType): Promise<void> => new Promise((resolve, reject) => {
|
export const whenExecuted = (
|
||||||
|
SafeDom: React.Component<any, any>,
|
||||||
|
ParentComponent: React.ElementType,
|
||||||
|
): Promise<void> => new Promise((resolve, reject) => {
|
||||||
let times = 0
|
let times = 0
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (times >= MAX_TIMES_EXECUTED) {
|
if (times >= MAX_TIMES_EXECUTED) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { type Match } from 'react-router-dom'
|
||||||
import { buildMatchPropsFrom } from '~/test/utils/buildReactRouterProps'
|
import { buildMatchPropsFrom } from '~/test/utils/buildReactRouterProps'
|
||||||
import { safeSelector } from '~/routes/safe/store/selectors/index'
|
import { safeSelector } from '~/routes/safe/store/selectors/index'
|
||||||
import { type Match } from 'react-router-dom'
|
|
||||||
import { type GlobalState } from '~/store'
|
import { type GlobalState } from '~/store'
|
||||||
import { type Safe } from '~/routes/safe/store/models/safe'
|
import { type Safe } from '~/routes/safe/store/models/safe'
|
||||||
|
|
||||||
|
|
|
@ -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<any, any>,
|
|
||||||
addOwner: React.Component<any, any>,
|
|
||||||
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<any, any>, name: string) => {
|
|
||||||
checkMinedTx(Transaction, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkPendingAddOwnerTx = async (
|
|
||||||
Transaction: React.Component<any, any>,
|
|
||||||
safeThreshold: number,
|
|
||||||
name: string,
|
|
||||||
statusses: string[],
|
|
||||||
) => {
|
|
||||||
await checkPendingTx(Transaction, safeThreshold, name, statusses)
|
|
||||||
}
|
|
|
@ -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<any, any>,
|
|
||||||
expandOwners: React.Component<any, any>,
|
|
||||||
) => {
|
|
||||||
// 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<any, any>, name: string) => {
|
|
||||||
checkMinedTx(Transaction, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkPendingRemoveOwnerTx = async (
|
|
||||||
Transaction: React.Component<any, any>,
|
|
||||||
safeThreshold: number,
|
|
||||||
name: string,
|
|
||||||
statusses: string[],
|
|
||||||
) => {
|
|
||||||
await checkPendingTx(Transaction, safeThreshold, name, statusses)
|
|
||||||
}
|
|
454
yarn.lock
454
yarn.lock
|
@ -2,10 +2,10 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@babel/cli@7.4.4":
|
"@babel/cli@7.5.0":
|
||||||
version "7.4.4"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.4.tgz#5454bb7112f29026a4069d8e6f0e1794e651966c"
|
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.5.0.tgz#f403c930692e28ecfa3bf02a9e7562b474f38271"
|
||||||
integrity sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ==
|
integrity sha512-qNH55fWbKrEsCwID+Qc/3JDPnsSGpIIiMDbppnR8Z6PxLAqMQCFNqBctkIkBrMH49Nx+qqVTrHRWUR+ho2k+qQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
commander "^2.8.1"
|
commander "^2.8.1"
|
||||||
convert-source-map "^1.1.0"
|
convert-source-map "^1.1.0"
|
||||||
|
@ -46,7 +46,27 @@
|
||||||
semver "^5.4.1"
|
semver "^5.4.1"
|
||||||
source-map "^0.5.0"
|
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"
|
version "7.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
|
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
|
||||||
integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==
|
integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==
|
||||||
|
@ -77,6 +97,17 @@
|
||||||
source-map "^0.5.0"
|
source-map "^0.5.0"
|
||||||
trim-right "^1.0.1"
|
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":
|
"@babel/helper-annotate-as-pure@^7.0.0":
|
||||||
version "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"
|
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-replace-supers" "^7.4.4"
|
||||||
"@babel/helper-split-export-declaration" "^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":
|
"@babel/helper-define-map@^7.4.0", "@babel/helper-define-map@^7.4.4":
|
||||||
version "7.4.4"
|
version "7.4.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a"
|
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/traverse" "^7.4.4"
|
||||||
"@babel/types" "^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":
|
"@babel/highlight@^7.0.0":
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
|
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"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872"
|
||||||
integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==
|
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":
|
"@babel/plugin-proposal-async-generator-functions@^7.2.0":
|
||||||
version "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"
|
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-create-class-features-plugin" "^7.4.0"
|
||||||
"@babel/helper-plugin-utils" "^7.0.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"
|
version "7.4.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce"
|
||||||
integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg==
|
integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg==
|
||||||
|
@ -326,6 +391,14 @@
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@babel/helper-plugin-utils" "^7.0.0"
|
||||||
"@babel/plugin-syntax-do-expressions" "^7.2.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":
|
"@babel/plugin-proposal-export-default-from@^7.0.0":
|
||||||
version "7.2.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"
|
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/helper-plugin-utils" "^7.0.0"
|
||||||
"@babel/plugin-syntax-object-rest-spread" "^7.2.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":
|
"@babel/plugin-proposal-optional-catch-binding@^7.2.0":
|
||||||
version "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"
|
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-plugin-utils" "^7.0.0"
|
||||||
"@babel/helper-remap-async-to-generator" "^7.1.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":
|
"@babel/plugin-transform-block-scoped-functions@^7.2.0":
|
||||||
version "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"
|
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:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@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":
|
"@babel/plugin-transform-dotall-regex@^7.4.3", "@babel/plugin-transform-dotall-regex@^7.4.4":
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3"
|
||||||
|
@ -691,6 +788,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@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":
|
"@babel/plugin-transform-exponentiation-operator@^7.2.0":
|
||||||
version "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"
|
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-module-transforms" "^7.1.0"
|
||||||
"@babel/helper-plugin-utils" "^7.0.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":
|
"@babel/plugin-transform-modules-commonjs@^7.4.3", "@babel/plugin-transform-modules-commonjs@^7.4.4":
|
||||||
version "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"
|
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-plugin-utils" "^7.0.0"
|
||||||
"@babel/helper-simple-access" "^7.1.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":
|
"@babel/plugin-transform-modules-systemjs@^7.4.0", "@babel/plugin-transform-modules-systemjs@^7.4.4":
|
||||||
version "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"
|
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-hoist-variables" "^7.4.4"
|
||||||
"@babel/helper-plugin-utils" "^7.0.0"
|
"@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":
|
"@babel/plugin-transform-modules-umd@^7.2.0":
|
||||||
version "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"
|
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"
|
js-levenshtein "^1.1.3"
|
||||||
semver "^5.5.0"
|
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"
|
version "7.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58"
|
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58"
|
||||||
integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==
|
integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==
|
||||||
|
@ -1129,6 +1317,21 @@
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
lodash "^4.17.11"
|
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":
|
"@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"
|
version "7.4.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0"
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0"
|
||||||
|
@ -1138,6 +1341,15 @@
|
||||||
lodash "^4.17.11"
|
lodash "^4.17.11"
|
||||||
to-fast-properties "^2.0.0"
|
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":
|
"@cnakazawa/watch@^1.0.3":
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
|
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:
|
dependencies:
|
||||||
object.assign "^4.1.0"
|
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"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
|
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==
|
integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
|
||||||
|
@ -6535,10 +6747,10 @@ eslint-plugin-import@2.18.0:
|
||||||
read-pkg-up "^2.0.0"
|
read-pkg-up "^2.0.0"
|
||||||
resolve "^1.11.0"
|
resolve "^1.11.0"
|
||||||
|
|
||||||
eslint-plugin-jest@22.7.1:
|
eslint-plugin-jest@22.7.2:
|
||||||
version "22.7.1"
|
version "22.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.1.tgz#5dcdf8f7a285f98040378220d6beca581f0ab2a1"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.2.tgz#7ab118a66a34e46ae5e16a128b5d24fd28b43dca"
|
||||||
integrity sha512-CrT3AzA738neimv8G8iK2HCkrCwHnAJeeo7k5TEHK86VMItKl6zdJT/tHBDImfnVVAYsVs4Y6BUdBZQCCgfiyw==
|
integrity sha512-Aecqe3ulBVI7amgOycVI8ZPL8o0SnGHOf3zn2/Ciu8TXyXDHcjtwD3hOs3ss/Qh/VAwlW/DMcuiXg5btgF+XMA==
|
||||||
|
|
||||||
eslint-plugin-jsx-a11y@^6.0.3:
|
eslint-plugin-jsx-a11y@^6.0.3:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
|
@ -7768,12 +7980,12 @@ fs-extra@6.0.1, fs-extra@^6.0.1:
|
||||||
jsonfile "^4.0.0"
|
jsonfile "^4.0.0"
|
||||||
universalify "^0.1.0"
|
universalify "^0.1.0"
|
||||||
|
|
||||||
fs-extra@8.0.1, fs-extra@^8.0.1:
|
fs-extra@8.1.0:
|
||||||
version "8.0.1"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.0.1.tgz#90294081f978b1f182f347a440a209154344285b"
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
|
||||||
integrity sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==
|
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.2.0"
|
||||||
jsonfile "^4.0.0"
|
jsonfile "^4.0.0"
|
||||||
universalify "^0.1.0"
|
universalify "^0.1.0"
|
||||||
|
|
||||||
|
@ -7824,6 +8036,15 @@ fs-extra@^7.0.0:
|
||||||
jsonfile "^4.0.0"
|
jsonfile "^4.0.0"
|
||||||
universalify "^0.1.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:
|
fs-minipass@^1.2.5:
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
|
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"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
|
||||||
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
|
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":
|
"graceful-readlink@>= 1.0.0":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
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"
|
react-fast-compare "2.0.4"
|
||||||
shallowequal "1.1.0"
|
shallowequal "1.1.0"
|
||||||
|
|
||||||
react-hot-loader@4.11.1:
|
react-hot-loader@4.12.3:
|
||||||
version "4.11.1"
|
version "4.12.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.11.1.tgz#2cabbd0f1c8a44c28837b86d6ce28521e6d9a8ac"
|
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.3.tgz#0972255cd110a00860902e82bb2b789a262cfe01"
|
||||||
integrity sha512-HAC0UedYzM3mD+ZaQHesntFO0yi2ftOV4ZMMRTj43E4GvW5sQqYTPvur+6J7EaH3MDr/RqjDKXyCqKepV8+y7w==
|
integrity sha512-XBhxogFOxEh8L4Ykdk2mp704Xc/eoy+bwadEYMvmBhjAz3wg+DfMpINMkA+kLTRDinqwjssDfA9DhUJznRjvuA==
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-levenshtein "^2.0.6"
|
fast-levenshtein "^2.0.6"
|
||||||
global "^4.3.0"
|
global "^4.3.0"
|
||||||
|
@ -13987,7 +14213,7 @@ request-promise-core@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.11"
|
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"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59"
|
resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59"
|
||||||
integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==
|
integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==
|
||||||
|
@ -15729,10 +15955,10 @@ truffle-artifactor@^2.1.2:
|
||||||
truffle-contract "^2.0.3"
|
truffle-contract "^2.0.3"
|
||||||
truffle-contract-schema "^0.0.5"
|
truffle-contract-schema "^0.0.5"
|
||||||
|
|
||||||
truffle-artifactor@^4.0.20:
|
truffle-artifactor@^4.0.22:
|
||||||
version "4.0.20"
|
version "4.0.22"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-artifactor/-/truffle-artifactor-4.0.20.tgz#1baba681f407485fda7c408976ce0ad31a13d5ac"
|
resolved "https://registry.yarnpkg.com/truffle-artifactor/-/truffle-artifactor-4.0.22.tgz#3ca2061842ac0803b83538a11126fd2d8480d4e6"
|
||||||
integrity sha512-IX+KvGNPkDQlMW1+9txYYn1+6IkvLPFiIh+hIjj2E7yYg9XS3zYxYbWg/wTcGyZsdtHwRVLOks7EvA0k+5LD1w==
|
integrity sha512-jqgLahO/29TnWvBZAj9Vo58LUs82fR5FcPvof5sy8q+b4y/nRc+v4cOWFYIZPkLo8sRRotN/U0T17S7CyrR9wQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
fs-extra "6.0.1"
|
fs-extra "6.0.1"
|
||||||
lodash "4.17.11"
|
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"
|
resolved "https://registry.yarnpkg.com/truffle-blockchain-utils/-/truffle-blockchain-utils-0.0.5.tgz#a4e5c064dadd69f782a137f3d276d21095da7a47"
|
||||||
integrity sha1-pOXAZNrdafeCoTfz0nbSEJXaekc=
|
integrity sha1-pOXAZNrdafeCoTfz0nbSEJXaekc=
|
||||||
|
|
||||||
truffle-box@^1.0.28:
|
truffle-box@^1.0.29:
|
||||||
version "1.0.28"
|
version "1.0.29"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-box/-/truffle-box-1.0.28.tgz#da117d387db4048e2e556080739a60e446f71dda"
|
resolved "https://registry.yarnpkg.com/truffle-box/-/truffle-box-1.0.29.tgz#dd785925541f2bb024e0fe34276d1f53af6546e6"
|
||||||
integrity sha512-mOmo2EdB0gUcV84pB6xPPhTsycLjjjz90kSd4V47IqJhx6dDSx6jEs/wBDNcA0gcEVf54x1IcuBgvnPZhH0wXw==
|
integrity sha512-QdhEIAae+Jdf/QvheOFrz0gVaR09Sy9Q36FG2lCJVmrA0hxTT+JWJxQ7EXn3CyZD03KGy8ORUHyDAJX8bX/QUA==
|
||||||
dependencies:
|
dependencies:
|
||||||
fs-extra "6.0.1"
|
fs-extra "6.0.1"
|
||||||
github-download "^0.5.0"
|
github-download "^0.5.0"
|
||||||
ora "^3.0.0"
|
ora "^3.0.0"
|
||||||
request "^2.85.0"
|
request "^2.85.0"
|
||||||
|
request-promise-native "^1.0.7"
|
||||||
tmp "0.0.33"
|
tmp "0.0.33"
|
||||||
truffle-config "^1.1.13"
|
truffle-config "^1.1.13"
|
||||||
vcsurl "^0.1.1"
|
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"
|
resolved "https://registry.yarnpkg.com/truffle-code-utils/-/truffle-code-utils-1.2.4.tgz#19acbc225a5c99081c7aa4bbda122f6fbf2d27e9"
|
||||||
integrity sha512-MtIusUMxJeJlOqoqda4DYJ86gqSGDVMGiqhVEVgHL2J5L4plci/uePgYROajklXE9H/g6u7yotnAKOhCdTB9/A==
|
integrity sha512-MtIusUMxJeJlOqoqda4DYJ86gqSGDVMGiqhVEVgHL2J5L4plci/uePgYROajklXE9H/g6u7yotnAKOhCdTB9/A==
|
||||||
|
|
||||||
truffle-compile-vyper@^1.0.18:
|
truffle-compile-vyper@^1.0.20:
|
||||||
version "1.0.18"
|
version "1.0.20"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-compile-vyper/-/truffle-compile-vyper-1.0.18.tgz#f9ed4c4141a76edfd1b32f6fd87eb95cd96495d3"
|
resolved "https://registry.yarnpkg.com/truffle-compile-vyper/-/truffle-compile-vyper-1.0.20.tgz#3984ffb17ca2bc9a4f207e1d930d0a27e22ff9f6"
|
||||||
integrity sha512-MAsmYqY0eFklYtG3IBtBqlT0sibyq2yox2T356R1XGh/KpsFC1AbjyxsSTBL+pzpSb0ro3Fd2qZkluS1sSoijg==
|
integrity sha512-LkF+puWaSg+1AjMl/hWrQS2DlbrB3rDuN3Ap974gXTwagLUDc4yvmN6luQ3Wd5Irn89ugX6lYrzXaETfrEsD+Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
async "2.6.1"
|
async "2.6.1"
|
||||||
colors "^1.1.2"
|
colors "^1.1.2"
|
||||||
eslint "^5.5.0"
|
eslint "^5.5.0"
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
truffle-compile "^4.1.1"
|
truffle-compile "^4.1.3"
|
||||||
|
|
||||||
truffle-compile@^4.1.1:
|
truffle-compile@^4.1.3:
|
||||||
version "4.1.1"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-compile/-/truffle-compile-4.1.1.tgz#581a3a6b851282b17ba010b141b8c1f5c9667420"
|
resolved "https://registry.yarnpkg.com/truffle-compile/-/truffle-compile-4.1.3.tgz#698a650d27b961b12e0594c078ec870a931eafde"
|
||||||
integrity sha512-b4XTcK2L/nb11ekeZy3Q4cx0DRaoqrZ4UQfd0cp6CXq3IVqMuEF+lNXjHnu+tIbzhMN2Zov1xV5DtoJC3vOJmw==
|
integrity sha512-pCMhw3r5+M9RNe37/Hvzjk8FrOvTXm3qpuGufynQCMWYcsOwLPyLrMRmgtTWL5sABP42JuNpM4xrfBDqatmv6Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
async "2.6.1"
|
|
||||||
colors "^1.1.2"
|
colors "^1.1.2"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
|
fs-extra "^8.0.1"
|
||||||
ora "^3.0.0"
|
ora "^3.0.0"
|
||||||
original-require "^1.0.1"
|
original-require "^1.0.1"
|
||||||
request "^2.85.0"
|
request "^2.85.0"
|
||||||
|
@ -15800,7 +16027,7 @@ truffle-compile@^4.1.1:
|
||||||
semver "^5.6.0"
|
semver "^5.6.0"
|
||||||
solc "^0.5.0"
|
solc "^0.5.0"
|
||||||
truffle-config "^1.1.13"
|
truffle-config "^1.1.13"
|
||||||
truffle-contract-sources "^0.1.4"
|
truffle-contract-sources "^0.1.5"
|
||||||
truffle-error "^0.0.5"
|
truffle-error "^0.0.5"
|
||||||
truffle-expect "^0.0.9"
|
truffle-expect "^0.0.9"
|
||||||
|
|
||||||
|
@ -15841,10 +16068,10 @@ truffle-contract-schema@^3.0.11:
|
||||||
crypto-js "^3.1.9-1"
|
crypto-js "^3.1.9-1"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
|
|
||||||
truffle-contract-sources@^0.1.4:
|
truffle-contract-sources@^0.1.5:
|
||||||
version "0.1.4"
|
version "0.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-contract-sources/-/truffle-contract-sources-0.1.4.tgz#bc6dacfe8a3744e00dc9238e11e70d8951c4c7b1"
|
resolved "https://registry.yarnpkg.com/truffle-contract-sources/-/truffle-contract-sources-0.1.5.tgz#cacd788b7904d53747fa88d04412b77a1826b46f"
|
||||||
integrity sha512-Evi9UEaOYz99XVkl11rrlAR6lho0rZzByEoqpdbPPku+z0ONeyqXls3fKoTtwp7Zrus4ihJKS/AIiQOSm93ccA==
|
integrity sha512-skhZ8s3sLM4PyRFqn8zbBWG2z1P5zX7ndQPYokr2aiez8o44x5hmUoPeiXgKlMYIyR1Y1zOyNpaVEZyBY1+4Cw==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
glob "^7.1.2"
|
glob "^7.1.2"
|
||||||
|
@ -15865,10 +16092,10 @@ truffle-contract@4.0.0-next.0:
|
||||||
web3-eth-abi "1.0.0-beta.35"
|
web3-eth-abi "1.0.0-beta.35"
|
||||||
web3-utils "1.0.0-beta.35"
|
web3-utils "1.0.0-beta.35"
|
||||||
|
|
||||||
truffle-contract@4.0.21, truffle-contract@^4.0.21:
|
truffle-contract@4.0.23, truffle-contract@^4.0.23:
|
||||||
version "4.0.21"
|
version "4.0.23"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-contract/-/truffle-contract-4.0.21.tgz#aa336764d9712346ee1914b383b43a57f6ff3962"
|
resolved "https://registry.yarnpkg.com/truffle-contract/-/truffle-contract-4.0.23.tgz#a1e5a83972552bf81e879323eb1990970496db9f"
|
||||||
integrity sha512-nFoEYDCyDihw3eDC7aHSJcNVP7WQbKl0N8umxXS5gAb3AujlNeV0OCV5lbAMMY4KwyfwaZp+uVjLnK/AnuSbww==
|
integrity sha512-aTiKSRoum01n9G77ESAqRvCoK8S5FBjNdN1NKkibAUfcdZTIm2VmFcmW702nKGX8hMAU+pSY3cXNaI8VhIdAfQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
bignumber.js "^7.2.1"
|
bignumber.js "^7.2.1"
|
||||||
ethers "^4.0.0-beta.1"
|
ethers "^4.0.0-beta.1"
|
||||||
|
@ -15891,10 +16118,10 @@ truffle-contract@^2.0.3:
|
||||||
truffle-contract-schema "^0.0.5"
|
truffle-contract-schema "^0.0.5"
|
||||||
web3 "^0.20.1"
|
web3 "^0.20.1"
|
||||||
|
|
||||||
truffle-core@^5.0.24:
|
truffle-core@^5.0.26:
|
||||||
version "5.0.24"
|
version "5.0.26"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-core/-/truffle-core-5.0.24.tgz#80e6694032411aca6d98c0f4e38d49143df25a5c"
|
resolved "https://registry.yarnpkg.com/truffle-core/-/truffle-core-5.0.26.tgz#d9ee01bf62dca5d8c1c48825547de93ef5637e44"
|
||||||
integrity sha512-rAjOUZnfGBivlwvkM2EyzL3tQJAqyDq/5IVgwR4LkM/dlYi5ZeIjA7Jj8LiUwtlmUiuYJi0R6bpk/cTjtJFaJQ==
|
integrity sha512-ylli1AcILbCFwuzXCjEdDyQCaGURKcQmYXNwuQqDhccFM2MbB+vohNXyoXHI5rT4SzIkaLfn7a4MaJEsGZXcbA==
|
||||||
dependencies:
|
dependencies:
|
||||||
app-module-path "^2.2.0"
|
app-module-path "^2.2.0"
|
||||||
async "2.6.1"
|
async "2.6.1"
|
||||||
|
@ -15923,26 +16150,26 @@ truffle-core@^5.0.24:
|
||||||
source-map-support "^0.5.3"
|
source-map-support "^0.5.3"
|
||||||
spawn-args "^0.1.0"
|
spawn-args "^0.1.0"
|
||||||
temp "^0.8.3"
|
temp "^0.8.3"
|
||||||
truffle-artifactor "^4.0.20"
|
truffle-artifactor "^4.0.22"
|
||||||
truffle-box "^1.0.28"
|
truffle-box "^1.0.29"
|
||||||
truffle-compile "^4.1.1"
|
truffle-compile "^4.1.3"
|
||||||
truffle-config "^1.1.13"
|
truffle-config "^1.1.13"
|
||||||
truffle-contract "^4.0.21"
|
truffle-contract "^4.0.23"
|
||||||
truffle-contract-sources "^0.1.4"
|
truffle-contract-sources "^0.1.5"
|
||||||
truffle-debug-utils "^1.0.18"
|
truffle-debug-utils "^1.0.18"
|
||||||
truffle-debugger "^5.0.16"
|
truffle-debugger "^5.0.18"
|
||||||
truffle-deployer "^3.0.22"
|
truffle-deployer "^3.0.24"
|
||||||
truffle-error "^0.0.5"
|
truffle-error "^0.0.5"
|
||||||
truffle-expect "^0.0.9"
|
truffle-expect "^0.0.9"
|
||||||
truffle-init "^1.0.7"
|
truffle-init "^1.0.7"
|
||||||
truffle-interface-adapter "^0.1.6"
|
truffle-interface-adapter "^0.1.6"
|
||||||
truffle-migrate "^3.0.22"
|
truffle-migrate "^3.0.24"
|
||||||
truffle-provider "^0.1.10"
|
truffle-provider "^0.1.10"
|
||||||
truffle-provisioner "^0.1.5"
|
truffle-provisioner "^0.1.5"
|
||||||
truffle-require "^2.0.13"
|
truffle-require "^2.0.13"
|
||||||
truffle-resolver "^5.0.14"
|
truffle-resolver "^5.0.14"
|
||||||
truffle-solidity-utils "^1.2.3"
|
truffle-solidity-utils "^1.2.3"
|
||||||
truffle-workflow-compile "^2.0.20"
|
truffle-workflow-compile "^2.0.22"
|
||||||
universal-analytics "^0.4.17"
|
universal-analytics "^0.4.17"
|
||||||
web3 "1.0.0-beta.37"
|
web3 "1.0.0-beta.37"
|
||||||
xregexp "^4.2.4"
|
xregexp "^4.2.4"
|
||||||
|
@ -15957,10 +16184,10 @@ truffle-debug-utils@^1.0.18:
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
node-dir "0.1.17"
|
node-dir "0.1.17"
|
||||||
|
|
||||||
truffle-debugger@^5.0.16:
|
truffle-debugger@^5.0.18:
|
||||||
version "5.0.16"
|
version "5.0.18"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-debugger/-/truffle-debugger-5.0.16.tgz#b2b42609357c127479b617be6c1fb410374d8789"
|
resolved "https://registry.yarnpkg.com/truffle-debugger/-/truffle-debugger-5.0.18.tgz#0b7162ffc983188eabac046c595cfead0cad7fb5"
|
||||||
integrity sha512-aeCRZPmowbFm8D0s8/e+FQPhStxcGpsEnZ2B446GHhSB+Y04fcqohOQqyPDXyua2P1nkLfuxhWBxFW2qgr23ig==
|
integrity sha512-5gtnUpx0HlY5J2W8BS9M4kO9KI392uk//cGBgKWsX6BuevS1Q8f4tiKwOr5yIIkPNt7Ua1ksU0O9N9cz29TpDw==
|
||||||
dependencies:
|
dependencies:
|
||||||
bn.js "^4.11.8"
|
bn.js "^4.11.8"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
|
@ -15973,7 +16200,7 @@ truffle-debugger@^5.0.16:
|
||||||
reselect-tree "^1.3.1"
|
reselect-tree "^1.3.1"
|
||||||
truffle-code-utils "^1.2.4"
|
truffle-code-utils "^1.2.4"
|
||||||
truffle-decode-utils "^1.0.14"
|
truffle-decode-utils "^1.0.14"
|
||||||
truffle-decoder "^3.0.5"
|
truffle-decoder "^3.0.6"
|
||||||
truffle-expect "^0.0.9"
|
truffle-expect "^0.0.9"
|
||||||
truffle-solidity-utils "^1.2.3"
|
truffle-solidity-utils "^1.2.3"
|
||||||
web3 "1.0.0-beta.37"
|
web3 "1.0.0-beta.37"
|
||||||
|
@ -15990,10 +16217,10 @@ truffle-decode-utils@^1.0.14:
|
||||||
web3 "1.0.0-beta.37"
|
web3 "1.0.0-beta.37"
|
||||||
web3-eth-abi "1.0.0-beta.52"
|
web3-eth-abi "1.0.0-beta.52"
|
||||||
|
|
||||||
truffle-decoder@^3.0.5:
|
truffle-decoder@^3.0.6:
|
||||||
version "3.0.5"
|
version "3.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-decoder/-/truffle-decoder-3.0.5.tgz#78d106eb50a3f032da963e0059a802bebafe618a"
|
resolved "https://registry.yarnpkg.com/truffle-decoder/-/truffle-decoder-3.0.6.tgz#81f2a79b3e9b7d8f481aa624a848fedfa465548f"
|
||||||
integrity sha512-lY1ls6Hht9NK/UAzNYh9bfx5fDPBe7DvJGTwYm4TehwpaCsz67xTMrYN/Fr+6qFSxXXQ0lSW3X0VRQcbD5CDlA==
|
integrity sha512-+oTTnymkCs/CClbk7xZCE2Im/2a26p0NRLFIhGghU3W25KxS6h810eeL+PHgtkNTCnbRbd/KNJaVS68j8u1IJg==
|
||||||
dependencies:
|
dependencies:
|
||||||
abi-decoder "^1.2.0"
|
abi-decoder "^1.2.0"
|
||||||
async-eventemitter "^0.2.4"
|
async-eventemitter "^0.2.4"
|
||||||
|
@ -16003,15 +16230,16 @@ truffle-decoder@^3.0.5:
|
||||||
lodash.isequal "^4.5.0"
|
lodash.isequal "^4.5.0"
|
||||||
lodash.merge "^4.6.1"
|
lodash.merge "^4.6.1"
|
||||||
truffle-decode-utils "^1.0.14"
|
truffle-decode-utils "^1.0.14"
|
||||||
|
utf8 "^3.0.0"
|
||||||
web3 "1.0.0-beta.37"
|
web3 "1.0.0-beta.37"
|
||||||
|
|
||||||
truffle-deployer@^3.0.22:
|
truffle-deployer@^3.0.24:
|
||||||
version "3.0.22"
|
version "3.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-deployer/-/truffle-deployer-3.0.22.tgz#6b779241208a6a8180041e9c0b0e6cc408803e17"
|
resolved "https://registry.yarnpkg.com/truffle-deployer/-/truffle-deployer-3.0.24.tgz#7c8fb6110018babee86911093d5949d45bf51704"
|
||||||
integrity sha512-5XmwBU/53xnVzOMAS3gjhdQAxRKRcy/rwUiqg0Fr5YMncqKj/gRfQvaJxav8k1GWPz7Cxjh7Yc7dC4HPd9sZjA==
|
integrity sha512-1CoUsG0J8eVGS+aG6+c/r4Bf9YV4G+jVcKINDA438mJMNoDW7QgKCRU4qRGki1v4KlfnlhRVp0JfRuRVQvqQCQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
emittery "^0.4.0"
|
emittery "^0.4.0"
|
||||||
truffle-contract "^4.0.21"
|
truffle-contract "^4.0.23"
|
||||||
truffle-expect "^0.0.9"
|
truffle-expect "^0.0.9"
|
||||||
|
|
||||||
truffle-error@^0.0.3:
|
truffle-error@^0.0.3:
|
||||||
|
@ -16070,16 +16298,16 @@ truffle-interface-adapter@^0.1.6:
|
||||||
bn.js "^4.11.8"
|
bn.js "^4.11.8"
|
||||||
web3 "1.0.0-beta.37"
|
web3 "1.0.0-beta.37"
|
||||||
|
|
||||||
truffle-migrate@^3.0.22:
|
truffle-migrate@^3.0.24:
|
||||||
version "3.0.22"
|
version "3.0.24"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-migrate/-/truffle-migrate-3.0.22.tgz#6f66f9b79c57d69f09cf17ee39bedb1506479c1e"
|
resolved "https://registry.yarnpkg.com/truffle-migrate/-/truffle-migrate-3.0.24.tgz#205e16aea91eca597e84d2f952c1dbda6367c685"
|
||||||
integrity sha512-saTphMwl8oG95N3HqYettzkh7jjLfBR/Z40Pp087OL84YbqGpLxGXzfW6vF37IRs0UZfE0Ip0Py5VFIXRdgvmw==
|
integrity sha512-0NekMBRC058Lk6PpJx7kCaBGVgOgM997tThRTL5brBb/oQg6wDOgPucChQEsKol07eqZ6BAfkXVyL9RkkFW+ow==
|
||||||
dependencies:
|
dependencies:
|
||||||
async "2.6.1"
|
async "2.6.1"
|
||||||
emittery "^0.4.0"
|
emittery "^0.4.0"
|
||||||
node-dir "0.1.17"
|
node-dir "0.1.17"
|
||||||
truffle-config "^1.1.13"
|
truffle-config "^1.1.13"
|
||||||
truffle-deployer "^3.0.22"
|
truffle-deployer "^3.0.24"
|
||||||
truffle-expect "^0.0.9"
|
truffle-expect "^0.0.9"
|
||||||
truffle-interface-adapter "^0.1.6"
|
truffle-interface-adapter "^0.1.6"
|
||||||
truffle-reporters "^1.0.10"
|
truffle-reporters "^1.0.10"
|
||||||
|
@ -16131,41 +16359,41 @@ truffle-resolver@^5.0.14:
|
||||||
truffle-expect "^0.0.9"
|
truffle-expect "^0.0.9"
|
||||||
truffle-provisioner "^0.1.5"
|
truffle-provisioner "^0.1.5"
|
||||||
|
|
||||||
truffle-solidity-loader@0.1.23:
|
truffle-solidity-loader@0.1.25:
|
||||||
version "0.1.23"
|
version "0.1.25"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-solidity-loader/-/truffle-solidity-loader-0.1.23.tgz#b94ced7b412f8760468fc5e8ea6a6c6c86628a2c"
|
resolved "https://registry.yarnpkg.com/truffle-solidity-loader/-/truffle-solidity-loader-0.1.25.tgz#458c5219e0ba5099a9a99f85a73607e7557ad6f3"
|
||||||
integrity sha512-AT83a1pqu8XSVa9NttTOVjDqCuwGFEVEUSjKKMxeKSorlK8tZqlipAe5GHs2lTZ1ffnRgGBFgPUrfiSr9tQp+w==
|
integrity sha512-5TgUk5w7fo+MKGDFyFmIt1icQxCXbqwRk0WeYDB5Xizr/PUbVW37scTfdv38cSgI2FJ7eZyvLsKE6zAa78hFCw==
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk "^1.1.3"
|
chalk "^1.1.3"
|
||||||
find-up "^1.1.2"
|
find-up "^1.1.2"
|
||||||
loader-utils "^1.1.0"
|
loader-utils "^1.1.0"
|
||||||
schema-utils "^1.0.0"
|
schema-utils "^1.0.0"
|
||||||
truffle-config "^1.1.13"
|
truffle-config "^1.1.13"
|
||||||
truffle-core "^5.0.24"
|
truffle-core "^5.0.26"
|
||||||
|
|
||||||
truffle-solidity-utils@^1.2.3:
|
truffle-solidity-utils@^1.2.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-solidity-utils/-/truffle-solidity-utils-1.2.3.tgz#9e83c80fe5eeac1b9587f227af57e3feee5e183c"
|
resolved "https://registry.yarnpkg.com/truffle-solidity-utils/-/truffle-solidity-utils-1.2.3.tgz#9e83c80fe5eeac1b9587f227af57e3feee5e183c"
|
||||||
integrity sha512-Rf9KLx8BFTX6/1jxKuzWC5AegSMTN9uxLIKWP38oBAxHq/ilD64W+W5eyEqBxAXUYlAABj9jpOg4Pn5NRYtxOg==
|
integrity sha512-Rf9KLx8BFTX6/1jxKuzWC5AegSMTN9uxLIKWP38oBAxHq/ilD64W+W5eyEqBxAXUYlAABj9jpOg4Pn5NRYtxOg==
|
||||||
|
|
||||||
truffle-workflow-compile@^2.0.20:
|
truffle-workflow-compile@^2.0.22:
|
||||||
version "2.0.20"
|
version "2.0.22"
|
||||||
resolved "https://registry.yarnpkg.com/truffle-workflow-compile/-/truffle-workflow-compile-2.0.20.tgz#2faa3c3e2d0873beebce2c5fc265027e36c12fd8"
|
resolved "https://registry.yarnpkg.com/truffle-workflow-compile/-/truffle-workflow-compile-2.0.22.tgz#9ff49d5ddb0547bfed0ca2771923d18a07792210"
|
||||||
integrity sha512-XyUW5DTmnsCF9149/0kgUkRPe5gBzCb7W8FfjqXsUuGMKbSqucb+yMOU7qidSsl7NGiDBekWoR4G5Li6Z5k21g==
|
integrity sha512-RVcwWj61SwrAZFwoKMGm1lwWtJgNX6Yor+m5mxeVwJVoBH1d++mKUYTtf8eeujJQSfNHCo6OJe5RKtzrePOVfg==
|
||||||
dependencies:
|
dependencies:
|
||||||
mkdirp "^0.5.1"
|
mkdirp "^0.5.1"
|
||||||
truffle-artifactor "^4.0.20"
|
truffle-artifactor "^4.0.22"
|
||||||
truffle-compile "^4.1.1"
|
truffle-compile "^4.1.3"
|
||||||
truffle-compile-vyper "^1.0.18"
|
truffle-compile-vyper "^1.0.20"
|
||||||
truffle-config "^1.1.13"
|
truffle-config "^1.1.13"
|
||||||
truffle-expect "^0.0.9"
|
truffle-expect "^0.0.9"
|
||||||
truffle-external-compile "^1.0.11"
|
truffle-external-compile "^1.0.11"
|
||||||
truffle-resolver "^5.0.14"
|
truffle-resolver "^5.0.14"
|
||||||
|
|
||||||
truffle@5.0.24:
|
truffle@5.0.26:
|
||||||
version "5.0.24"
|
version "5.0.26"
|
||||||
resolved "https://registry.yarnpkg.com/truffle/-/truffle-5.0.24.tgz#5cbba0bb280a2907529e9b71b9444f064c649262"
|
resolved "https://registry.yarnpkg.com/truffle/-/truffle-5.0.26.tgz#cb9a6dcd77501821ba6e8ba51b2d4eef58da7458"
|
||||||
integrity sha512-a7sAi7S3s82+qQBstGJzEUE09QjJLRlY00ITrK4N4Qfzgje3thUA2nIGl2uExdD07w6bUZBOLWW9eoCVwps1Eg==
|
integrity sha512-gf3Khot59ZM2Tegqb2md0lpr1SXGUHKJkSajCyXsiFCiiHUbX6NpXn/mgJgk1SXt7FAHpL0zYdLI1pYfqzvBFw==
|
||||||
dependencies:
|
dependencies:
|
||||||
app-module-path "^2.2.0"
|
app-module-path "^2.2.0"
|
||||||
mocha "5.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-list-map "^2.0.0"
|
||||||
source-map "~0.6.1"
|
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"
|
version "4.35.0"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.0.tgz#ad3f0f8190876328806ccb7a36f3ce6e764b8378"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.0.tgz#ad3f0f8190876328806ccb7a36f3ce6e764b8378"
|
||||||
integrity sha512-M5hL3qpVvtr8d4YaJANbAQBc4uT01G33eDpl/psRTBCfjxFTihdhin1NtAKB1ruDwzeVdcsHHV3NX+QsAgOosw==
|
integrity sha512-M5hL3qpVvtr8d4YaJANbAQBc4uT01G33eDpl/psRTBCfjxFTihdhin1NtAKB1ruDwzeVdcsHHV3NX+QsAgOosw==
|
||||||
|
|
Loading…
Reference in New Issue