Merge pull request #120 from gnosis/77-change-threshold
#77 Safe settings: Change threshold
This commit is contained in:
commit
277ae8f311
29
package.json
29
package.json
|
@ -32,13 +32,13 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gnosis.pm/safe-contracts": "^1.0.0",
|
"@gnosis.pm/safe-contracts": "^1.0.0",
|
||||||
"@gnosis.pm/util-contracts": "2.0.1",
|
"@gnosis.pm/util-contracts": "2.0.1",
|
||||||
"@material-ui/core": "4.1.0",
|
"@material-ui/core": "4.1.1",
|
||||||
"@material-ui/icons": "4.1.0",
|
"@material-ui/icons": "4.2.0",
|
||||||
"@welldone-software/why-did-you-render": "^3.0.9",
|
"@welldone-software/why-did-you-render": "3.2.1",
|
||||||
"axios": "0.19.0",
|
"axios": "0.19.0",
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.0",
|
||||||
"connected-react-router": "^6.3.1",
|
"connected-react-router": "^6.3.1",
|
||||||
"final-form": "4.14.1",
|
"final-form": "4.15.0",
|
||||||
"history": "^4.7.2",
|
"history": "^4.7.2",
|
||||||
"immortal-db": "^1.0.2",
|
"immortal-db": "^1.0.2",
|
||||||
"immutable": "^4.0.0-rc.9",
|
"immutable": "^4.0.0-rc.9",
|
||||||
|
@ -47,9 +47,9 @@
|
||||||
"qrcode.react": "^0.9.3",
|
"qrcode.react": "^0.9.3",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-final-form": "6.1.0",
|
"react-final-form": "6.2.1",
|
||||||
"react-final-form-listeners": "^1.0.2",
|
"react-final-form-listeners": "^1.0.2",
|
||||||
"react-hot-loader": "4.11.0",
|
"react-hot-loader": "4.11.1",
|
||||||
"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",
|
||||||
|
@ -86,14 +86,13 @@
|
||||||
"@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",
|
||||||
"@storybook/addon-actions": "5.1.4",
|
"@storybook/addon-actions": "5.1.8",
|
||||||
"@storybook/addon-knobs": "5.1.4",
|
"@storybook/addon-knobs": "5.1.8",
|
||||||
"@storybook/addon-links": "5.1.4",
|
"@storybook/addon-links": "5.1.8",
|
||||||
"@storybook/react": "5.1.4",
|
"@storybook/react": "5.1.8",
|
||||||
"@testing-library/react": "^8.0.1",
|
|
||||||
"autoprefixer": "9.6.0",
|
"autoprefixer": "9.6.0",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "10.0.2",
|
||||||
"babel-jest": "24.8.0",
|
"babel-jest": "24.8.0",
|
||||||
"babel-loader": "8.0.6",
|
"babel-loader": "8.0.6",
|
||||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||||
|
@ -104,7 +103,7 @@
|
||||||
"detect-port": "^1.2.2",
|
"detect-port": "^1.2.2",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
"eslint-config-airbnb": "^17.1.0",
|
||||||
"eslint-plugin-flowtype": "3.10.2",
|
"eslint-plugin-flowtype": "3.10.3",
|
||||||
"eslint-plugin-import": "2.17.3",
|
"eslint-plugin-import": "2.17.3",
|
||||||
"eslint-plugin-jest": "22.6.4",
|
"eslint-plugin-jest": "22.6.4",
|
||||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||||
|
@ -124,7 +123,7 @@
|
||||||
"postcss-mixins": "^6.2.0",
|
"postcss-mixins": "^6.2.0",
|
||||||
"postcss-simple-vars": "^5.0.2",
|
"postcss-simple-vars": "^5.0.2",
|
||||||
"pre-commit": "^1.2.2",
|
"pre-commit": "^1.2.2",
|
||||||
"prettier-eslint-cli": "^4.7.1",
|
"prettier-eslint-cli": "5.0.0",
|
||||||
"run-with-testrpc": "0.3.1",
|
"run-with-testrpc": "0.3.1",
|
||||||
"storybook-host": "^5.0.3",
|
"storybook-host": "^5.0.3",
|
||||||
"storybook-router": "^0.3.3",
|
"storybook-router": "^0.3.3",
|
||||||
|
@ -136,7 +135,7 @@
|
||||||
"webpack": "4.34.0",
|
"webpack": "4.34.0",
|
||||||
"webpack-bundle-analyzer": "3.3.2",
|
"webpack-bundle-analyzer": "3.3.2",
|
||||||
"webpack-cli": "3.3.4",
|
"webpack-cli": "3.3.4",
|
||||||
"webpack-dev-server": "3.7.1",
|
"webpack-dev-server": "3.7.2",
|
||||||
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,10 @@ const SelectInput = ({
|
||||||
formControlProps,
|
formControlProps,
|
||||||
classes,
|
classes,
|
||||||
renderValue,
|
renderValue,
|
||||||
|
disableError,
|
||||||
...rest
|
...rest
|
||||||
}: SelectFieldProps) => {
|
}: SelectFieldProps) => {
|
||||||
const showError = ((meta.submitError && !meta.dirtySinceLastSubmit) || meta.error) && meta.touched
|
const showError = ((meta.submitError && !meta.dirtySinceLastSubmit) || meta.error) && meta.touched && !disableError
|
||||||
const inputProps = {
|
const inputProps = {
|
||||||
...restInput,
|
...restInput,
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -70,4 +70,12 @@ export const inLimit = (limit: number, base: number, baseText: string, symbol: s
|
||||||
return `Should not exceed ${max} ${symbol} (amount to reach ${baseText})`
|
return `Should not exceed ${max} ${symbol} (amount to reach ${baseText})`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const differentFrom = (diffValue: string) => (value: string) => {
|
||||||
|
if (value === diffValue.toString()) {
|
||||||
|
return `Value should be different than ${value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export const noErrorsOn = (name: string, errors: Object) => errors[name] === undefined
|
export const noErrorsOn = (name: string, errors: Object) => errors[name] === undefined
|
||||||
|
|
|
@ -10,7 +10,7 @@ type Props = {
|
||||||
type: 'button' | 'submit' | 'reset',
|
type: 'button' | 'submit' | 'reset',
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
|
||||||
weight?: 'light' | 'regular' | 'bolder' | 'bold',
|
weight?: 'light' | 'regular' | 'bolder' | 'bold',
|
||||||
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled',
|
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled' | 'error',
|
||||||
testId?: string,
|
testId?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,10 @@
|
||||||
color: $disabled;
|
color: $disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $error;
|
||||||
|
}
|
||||||
|
|
||||||
.white {
|
.white {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
|
@ -11,7 +11,7 @@ type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4';
|
||||||
type Props = {
|
type Props = {
|
||||||
align?: 'left' | 'center' | 'right',
|
align?: 'left' | 'center' | 'right',
|
||||||
margin?: 'sm' | 'md' | 'lg' | 'xl',
|
margin?: 'sm' | 'md' | 'lg' | 'xl',
|
||||||
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled',
|
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled' | 'error',
|
||||||
tag: HeadingTag,
|
tag: HeadingTag,
|
||||||
truncate?: boolean,
|
truncate?: boolean,
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
|
|
|
@ -95,6 +95,10 @@
|
||||||
color: $disabled;
|
color: $disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $error;
|
||||||
|
}
|
||||||
|
|
||||||
.white {
|
.white {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ type Props = {
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
color?: 'regular' | 'white',
|
color?: 'regular' | 'white',
|
||||||
className?: string,
|
className?: string,
|
||||||
innerRef: React.ElementRef<any>,
|
innerRef?: React.ElementRef<any>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const GnosisLink = ({
|
const GnosisLink = ({
|
||||||
|
|
|
@ -11,7 +11,7 @@ type Props = {
|
||||||
noPadding?: boolean,
|
noPadding?: boolean,
|
||||||
weight?: 'light' | 'regular' | 'bolder' | 'bold',
|
weight?: 'light' | 'regular' | 'bolder' | 'bold',
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
|
||||||
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled',
|
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled' | 'error',
|
||||||
transform?: 'capitalize' | 'lowercase' | 'uppercase',
|
transform?: 'capitalize' | 'lowercase' | 'uppercase',
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
dot?: boolean,
|
dot?: boolean,
|
||||||
|
|
|
@ -35,6 +35,10 @@
|
||||||
color: $disabled;
|
color: $disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $error;
|
||||||
|
}
|
||||||
|
|
||||||
.white {
|
.white {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import React from 'react'
|
import * as React from 'react'
|
||||||
import { capitalize } from '~/utils/css'
|
import { capitalize } from '~/utils/css'
|
||||||
import styles from './index.scss'
|
import styles from './index.scss'
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
safeInstance: any,
|
safeInstance: any,
|
||||||
to: string,
|
to: string,
|
||||||
valueInWei: number,
|
valueInWei: number | string,
|
||||||
data: string,
|
data: string,
|
||||||
operation: number | string,
|
operation: number | string,
|
||||||
nonce: string | number,
|
nonce: string | number,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { loadFromStorage, saveToStorage } from '~/utils/storage'
|
import { loadFromStorage, saveToStorage, removeFromStorage } from '~/utils/storage'
|
||||||
|
|
||||||
export const SAFES_KEY = 'SAFES'
|
export const SAFES_KEY = 'SAFES'
|
||||||
export const TX_KEY = 'TX'
|
export const TX_KEY = 'TX'
|
||||||
|
@ -41,3 +41,12 @@ 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> => {
|
||||||
|
try {
|
||||||
|
await removeFromStorage(`${OWNERS_KEY}-${safeAddress}`)
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error removing owners from localstorage')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,9 +8,6 @@ type AddTokenProps = {
|
||||||
token: Token,
|
token: Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addToken = createAction<string, *, *>(
|
export const addToken = createAction<string, *, *>(ADD_TOKEN, (token: Token): AddTokenProps => ({
|
||||||
ADD_TOKEN,
|
|
||||||
(token: Token): AddTokenProps => ({
|
|
||||||
token,
|
token,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
|
@ -9,11 +9,8 @@ type TokenProps = {
|
||||||
tokens: Map<string, Token>,
|
tokens: Map<string, Token>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const addTokens = createAction<string, *, *>(
|
const addTokens = createAction<string, *, *>(ADD_TOKENS, (tokens: Map<string, Token>): TokenProps => ({
|
||||||
ADD_TOKENS,
|
|
||||||
(tokens: Map<string, Token>): TokenProps => ({
|
|
||||||
tokens,
|
tokens,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
export default addTokens
|
export default addTokens
|
||||||
|
|
|
@ -17,13 +17,8 @@ export const getNamesFrom = (values: Object): string[] => {
|
||||||
return accounts.map(account => values[account]).slice(0, values.owners)
|
return accounts.map(account => values[account]).slice(0, values.owners)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOwnersFrom = (
|
export const getOwnersFrom = (names: string[], addresses: string[]): Array<string, string> => {
|
||||||
names: string[],
|
const owners = names.map((name: string, index: number) => makeOwner({ name, address: addresses[index] }))
|
||||||
addresses: string[],
|
|
||||||
): Array<string, string> => {
|
|
||||||
const owners = names.map((name: string, index: number) => makeOwner(
|
|
||||||
{ name, address: addresses[index] },
|
|
||||||
))
|
|
||||||
|
|
||||||
return owners
|
return owners
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,12 @@ import { copyToClipboard } from '~/utils/clipboard'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||||
|
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import ArrowDown from '../assets/arrow-down.svg'
|
import ArrowDown from '../assets/arrow-down.svg'
|
||||||
import { secondary } from '~/theme/variables'
|
import { secondary } from '~/theme/variables'
|
||||||
|
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -50,7 +54,31 @@ const ReviewTx = ({
|
||||||
createTransaction,
|
createTransaction,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<SharedSnackbarConsumer>
|
<SharedSnackbarConsumer>
|
||||||
{({ openSnackbar }) => (
|
{({ openSnackbar }) => {
|
||||||
|
const submitTx = async () => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const isSendingETH = isEther(tx.token.symbol)
|
||||||
|
const txRecipient = isSendingETH ? tx.recipientAddress : tx.token.address
|
||||||
|
let txData = EMPTY_DATA
|
||||||
|
let txAmount = web3.utils.toWei(tx.amount, 'ether')
|
||||||
|
|
||||||
|
|
||||||
|
if (!isSendingETH) {
|
||||||
|
const StandardToken = await getStandardTokenContract()
|
||||||
|
const tokenInstance = await StandardToken.at(tx.token.address)
|
||||||
|
|
||||||
|
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI()
|
||||||
|
// txAmount should be 0 if we send tokens
|
||||||
|
// the real value is encoded in txData and will be used by the contract
|
||||||
|
// if txAmount > 0 it would send ETH from the safe
|
||||||
|
txAmount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
createTransaction(safeAddress, txRecipient, txAmount, txData, openSnackbar)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Row align="center" grow className={classes.heading}>
|
<Row align="center" grow className={classes.heading}>
|
||||||
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||||
|
@ -117,10 +145,7 @@ const ReviewTx = ({
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
onClick={() => {
|
onClick={submitTx}
|
||||||
createTransaction(safeAddress, tx.recipientAddress, tx.amount, tx.token, openSnackbar)
|
|
||||||
onClose()
|
|
||||||
}}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -130,7 +155,8 @@ const ReviewTx = ({
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)
|
||||||
|
}}
|
||||||
</SharedSnackbarConsumer>
|
</SharedSnackbarConsumer>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
import { copyToClipboard } from '~/utils/clipboard'
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
import Balances from './Balances'
|
import Balances from './Balances'
|
||||||
|
import Settings from './Settings'
|
||||||
|
|
||||||
type Props = SelectorProps & {
|
type Props = SelectorProps & {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
|
@ -141,6 +142,17 @@ class Layout extends React.Component<Props, State> {
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tabIndex === 2 && (
|
||||||
|
<Settings
|
||||||
|
granted={granted}
|
||||||
|
safeAddress={address}
|
||||||
|
safeName={name}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
threshold={safe.threshold}
|
||||||
|
owners={safe.owners}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
// @flow
|
||||||
|
import removeSafe from '~/routes/safe/store/actions/removeSafe'
|
||||||
|
|
||||||
|
export type Actions = {
|
||||||
|
removeSafe: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
removeSafe,
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { history } from '~/store'
|
||||||
|
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Modal from '~/components/Modal'
|
||||||
|
import Identicon from '~/components/Identicon'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Link from '~/components/layout/Link'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import actions, { type Actions } from './actions'
|
||||||
|
import { lg, md, secondary } from '~/theme/variables'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
const openIconStyle = {
|
||||||
|
height: '16px',
|
||||||
|
color: secondary,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = Actions & {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
isOpen: boolean,
|
||||||
|
safeAddress: string,
|
||||||
|
etherScanLink: string,
|
||||||
|
safeName: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const RemoveSafeComponent = ({
|
||||||
|
onClose,
|
||||||
|
isOpen,
|
||||||
|
classes,
|
||||||
|
safeAddress,
|
||||||
|
etherScanLink,
|
||||||
|
safeName,
|
||||||
|
removeSafe,
|
||||||
|
}: Props) => (
|
||||||
|
<Modal
|
||||||
|
title="Remove Safe"
|
||||||
|
description="Remove the selected Safe"
|
||||||
|
handleClose={onClose}
|
||||||
|
open={isOpen}
|
||||||
|
>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph className={classes.manage} noMargin>
|
||||||
|
Remove Safe
|
||||||
|
</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.close} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.container}>
|
||||||
|
<Row className={classes.owner}>
|
||||||
|
<Col xs={1} align="center">
|
||||||
|
<Identicon address={safeAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11}>
|
||||||
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
|
<Paragraph size="lg" noMargin>
|
||||||
|
{safeName}
|
||||||
|
</Paragraph>
|
||||||
|
<Block align="center" className={classes.user}>
|
||||||
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
|
{safeAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<Link className={classes.open} to={etherScanLink} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row className={classes.description}>
|
||||||
|
<Paragraph noMargin>
|
||||||
|
Removing a Safe only removes it from your interface.
|
||||||
|
<b>It does not delete the Safe</b>.
|
||||||
|
You can always add it back using the Safe's address.
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.buttonRemove}
|
||||||
|
onClick={() => {
|
||||||
|
removeSafe(safeAddress)
|
||||||
|
onClose()
|
||||||
|
history.push(SAFELIST_ADDRESS)
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
|
||||||
|
const RemoveSafeModal = withStyles(styles)(RemoveSafeComponent)
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
undefined,
|
||||||
|
actions,
|
||||||
|
)(RemoveSafeModal)
|
|
@ -0,0 +1,56 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
lg, md, sm, error, background,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = (theme: Object) => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
maxHeight: '75px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
minHeight: '369px',
|
||||||
|
},
|
||||||
|
manage: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
buttonRemove: {
|
||||||
|
color: '#fff',
|
||||||
|
backgroundColor: error,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
userName: {
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
backgroundColor: background,
|
||||||
|
padding: md,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
justifyContent: 'left',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
padding: md,
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
paddingLeft: sm,
|
||||||
|
width: 'auto',
|
||||||
|
'&:hover': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,120 @@
|
||||||
|
// @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 SelectField from '~/components/forms/SelectField'
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem'
|
||||||
|
import {
|
||||||
|
composeValidators, minValue, mustBeInteger, required, differentFrom,
|
||||||
|
} from '~/components/forms/validator'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
import GnoForm from '~/components/forms/GnoForm'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import type { Owner } from '~/routes/safe/store/models/owner'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
threshold: number,
|
||||||
|
owners: List<Owner>,
|
||||||
|
onChangeThreshold: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const THRESHOLD_FIELD_NAME = 'threshold'
|
||||||
|
|
||||||
|
const ChangeThreshold = ({
|
||||||
|
onClose, owners, threshold, classes, onChangeThreshold,
|
||||||
|
}: Props) => {
|
||||||
|
const handleSubmit = async (values) => {
|
||||||
|
const newThreshold = values[THRESHOLD_FIELD_NAME]
|
||||||
|
|
||||||
|
await onChangeThreshold(newThreshold)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph className={classes.headingText} weight="bolder" noMargin>
|
||||||
|
Change required confirmations
|
||||||
|
</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.close} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit} initialValues={{ threshold: threshold.toString() }}>
|
||||||
|
{() => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block className={classes.modalContent}>
|
||||||
|
<Row>
|
||||||
|
<Paragraph>
|
||||||
|
Every transaction outside any specified daily limits, needs to be confirmed by all specified owners.
|
||||||
|
If no daily limits are set, all owners will need to sign for transactions.
|
||||||
|
</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_FIELD_NAME}
|
||||||
|
render={props => (
|
||||||
|
<>
|
||||||
|
<SelectField {...props} disableError>
|
||||||
|
{[...Array(Number(owners.size))].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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
validate={composeValidators(required, mustBeInteger, minValue(1), differentFrom(threshold))}
|
||||||
|
data-testid="threshold-select-input"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={10}>
|
||||||
|
<Paragraph size="lg" color="primary" noMargin className={classes.ownersText}>
|
||||||
|
out of
|
||||||
|
{' '}
|
||||||
|
{owners.size}
|
||||||
|
{' '}
|
||||||
|
owner(s)
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
||||||
|
BACK
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" color="primary" className={classes.button} minWidth={140} variant="contained">
|
||||||
|
CHANGE
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</GnoForm>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ChangeThreshold)
|
|
@ -0,0 +1,44 @@
|
||||||
|
// @flow
|
||||||
|
import { lg, md, sm } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
letterSpacing: '-1px',
|
||||||
|
color: '#a2a8ba',
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
headingText: {
|
||||||
|
fontSize: '20px',
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
ownersText: {
|
||||||
|
marginLeft: sm,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
inputRow: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-25px',
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,101 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
|
||||||
|
import Heading from '~/components/layout/Heading'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Modal from '~/components/Modal'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import ChangeThreshold from './ChangeThreshold'
|
||||||
|
import type { Owner } from '~/routes/safe/store/models/owner'
|
||||||
|
import { styles } from './style'
|
||||||
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
|
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
owners: List<Owner>,
|
||||||
|
threshold: number,
|
||||||
|
classes: Object,
|
||||||
|
createTransaction: Function,
|
||||||
|
safeAddress: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThresholdSettings = ({
|
||||||
|
owners, threshold, classes, createTransaction, safeAddress,
|
||||||
|
}: Props) => {
|
||||||
|
const [isModalOpen, setModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const toggleModal = () => {
|
||||||
|
setModalOpen(prevOpen => !prevOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<SharedSnackbarConsumer>
|
||||||
|
{({ openSnackbar }) => {
|
||||||
|
const onChangeThreshold = async (newThreshold) => {
|
||||||
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI()
|
||||||
|
|
||||||
|
createTransaction(safeAddress, safeAddress, 0, txData, openSnackbar)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Block className={classes.container}>
|
||||||
|
<Heading tag="h3">Required confirmations</Heading>
|
||||||
|
<Paragraph>
|
||||||
|
Any transaction over any daily limit
|
||||||
|
<br />
|
||||||
|
{' '}
|
||||||
|
requires the confirmation of:
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="xxl" className={classes.ownersText}>
|
||||||
|
<Bold>{threshold}</Bold>
|
||||||
|
{' '}
|
||||||
|
out of
|
||||||
|
{' '}
|
||||||
|
<Bold>{owners.size}</Bold>
|
||||||
|
{' '}
|
||||||
|
owners
|
||||||
|
</Paragraph>
|
||||||
|
{owners.size > 1 && (
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
minWidth={120}
|
||||||
|
className={classes.modifyBtn}
|
||||||
|
onClick={toggleModal}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Modify
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Block>
|
||||||
|
<Modal
|
||||||
|
title="Change Required Confirmations"
|
||||||
|
description="Change Required Confirmations Form"
|
||||||
|
handleClose={toggleModal}
|
||||||
|
open={isModalOpen}
|
||||||
|
>
|
||||||
|
<ChangeThreshold
|
||||||
|
onClose={toggleModal}
|
||||||
|
owners={owners}
|
||||||
|
threshold={threshold}
|
||||||
|
onChangeThreshold={onChangeThreshold}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</SharedSnackbarConsumer>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ThresholdSettings)
|
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
fontColor, lg, smallFontSize, md,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
ownersText: {
|
||||||
|
fontSize: '26px',
|
||||||
|
color: '#8896b6',
|
||||||
|
'& b': {
|
||||||
|
color: fontColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
padding: lg,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '51px',
|
||||||
|
left: 0,
|
||||||
|
height: '51px',
|
||||||
|
width: '100%',
|
||||||
|
paddingRight: md,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
borderTop: 'solid 1px #e4e8f1',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
modifyBtn: {
|
||||||
|
height: '32px',
|
||||||
|
fontSize: smallFontSize,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,56 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import { sm, boldFont } from '~/theme/variables'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
const controlsStyle = {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
padding: sm,
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveButtonStyle = {
|
||||||
|
marginRight: sm,
|
||||||
|
fontWeight: boldFont,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateSafeName extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Block margin="lg">
|
||||||
|
<Paragraph size="lg" color="primary" noMargin>
|
||||||
|
Details
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Row style={controlsStyle} align="end" grow>
|
||||||
|
<Col end="xs">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
style={saveButtonStyle}
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
SAVE
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(UpdateSafeName)
|
|
@ -0,0 +1,4 @@
|
||||||
|
// @flow
|
||||||
|
import { lg, border } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({})
|
|
@ -0,0 +1,135 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import RemoveSafeModal from './RemoveSafeModal'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import type { Owner } from '~/routes/safe/store/models/owner'
|
||||||
|
import ThresholdSettings from './ThresholdSettings'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
showRemoveSafe: boolean,
|
||||||
|
menuOptionIndex: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: Object,
|
||||||
|
granted: boolean,
|
||||||
|
etherScanLink: string,
|
||||||
|
safeAddress: string,
|
||||||
|
safeName: string,
|
||||||
|
owners: List<Owner>,
|
||||||
|
threshold: number,
|
||||||
|
createTransaction: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = 'RemoveSafe'
|
||||||
|
|
||||||
|
class Settings extends React.Component<Props, State> {
|
||||||
|
state = {
|
||||||
|
showRemoveSafe: false,
|
||||||
|
menuOptionIndex: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = menuOptionIndex => () => {
|
||||||
|
this.setState({ menuOptionIndex })
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow = (action: Action) => () => {
|
||||||
|
this.setState(() => ({ [`show${action}`]: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide = (action: Action) => () => {
|
||||||
|
this.setState(() => ({ [`show${action}`]: false }))
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { showRemoveSafe, menuOptionIndex } = this.state
|
||||||
|
const {
|
||||||
|
classes, granted, etherScanLink, safeAddress, safeName, owners, threshold, createTransaction,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" className={classes.message}>
|
||||||
|
<Col xs={6}>
|
||||||
|
<Paragraph className={classes.settings}>Settings</Paragraph>
|
||||||
|
</Col>
|
||||||
|
<Col xs={6} end="sm">
|
||||||
|
<Paragraph noMargin size="md" color="error" className={classes.links} onClick={this.onShow('RemoveSafe')}>
|
||||||
|
Remove Safe
|
||||||
|
</Paragraph>
|
||||||
|
<RemoveSafeModal
|
||||||
|
onClose={this.onHide('RemoveSafe')}
|
||||||
|
isOpen={showRemoveSafe}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
safeName={safeName}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Block className={classes.root}>
|
||||||
|
<Col xs={3} layout="column">
|
||||||
|
<Block className={classes.menu}>
|
||||||
|
<Row
|
||||||
|
className={cn(classes.menuOption, menuOptionIndex === 1 && classes.active)}
|
||||||
|
onClick={this.handleChange(1)}
|
||||||
|
>
|
||||||
|
Safe name
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
{granted && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row
|
||||||
|
className={cn(classes.menuOption, menuOptionIndex === 2 && classes.active)}
|
||||||
|
onClick={this.handleChange(2)}
|
||||||
|
>
|
||||||
|
Owners
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row
|
||||||
|
className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)}
|
||||||
|
onClick={this.handleChange(3)}
|
||||||
|
>
|
||||||
|
Required confirmations
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row
|
||||||
|
className={cn(classes.menuOption, menuOptionIndex === 4 && classes.active)}
|
||||||
|
onClick={this.handleChange(4)}
|
||||||
|
>
|
||||||
|
Modules
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
<Col xs={9} layout="column">
|
||||||
|
<Block className={classes.container}>
|
||||||
|
{menuOptionIndex === 1 && <p>To be done</p>}
|
||||||
|
{granted && menuOptionIndex === 2 && <p>To be done</p>}
|
||||||
|
{granted && menuOptionIndex === 3 && (
|
||||||
|
<ThresholdSettings
|
||||||
|
owners={owners}
|
||||||
|
threshold={threshold}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{granted && menuOptionIndex === 4 && <p>To be done</p>}
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Block>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(Settings)
|
|
@ -0,0 +1,42 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
sm, md, lg, border, secondary, bolderFont,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = (theme: Object) => ({
|
||||||
|
root: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
boxShadow: '0 -1px 4px 0 rgba(74, 85, 121, 0.5)',
|
||||||
|
minHeight: '400px',
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
letterSpacing: '-0.5px',
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
borderRight: `solid 1px ${border}`,
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
menuOption: {
|
||||||
|
padding: lg,
|
||||||
|
alignItems: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
backgroundColor: '#f4f4f9',
|
||||||
|
color: secondary,
|
||||||
|
fontWeight: bolderFont,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
margin: `${sm} 0`,
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
'&:hover': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -11,7 +11,7 @@ export type Props = Actions &
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000
|
const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
|
||||||
|
|
||||||
class SafeView extends React.Component<Props> {
|
class SafeView extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
|
@ -18,12 +18,9 @@ type ActionReturn = {
|
||||||
safe: Safe,
|
safe: Safe,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addSafe = createAction<string, Function, ActionReturn>(
|
export const addSafe = createAction<string, Function, ActionReturn>(ADD_SAFE, (safe: Safe): ActionReturn => ({
|
||||||
ADD_SAFE,
|
|
||||||
(safe: Safe): ActionReturn => ({
|
|
||||||
safe,
|
safe,
|
||||||
}),
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
const saveSafe = (name: string, address: string, threshold: number, ownersName: string[], ownersAddress: string[]) => (
|
const saveSafe = (name: string, address: string, threshold: number, ownersName: string[], ownersAddress: string[]) => (
|
||||||
dispatch: ReduxDispatch<GlobalState>,
|
dispatch: ReduxDispatch<GlobalState>,
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||||
import { createAction } from 'redux-actions'
|
import { createAction } from 'redux-actions'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
|
||||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||||
import { type GlobalState } from '~/store'
|
import { type GlobalState } from '~/store'
|
||||||
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
import { executeTransaction, CALL } from '~/logic/safe/transactions'
|
import { executeTransaction, CALL } from '~/logic/safe/transactions'
|
||||||
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
|
||||||
|
|
||||||
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
||||||
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
||||||
|
@ -17,39 +13,22 @@ export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
||||||
const createTransaction = (
|
const createTransaction = (
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
to: string,
|
to: string,
|
||||||
valueInEth: string,
|
valueInWei: string,
|
||||||
token: Token,
|
txData: string = EMPTY_DATA,
|
||||||
openSnackbar: Function,
|
openSnackbar: Function,
|
||||||
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
||||||
const isSendingETH = isEther(token.symbol)
|
|
||||||
const state: GlobalState = getState()
|
const state: GlobalState = getState()
|
||||||
|
|
||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const web3 = getWeb3()
|
|
||||||
const from = userAccountSelector(state)
|
const from = userAccountSelector(state)
|
||||||
const threshold = await safeInstance.getThreshold()
|
const threshold = await safeInstance.getThreshold()
|
||||||
const nonce = await safeInstance.nonce()
|
const nonce = await safeInstance.nonce()
|
||||||
const txRecipient = isSendingETH ? to : token.address
|
|
||||||
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
|
|
||||||
let txAmount = valueInWei
|
|
||||||
const isExecution = threshold.toNumber() === 1
|
const isExecution = threshold.toNumber() === 1
|
||||||
|
|
||||||
let txData = EMPTY_DATA
|
|
||||||
if (!isSendingETH) {
|
|
||||||
const StandardToken = await getStandardTokenContract()
|
|
||||||
const sendToken = await StandardToken.at(token.address)
|
|
||||||
|
|
||||||
txData = sendToken.contract.methods.transfer(to, valueInWei).encodeABI()
|
|
||||||
// txAmount should be 0 if we send tokens
|
|
||||||
// the real value is encoded in txData and will be used by the contract
|
|
||||||
// if txAmount > 0 it would send ETH from the safe
|
|
||||||
txAmount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
let txHash
|
let txHash
|
||||||
if (isExecution) {
|
if (isExecution) {
|
||||||
openSnackbar('Transaction has been submitted', 'success')
|
openSnackbar('Transaction has been submitted', 'success')
|
||||||
txHash = await executeTransaction(safeInstance, txRecipient, txAmount, 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 {
|
||||||
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const REMOVE_SAFE = 'REMOVE_SAFE'
|
||||||
|
|
||||||
|
const removeSafe = createAction<string, *>(REMOVE_SAFE)
|
||||||
|
|
||||||
|
export default removeSafe
|
|
@ -1,9 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
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 type { Store, AnyAction } from 'redux'
|
import type { Store, AnyAction } from 'redux'
|
||||||
import { type GlobalState } from '~/store/'
|
import { type GlobalState } from '~/store/'
|
||||||
import { saveSafes, setOwners } 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'
|
||||||
|
@ -11,7 +12,7 @@ import type { Token } from '~/logic/tokens/store/model/token'
|
||||||
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, ACTIVATE_TOKEN_FOR_ALL_SAFES]
|
const watchedActions = [ADD_SAFE, UPDATE_SAFE, REMOVE_SAFE, 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)
|
||||||
|
@ -40,6 +41,9 @@ const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => asyn
|
||||||
if (action.type === 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) {
|
||||||
|
const safeAddress = action.payload
|
||||||
|
removeOwners(safeAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ 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'
|
||||||
|
|
||||||
export const SAFE_REDUCER_ID = 'safes'
|
export const SAFE_REDUCER_ID = 'safes'
|
||||||
|
|
||||||
|
@ -91,6 +92,11 @@ export default handleActions<State, *>(
|
||||||
|
|
||||||
return state.set(safe.address, SafeRecord(safe))
|
return state.set(safe.address, SafeRecord(safe))
|
||||||
},
|
},
|
||||||
|
[REMOVE_SAFE]: (state: State, action: ActionType<Function>): State => {
|
||||||
|
const safeAddress = action.payload
|
||||||
|
|
||||||
|
return state.delete(safeAddress)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Map(),
|
Map(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
disabled,
|
disabled,
|
||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
|
error,
|
||||||
md,
|
md,
|
||||||
lg,
|
lg,
|
||||||
bolderFont,
|
bolderFont,
|
||||||
|
@ -27,7 +28,7 @@ const palette = {
|
||||||
main: secondary,
|
main: secondary,
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#FB4F62',
|
main: error,
|
||||||
},
|
},
|
||||||
contrastThreshold: 3,
|
contrastThreshold: 3,
|
||||||
tonalOffset: 0.2,
|
tonalOffset: 0.2,
|
||||||
|
|
|
@ -7,6 +7,7 @@ const tertiary = '#f6f9fc'
|
||||||
const fontColor = '#4a5579'
|
const fontColor = '#4a5579'
|
||||||
const fancyColor = '#fd7890'
|
const fancyColor = '#fd7890'
|
||||||
const warningColor = '#ffc05f'
|
const warningColor = '#ffc05f'
|
||||||
|
const errorColor = '#fb4f62'
|
||||||
const connectedColor = '#00c4c4'
|
const connectedColor = '#00c4c4'
|
||||||
const disabled = '#65707e'
|
const disabled = '#65707e'
|
||||||
const xs = '4px'
|
const xs = '4px'
|
||||||
|
@ -28,6 +29,7 @@ module.exports = Object.assign(
|
||||||
fontColor,
|
fontColor,
|
||||||
fancy: fancyColor,
|
fancy: fancyColor,
|
||||||
warning: warningColor,
|
warning: warningColor,
|
||||||
|
error: errorColor,
|
||||||
connected: connectedColor,
|
connected: connectedColor,
|
||||||
xs,
|
xs,
|
||||||
sm,
|
sm,
|
||||||
|
|
|
@ -31,3 +31,11 @@ export const saveToStorage = async (key: string, value: *): Promise<*> => {
|
||||||
console.error(`Failed to save ${key} in the storage:`, err)
|
console.error(`Failed to save ${key} in the storage:`, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const removeFromStorage = async (key: string): Promise<*> => {
|
||||||
|
try {
|
||||||
|
await storage.remove(`${PREFIX}__${key}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to remove ${key} from the storage:`, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue