pull from dev
This commit is contained in:
commit
3f98ac903b
|
@ -34,7 +34,7 @@
|
|||
"@gnosis.pm/util-contracts": "2.0.1",
|
||||
"@material-ui/core": "4.1.1",
|
||||
"@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",
|
||||
"bignumber.js": "9.0.0",
|
||||
"connected-react-router": "^6.3.1",
|
||||
|
@ -93,7 +93,7 @@
|
|||
"@testing-library/react": "^8.0.1",
|
||||
"autoprefixer": "9.6.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-eslint": "10.0.2",
|
||||
"babel-jest": "24.8.0",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||
|
@ -124,7 +124,7 @@
|
|||
"postcss-mixins": "^6.2.0",
|
||||
"postcss-simple-vars": "^5.0.2",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier-eslint-cli": "^4.7.1",
|
||||
"prettier-eslint-cli": "5.0.0",
|
||||
"run-with-testrpc": "0.3.1",
|
||||
"storybook-host": "^5.0.3",
|
||||
"storybook-router": "^0.3.3",
|
||||
|
@ -136,7 +136,7 @@
|
|||
"webpack": "4.34.0",
|
||||
"webpack-bundle-analyzer": "3.3.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,10 @@ const SelectInput = ({
|
|||
formControlProps,
|
||||
classes,
|
||||
renderValue,
|
||||
disableError,
|
||||
...rest
|
||||
}: SelectFieldProps) => {
|
||||
const showError = ((meta.submitError && !meta.dirtySinceLastSubmit) || meta.error) && meta.touched
|
||||
const showError = ((meta.submitError && !meta.dirtySinceLastSubmit) || meta.error) && meta.touched && !disableError
|
||||
const inputProps = {
|
||||
...restInput,
|
||||
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})`
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -24,7 +24,7 @@ class Block extends PureComponent<Props> {
|
|||
const paddingStyle = padding ? capitalize(padding, 'padding') : undefined
|
||||
return (
|
||||
<div className={cx(className, 'block', margin, paddingStyle, align)} {...props}>
|
||||
{ children }
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ type Props = {
|
|||
children: React.Node,
|
||||
color?: 'regular' | 'white',
|
||||
className?: string,
|
||||
innerRef: React.ElementRef<any>,
|
||||
innerRef?: React.ElementRef<any>,
|
||||
}
|
||||
|
||||
const GnosisLink = ({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import classNames from 'classnames/bind'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import { capitalize } from '~/utils/css'
|
||||
import styles from './index.scss'
|
||||
|
||||
|
@ -27,7 +27,7 @@ const Row = ({
|
|||
|
||||
return (
|
||||
<div className={rowClassNames} {...props}>
|
||||
{ children }
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
|||
export const executeTransaction = async (
|
||||
safeInstance: any,
|
||||
to: string,
|
||||
valueInWei: number,
|
||||
valueInWei: number | string,
|
||||
data: string,
|
||||
operation: number | string,
|
||||
nonce: string | number,
|
||||
|
|
|
@ -17,8 +17,12 @@ import { copyToClipboard } from '~/utils/clipboard'
|
|||
import Hairline from '~/components/layout/Hairline'
|
||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
import { 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 { secondary } from '~/theme/variables'
|
||||
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { styles } from './style'
|
||||
|
||||
type Props = {
|
||||
|
@ -50,87 +54,109 @@ const ReviewTx = ({
|
|||
createTransaction,
|
||||
}: Props) => (
|
||||
<SharedSnackbarConsumer>
|
||||
{({ openSnackbar }) => (
|
||||
<React.Fragment>
|
||||
<Row align="center" grow className={classes.heading}>
|
||||
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||
Send Funds
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton onClick={onClose} disableRipple>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo
|
||||
safeAddress={safeAddress}
|
||||
etherScanLink={etherScanLink}
|
||||
safeName={safeName}
|
||||
ethBalance={ethBalance}
|
||||
/>
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
|
||||
</Col>
|
||||
<Col xs={11} center="xs" layout="column">
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||
Recipient
|
||||
{({ 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>
|
||||
<Row align="center" grow className={classes.heading}>
|
||||
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||
Send Funds
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton onClick={onClose} disableRipple>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Row margin="md" align="center">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11} layout="column">
|
||||
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
|
||||
{tx.recipientAddress}
|
||||
<Link to={etherScanLink} target="_blank">
|
||||
<OpenInNew style={openIconStyle} />
|
||||
</Link>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo
|
||||
safeAddress={safeAddress}
|
||||
etherScanLink={etherScanLink}
|
||||
safeName={safeName}
|
||||
ethBalance={ethBalance}
|
||||
/>
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
|
||||
</Col>
|
||||
<Col xs={11} center="xs" layout="column">
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="md" align="center">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11} layout="column">
|
||||
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
|
||||
{tx.recipientAddress}
|
||||
<Link to={etherScanLink} target="_blank">
|
||||
<OpenInNew style={openIconStyle} />
|
||||
</Link>
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||
Amount
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row margin="md" align="center">
|
||||
<Img src={tx.token.logoUri} height={28} alt={tx.token.name} onError={setImageToPlaceholder} />
|
||||
<Paragraph size="md" noMargin className={classes.amount}>
|
||||
{tx.amount}
|
||||
{' '}
|
||||
{tx.token.symbol}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
onClick={submitTx}
|
||||
variant="contained"
|
||||
minWidth={140}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
>
|
||||
SUBMIT
|
||||
</Button>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||
Amount
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row margin="md" align="center">
|
||||
<Img src={tx.token.logoUri} height={28} alt={tx.token.name} onError={setImageToPlaceholder} />
|
||||
<Paragraph size="md" noMargin className={classes.amount}>
|
||||
{tx.amount}
|
||||
{' '}
|
||||
{tx.token.symbol}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
onClick={() => {
|
||||
createTransaction(safeAddress, tx.recipientAddress, tx.amount, tx.token, openSnackbar)
|
||||
onClose()
|
||||
}}
|
||||
variant="contained"
|
||||
minWidth={140}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
>
|
||||
SUBMIT
|
||||
</Button>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}}
|
||||
</SharedSnackbarConsumer>
|
||||
)
|
||||
|
||||
|
|
|
@ -149,6 +149,9 @@ class Layout extends React.Component<Props, State> {
|
|||
safeName={name}
|
||||
etherScanLink={etherScanLink}
|
||||
updateSafeName={updateSafeName}
|
||||
threshold={safe.threshold}
|
||||
owners={safe.owners}
|
||||
createTransaction={createTransaction}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -18,7 +18,7 @@ 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 { secondary } from '~/theme/variables'
|
||||
import { styles } from './style'
|
||||
|
||||
const openIconStyle = {
|
||||
|
@ -36,23 +36,12 @@ type Props = Actions & {
|
|||
}
|
||||
|
||||
const RemoveSafeComponent = ({
|
||||
onClose,
|
||||
isOpen,
|
||||
classes,
|
||||
safeAddress,
|
||||
etherScanLink,
|
||||
safeName,
|
||||
removeSafe,
|
||||
onClose, isOpen, classes, safeAddress, etherScanLink, safeName, removeSafe,
|
||||
}: Props) => (
|
||||
<Modal
|
||||
title="Remove Safe"
|
||||
description="Remove the selected Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
>
|
||||
<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 weight="bolder">
|
||||
Remove Safe
|
||||
Remove Safe
|
||||
</Paragraph>
|
||||
<IconButton onClick={onClose} disableRipple>
|
||||
<Close className={classes.close} />
|
||||
|
@ -83,17 +72,16 @@ const RemoveSafeComponent = ({
|
|||
<Hairline />
|
||||
<Row className={classes.description}>
|
||||
<Paragraph noMargin>
|
||||
Removing a Safe only removes it from your interface.
|
||||
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.
|
||||
. 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
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -106,7 +94,7 @@ const RemoveSafeComponent = ({
|
|||
variant="contained"
|
||||
minWidth={140}
|
||||
>
|
||||
Remove
|
||||
Remove
|
||||
</Button>
|
||||
</Row>
|
||||
</Modal>
|
||||
|
|
|
@ -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,100 @@
|
|||
// @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'
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Col from '~/components/layout/Col'
|
||||
|
@ -30,7 +30,7 @@ type Props = {
|
|||
classes: Object,
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
updateSafe: Funtion
|
||||
updateSafeName: Function
|
||||
}
|
||||
|
||||
const UpdateSafeName = (props: Props) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { lg, border } from '~/theme/variables'
|
||||
import { lg } from '~/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
title: {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { List } from 'immutable'
|
||||
import { connect } from 'react-redux'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Block from '~/components/layout/Block'
|
||||
|
@ -8,12 +10,15 @@ 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 UpdateSafeName from './UpdateSafeName'
|
||||
import ThresholdSettings from './ThresholdSettings'
|
||||
import actions, { type Actions } from './actions'
|
||||
import { styles } from './style'
|
||||
|
||||
type State = {
|
||||
showRemoveSafe: boolean,
|
||||
menuOptionIndex: number
|
||||
}
|
||||
|
||||
type Props = Actions & {
|
||||
|
@ -22,6 +27,9 @@ type Props = Actions & {
|
|||
etherScanLink: string,
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
owners: List<Owner>,
|
||||
threshold: number,
|
||||
createTransaction: Function,
|
||||
}
|
||||
|
||||
type Action = 'RemoveSafe'
|
||||
|
@ -53,6 +61,9 @@ class Settings extends React.Component<Props, State> {
|
|||
safeAddress,
|
||||
safeName,
|
||||
updateSafeName,
|
||||
owners,
|
||||
threshold,
|
||||
createTransaction,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
|
@ -79,21 +90,33 @@ class Settings extends React.Component<Props, State> {
|
|||
<Block className={classes.root}>
|
||||
<Col xs={3} layout="column">
|
||||
<Block className={classes.menu}>
|
||||
<Row className={classes.menuOption} onClick={this.handleChange(1)}>
|
||||
<Row
|
||||
className={cn(classes.menuOption, menuOptionIndex === 1 && classes.active)}
|
||||
onClick={this.handleChange(1)}
|
||||
>
|
||||
Safe name
|
||||
</Row>
|
||||
<Hairline />
|
||||
{granted && (
|
||||
<React.Fragment>
|
||||
<Row className={classes.menuOption} onClick={this.handleChange(2)}>
|
||||
<Row
|
||||
className={cn(classes.menuOption, menuOptionIndex === 2 && classes.active)}
|
||||
onClick={this.handleChange(2)}
|
||||
>
|
||||
Owners
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.menuOption} onClick={this.handleChange(3)}>
|
||||
<Row
|
||||
className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)}
|
||||
onClick={this.handleChange(3)}
|
||||
>
|
||||
Required confirmations
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.menuOption} onClick={this.handleChange(4)}>
|
||||
<Row
|
||||
className={cn(classes.menuOption, menuOptionIndex === 4 && classes.active)}
|
||||
onClick={this.handleChange(4)}
|
||||
>
|
||||
Modules
|
||||
</Row>
|
||||
<Hairline />
|
||||
|
@ -104,21 +127,18 @@ class Settings extends React.Component<Props, State> {
|
|||
<Col xs={9} layout="column">
|
||||
<Block className={classes.container}>
|
||||
{menuOptionIndex === 1 && (
|
||||
<UpdateSafeName
|
||||
<UpdateSafeName safeAddress={safeAddress} safeName={safeName} updateSafeName={updateSafeName} />
|
||||
)}
|
||||
{granted && menuOptionIndex === 2 && <p>To be done</p>}
|
||||
{granted && menuOptionIndex === 3 && (
|
||||
<ThresholdSettings
|
||||
owners={owners}
|
||||
threshold={threshold}
|
||||
createTransaction={createTransaction}
|
||||
safeAddress={safeAddress}
|
||||
safeName={safeName}
|
||||
updateSafeName={updateSafeName}
|
||||
/>
|
||||
)}
|
||||
{granted && menuOptionIndex === 2 && (
|
||||
<p>To be done</p>
|
||||
)}
|
||||
{granted && menuOptionIndex === 3 && (
|
||||
<p>To be done</p>
|
||||
)}
|
||||
{granted && menuOptionIndex === 4 && (
|
||||
<p>To be done</p>
|
||||
)}
|
||||
{granted && menuOptionIndex === 4 && <p>To be done</p>}
|
||||
</Block>
|
||||
</Col>
|
||||
</Block>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
import {
|
||||
sm, md, lg, border,
|
||||
sm, lg, border, secondary, bolderFont,
|
||||
} from '~/theme/variables'
|
||||
|
||||
export const styles = (theme: Object) => ({
|
||||
export const styles = () => ({
|
||||
root: {
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 -1px 4px 0 rgba(74, 85, 121, 0.5)',
|
||||
|
@ -22,6 +22,14 @@ export const styles = (theme: Object) => ({
|
|||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
active: {
|
||||
backgroundColor: '#f4f4f9',
|
||||
color: secondary,
|
||||
fontWeight: bolderFont,
|
||||
},
|
||||
container: {
|
||||
height: '100%',
|
||||
},
|
||||
message: {
|
||||
margin: `${sm} 0`,
|
||||
},
|
||||
|
|
|
@ -11,7 +11,7 @@ export type Props = Actions &
|
|||
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> {
|
||||
componentDidMount() {
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
// @flow
|
||||
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||
import { createAction } from 'redux-actions'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||
import { type GlobalState } from '~/store'
|
||||
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
import { executeTransaction, CALL } from '~/logic/safe/transactions'
|
||||
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
||||
|
||||
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
||||
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
||||
|
@ -17,39 +13,22 @@ export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
|||
const createTransaction = (
|
||||
safeAddress: string,
|
||||
to: string,
|
||||
valueInEth: string,
|
||||
token: Token,
|
||||
valueInWei: string,
|
||||
txData: string = EMPTY_DATA,
|
||||
openSnackbar: Function,
|
||||
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
||||
const isSendingETH = isEther(token.symbol)
|
||||
const state: GlobalState = getState()
|
||||
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const web3 = getWeb3()
|
||||
const from = userAccountSelector(state)
|
||||
const threshold = await safeInstance.getThreshold()
|
||||
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
|
||||
|
||||
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
|
||||
if (isExecution) {
|
||||
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')
|
||||
} else {
|
||||
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue