Merge pull request #39 from gnosis/development
WA-234 & WA-235 - Threshold modifications & Add owners
This commit is contained in:
commit
90920b619c
|
@ -1,2 +1,2 @@
|
||||||
// @flow
|
// @flow
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(45000)
|
||||||
|
|
|
@ -1276,8 +1276,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0x5fd674bc2873513f8e5a19d69637d0211e476380",
|
"address": "0x5fd674bc2873513f8e5a19d69637d0211e476380",
|
||||||
"transactionHash": "0x288775644c087eed5e41b96fecebdb23be4e6d40bef5b6fb9a2876c2a3145157"
|
"transactionHash": "0x288775644c087eed5e41b96fecebdb23be4e6d40bef5b6fb9a2876c2a3145157"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0x194371bd036c34314ded31a7ffe7e66f5461a62d",
|
||||||
|
"transactionHash": "0x288775644c087eed5e41b96fecebdb23be4e6d40bef5b6fb9a2876c2a3145157"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.138Z"
|
"updatedAt": "2018-06-06T14:51:43.695Z"
|
||||||
}
|
}
|
|
@ -6699,8 +6699,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0x3bdceb07fddd50d259a059ca9a75ecda561d4afc",
|
"address": "0x3bdceb07fddd50d259a059ca9a75ecda561d4afc",
|
||||||
"transactionHash": "0xf501438a4ec967e2928d922e4af568a2a5365002f8b3f9e32117bbacfaa49331"
|
"transactionHash": "0xf501438a4ec967e2928d922e4af568a2a5365002f8b3f9e32117bbacfaa49331"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0x0fb244fb862c95c66250c6c72e7e91a3d41a47d5",
|
||||||
|
"transactionHash": "0xf501438a4ec967e2928d922e4af568a2a5365002f8b3f9e32117bbacfaa49331"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.129Z"
|
"updatedAt": "2018-06-06T14:51:43.688Z"
|
||||||
}
|
}
|
|
@ -9864,8 +9864,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0x8c55b458a53e8c6e9efa7f54e7be9ca76b43dd9b",
|
"address": "0x8c55b458a53e8c6e9efa7f54e7be9ca76b43dd9b",
|
||||||
"transactionHash": "0x67117c1452ee2f4b904621b6f30790ff998d1f1a72f11c6b71ef47e3dd254724"
|
"transactionHash": "0x67117c1452ee2f4b904621b6f30790ff998d1f1a72f11c6b71ef47e3dd254724"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0xc3d42147f9b51c9749d3362e8b9ee6cb94861778",
|
||||||
|
"transactionHash": "0x67117c1452ee2f4b904621b6f30790ff998d1f1a72f11c6b71ef47e3dd254724"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.124Z"
|
"updatedAt": "2018-06-06T14:51:43.672Z"
|
||||||
}
|
}
|
|
@ -6876,8 +6876,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0xd4edae2f2d5718d1798deb48c062b939d6e9d4f4",
|
"address": "0xd4edae2f2d5718d1798deb48c062b939d6e9d4f4",
|
||||||
"transactionHash": "0xa71d3b0b3752acc18733fa881f70c256d63562f28ccca9af910fad3beee9181a"
|
"transactionHash": "0xa71d3b0b3752acc18733fa881f70c256d63562f28ccca9af910fad3beee9181a"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0xf2bc99498b610a01d76358d8e2fe251c9783a216",
|
||||||
|
"transactionHash": "0xa71d3b0b3752acc18733fa881f70c256d63562f28ccca9af910fad3beee9181a"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.114Z"
|
"updatedAt": "2018-06-06T14:51:43.677Z"
|
||||||
}
|
}
|
|
@ -1398,8 +1398,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0x2ebea54cbbd4f5491deba7a37605f8f0be3e3c9b",
|
"address": "0x2ebea54cbbd4f5491deba7a37605f8f0be3e3c9b",
|
||||||
"transactionHash": "0xb6a19a7a679a1474c09c651e4151421f210afa3f47effed019d4c0206144ee5f"
|
"transactionHash": "0xb6a19a7a679a1474c09c651e4151421f210afa3f47effed019d4c0206144ee5f"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0x4aa39923aa66871debec9b8aa9008a3b220eb1df",
|
||||||
|
"transactionHash": "0xb6a19a7a679a1474c09c651e4151421f210afa3f47effed019d4c0206144ee5f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.139Z"
|
"updatedAt": "2018-06-06T14:51:43.697Z"
|
||||||
}
|
}
|
|
@ -360,8 +360,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0x3946fcaaa0ba21aaffc5e06a3cc45debc9e07f7f",
|
"address": "0x3946fcaaa0ba21aaffc5e06a3cc45debc9e07f7f",
|
||||||
"transactionHash": "0xd044f1662e339061a8cabf2b06ac94a9f86fcccf3f5d80ebd1bea2a7542d4021"
|
"transactionHash": "0xd044f1662e339061a8cabf2b06ac94a9f86fcccf3f5d80ebd1bea2a7542d4021"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0xef9e7829f057e4d640bf66e17e06b3ab5cae508d",
|
||||||
|
"transactionHash": "0xd044f1662e339061a8cabf2b06ac94a9f86fcccf3f5d80ebd1bea2a7542d4021"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.139Z"
|
"updatedAt": "2018-06-06T14:51:43.696Z"
|
||||||
}
|
}
|
|
@ -1011,8 +1011,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0xbefd9f4a40b1bec8ec730969a3508d1739fb2742",
|
"address": "0xbefd9f4a40b1bec8ec730969a3508d1739fb2742",
|
||||||
"transactionHash": "0x75ad1066b44cd801ac66a316dbe4c09e72636d72b70fd62eb647295a0fc5e285"
|
"transactionHash": "0x75ad1066b44cd801ac66a316dbe4c09e72636d72b70fd62eb647295a0fc5e285"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0xaa973df8ec251cf67ec387d5627d42dbb738605f",
|
||||||
|
"transactionHash": "0x75ad1066b44cd801ac66a316dbe4c09e72636d72b70fd62eb647295a0fc5e285"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.112Z"
|
"updatedAt": "2018-06-06T14:51:43.668Z"
|
||||||
}
|
}
|
|
@ -7310,8 +7310,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0xfb1771240bb7edf209c70bd520a5d5424d23b084",
|
"address": "0xfb1771240bb7edf209c70bd520a5d5424d23b084",
|
||||||
"transactionHash": "0xf0cd95843453bdac02ad8018ef507479ea62989e56d69ad0ac1aad9d3a8515d2"
|
"transactionHash": "0xf0cd95843453bdac02ad8018ef507479ea62989e56d69ad0ac1aad9d3a8515d2"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0x9be4b89520d1dd6f2d115192587689c6c9bd1a99",
|
||||||
|
"transactionHash": "0xf0cd95843453bdac02ad8018ef507479ea62989e56d69ad0ac1aad9d3a8515d2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.135Z"
|
"updatedAt": "2018-06-06T14:51:43.692Z"
|
||||||
}
|
}
|
|
@ -5881,8 +5881,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0x589fd9eea7cca488a80e17a5105befff9616f11d",
|
"address": "0x589fd9eea7cca488a80e17a5105befff9616f11d",
|
||||||
"transactionHash": "0x0396e1c9da4fa7bd313286e6033446dbb6e491f267956f8cf13202ce534fd0e6"
|
"transactionHash": "0x0396e1c9da4fa7bd313286e6033446dbb6e491f267956f8cf13202ce534fd0e6"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0xe8abcdb37db8e7c563de32c5d207433ce44d1445",
|
||||||
|
"transactionHash": "0x0396e1c9da4fa7bd313286e6033446dbb6e491f267956f8cf13202ce534fd0e6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.118Z"
|
"updatedAt": "2018-06-06T14:51:43.684Z"
|
||||||
}
|
}
|
|
@ -4354,8 +4354,14 @@
|
||||||
"links": {},
|
"links": {},
|
||||||
"address": "0xca574a31a4cf1eeabeecaffc555bdc7f91c5caf9",
|
"address": "0xca574a31a4cf1eeabeecaffc555bdc7f91c5caf9",
|
||||||
"transactionHash": "0x463374c2fbc7eaff5b87e65c6a8fdc1177ef82c66084df6e7b88b506f99b193c"
|
"transactionHash": "0x463374c2fbc7eaff5b87e65c6a8fdc1177ef82c66084df6e7b88b506f99b193c"
|
||||||
|
},
|
||||||
|
"1528296677763": {
|
||||||
|
"events": {},
|
||||||
|
"links": {},
|
||||||
|
"address": "0x66c535e20f0c90530431ebab626da0ebdd55ec2d",
|
||||||
|
"transactionHash": "0x463374c2fbc7eaff5b87e65c6a8fdc1177ef82c66084df6e7b88b506f99b193c"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemaVersion": "2.0.0",
|
"schemaVersion": "2.0.0",
|
||||||
"updatedAt": "2018-06-04T10:56:37.132Z"
|
"updatedAt": "2018-06-06T14:51:43.700Z"
|
||||||
}
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import Checkbox, { type CheckoxProps } from 'material-ui/Checkbox'
|
||||||
|
|
||||||
|
class GnoCheckbox extends React.PureComponent<CheckoxProps> {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
input: {
|
||||||
|
checked, name, onChange, ...restInput
|
||||||
|
},
|
||||||
|
meta,
|
||||||
|
...rest
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
{...rest}
|
||||||
|
name={name}
|
||||||
|
inputProps={restInput}
|
||||||
|
onChange={onChange}
|
||||||
|
checked={!!checked}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GnoCheckbox
|
|
@ -29,7 +29,7 @@ const GnoForm = ({
|
||||||
render={({ handleSubmit, ...rest }) => (
|
render={({ handleSubmit, ...rest }) => (
|
||||||
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
|
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
|
||||||
{render(rest)}
|
{render(rest)}
|
||||||
{children(rest.submitting)}
|
{children(rest.submitting, rest.submitSucceeded)}
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,8 +10,12 @@ import PageFrame from '~/components/layout/PageFrame'
|
||||||
import { history, store } from '~/store'
|
import { history, store } from '~/store'
|
||||||
import theme from '~/theme/mui'
|
import theme from '~/theme/mui'
|
||||||
import AppRoutes from '~/routes'
|
import AppRoutes from '~/routes'
|
||||||
|
import fetchSafes from '~/routes/safe/store/actions/fetchSafes'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
store.dispatch(fetchSafes())
|
||||||
|
|
||||||
const Root = () => (
|
const Root = () => (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<MuiThemeProvider theme={theme}>
|
<MuiThemeProvider theme={theme}>
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
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) => () => (
|
||||||
|
<Block margin="md">
|
||||||
|
<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 owner?</Block>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default AddOwnerForm
|
|
@ -0,0 +1,43 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import { CircularProgress } from 'material-ui/Progress'
|
||||||
|
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/component/AddOwner/AddOwnerForm'
|
||||||
|
|
||||||
|
type FormProps = {
|
||||||
|
values: Object,
|
||||||
|
submitting: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinnerStyle = {
|
||||||
|
minHeight: '50px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Review = () => ({ 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 (
|
||||||
|
<Block>
|
||||||
|
<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>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Review
|
|
@ -0,0 +1,12 @@
|
||||||
|
// @flow
|
||||||
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
|
|
||||||
|
type FetchTransactions = typeof fetchTransactions
|
||||||
|
|
||||||
|
export type Actions = {
|
||||||
|
fetchTransactions: FetchTransactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchTransactions,
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
// @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/model/safe'
|
||||||
|
import { type Owner, makeOwner } from '~/routes/safe/store/model/owner'
|
||||||
|
import { getSafeEthereumInstance, createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||||
|
import { setOwners } from '~/utils/localStorage'
|
||||||
|
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'))
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddOwner extends React.Component<Props, State> {
|
||||||
|
state = {
|
||||||
|
done: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddOwner = async (values: Object) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
safe, threshold, userAddress, fetchTransactions,
|
||||||
|
} = this.props
|
||||||
|
const nonce = Date.now()
|
||||||
|
const newThreshold = values[INCREASE_PARAM] ? threshold + 1 : threshold
|
||||||
|
const newOwnerAddress = values[OWNER_ADDRESS_PARAM]
|
||||||
|
const newOwnerName = values[NAME_PARAM]
|
||||||
|
const safeAddress = safe.get('address')
|
||||||
|
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||||
|
const data = gnosisSafe.contract.addOwnerWithThreshold.getData(newOwnerAddress, newThreshold)
|
||||||
|
await createTransaction(safe, `Add Owner ${newOwnerName}`, safeAddress, 0, nonce, userAddress, data)
|
||||||
|
setOwners(safeAddress, safe.get('owners').push(makeOwner({ name: newOwnerName, address: newOwnerAddress })))
|
||||||
|
fetchTransactions()
|
||||||
|
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)
|
|
@ -0,0 +1,11 @@
|
||||||
|
// @flow
|
||||||
|
import { createStructuredSelector } from 'reselect'
|
||||||
|
import { userAccountSelector } from '~/wallets/store/selectors/index'
|
||||||
|
|
||||||
|
export type SelectorProps = {
|
||||||
|
userAddress: userAccountSelector,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createStructuredSelector({
|
||||||
|
userAddress: userAccountSelector,
|
||||||
|
})
|
|
@ -47,6 +47,7 @@ export const storeTransaction = (
|
||||||
tx: string,
|
tx: string,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
safeThreshold: number,
|
safeThreshold: number,
|
||||||
|
data: string,
|
||||||
) => {
|
) => {
|
||||||
const notMinedWhenOneOwnerSafe = confirmations.count() === 1 && !tx
|
const notMinedWhenOneOwnerSafe = confirmations.count() === 1 && !tx
|
||||||
if (notMinedWhenOneOwnerSafe) {
|
if (notMinedWhenOneOwnerSafe) {
|
||||||
|
@ -54,7 +55,7 @@ export const storeTransaction = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const transaction: Transaction = makeTransaction({
|
const transaction: Transaction = makeTransaction({
|
||||||
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx,
|
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx, data,
|
||||||
})
|
})
|
||||||
|
|
||||||
const safeTransactions = load(TX_KEY) || {}
|
const safeTransactions = load(TX_KEY) || {}
|
||||||
|
@ -79,37 +80,44 @@ const hasOneOwner = (safe: Safe) => {
|
||||||
return owners.count() === 1
|
return owners.count() === 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSafeEthereumInstance = async (safeAddress: string) => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||||
|
return GnosisSafe.at(safeAddress)
|
||||||
|
}
|
||||||
|
|
||||||
export const createTransaction = async (
|
export const createTransaction = async (
|
||||||
safe: Safe,
|
safe: Safe,
|
||||||
txName: string,
|
txName: string,
|
||||||
txDestination: string,
|
txDest: string,
|
||||||
txValue: number,
|
txValue: number,
|
||||||
nonce: number,
|
nonce: number,
|
||||||
user: string,
|
user: string,
|
||||||
|
data: string = '0x',
|
||||||
) => {
|
) => {
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
|
||||||
const safeAddress = safe.get('address')
|
const safeAddress = safe.get('address')
|
||||||
const gnosisSafe = GnosisSafe.at(safeAddress)
|
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||||
|
|
||||||
const valueInWei = web3.toWei(txValue, 'ether')
|
const valueInWei = web3.toWei(txValue, 'ether')
|
||||||
const CALL = 0
|
const CALL = 0
|
||||||
|
|
||||||
const thresholdIsOne = safe.get('confirmations') === 1
|
const thresholdIsOne = safe.get('threshold') === 1
|
||||||
if (hasOneOwner(safe) || thresholdIsOne) {
|
if (hasOneOwner(safe) || thresholdIsOne) {
|
||||||
const txConfirmationData = gnosisSafe.contract.execTransactionIfApproved.getData(txDestination, valueInWei, '0x', CALL, nonce)
|
const txConfirmationData =
|
||||||
|
gnosisSafe.contract.execTransactionIfApproved.getData(txDest, valueInWei, data, CALL, nonce)
|
||||||
const txHash = await executeTransaction(txConfirmationData, user, safeAddress)
|
const txHash = await executeTransaction(txConfirmationData, user, safeAddress)
|
||||||
checkReceiptStatus(txHash)
|
checkReceiptStatus(txHash)
|
||||||
|
|
||||||
const executedConfirmations: List<Confirmation> = buildExecutedConfirmationFrom(safe.get('owners'), user)
|
const executedConfirmations: List<Confirmation> = buildExecutedConfirmationFrom(safe.get('owners'), user)
|
||||||
return storeTransaction(txName, nonce, txDestination, txValue, user, executedConfirmations, txHash, safeAddress, safe.get('confirmations'))
|
return storeTransaction(txName, nonce, txDest, txValue, user, executedConfirmations, txHash, safeAddress, safe.get('threshold'), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const txConfirmationData = gnosisSafe.contract.approveTransactionWithParameters.getData(txDestination, valueInWei, '0x', CALL, nonce)
|
const txConfirmationData =
|
||||||
|
gnosisSafe.contract.approveTransactionWithParameters.getData(txDest, valueInWei, data, CALL, nonce)
|
||||||
const txConfirmationHash = await executeTransaction(txConfirmationData, user, safeAddress)
|
const txConfirmationHash = await executeTransaction(txConfirmationData, user, safeAddress)
|
||||||
checkReceiptStatus(txConfirmationHash)
|
checkReceiptStatus(txConfirmationHash)
|
||||||
|
|
||||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(safe.get('owners'), user, txConfirmationHash)
|
const confirmations: List<Confirmation> = buildConfirmationsFrom(safe.get('owners'), user, txConfirmationHash)
|
||||||
|
|
||||||
return storeTransaction(txName, nonce, txDestination, txValue, user, confirmations, '', safeAddress, safe.get('confirmations'))
|
return storeTransaction(txName, nonce, txDest, txValue, user, confirmations, '', safeAddress, safe.get('threshold'), data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ describe('Transactions Suite', () => {
|
||||||
const txName = 'Buy butteries for project'
|
const txName = 'Buy butteries for project'
|
||||||
const nonce: number = 10
|
const nonce: number = 10
|
||||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, 'foo', 'confirmationHash')
|
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, 'foo', 'confirmationHash')
|
||||||
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('confirmations'))
|
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||||
|
@ -45,7 +45,7 @@ describe('Transactions Suite', () => {
|
||||||
if (!safeTransactions) { throw new Error() }
|
if (!safeTransactions) { throw new Error() }
|
||||||
testSizeOfTransactions(safeTransactions, 1)
|
testSizeOfTransactions(safeTransactions, 1)
|
||||||
|
|
||||||
testTransactionFrom(safeTransactions, 0, txName, nonce, value, 2, destination, 'foo', 'confirmationHash', owners.get(0), owners.get(1))
|
testTransactionFrom(safeTransactions, 0, txName, nonce, value, 2, destination, '0x', 'foo', 'confirmationHash', owners.get(0), owners.get(1))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds second confirmation to stored safe with one confirmation', async () => {
|
it('adds second confirmation to stored safe with one confirmation', async () => {
|
||||||
|
@ -55,12 +55,12 @@ describe('Transactions Suite', () => {
|
||||||
const safeAddress = safe.get('address')
|
const safeAddress = safe.get('address')
|
||||||
const creator = 'foo'
|
const creator = 'foo'
|
||||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||||
storeTransaction(firstTxName, firstNonce, destination, value, creator, confirmations, '', safeAddress, safe.get('confirmations'))
|
storeTransaction(firstTxName, firstNonce, destination, value, creator, confirmations, '', safeAddress, safe.get('threshold'), '0x')
|
||||||
|
|
||||||
const secondTxName = 'Buy printers for project'
|
const secondTxName = 'Buy printers for project'
|
||||||
const secondNonce: number = firstNonce + 100
|
const secondNonce: number = firstNonce + 100
|
||||||
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||||
storeTransaction(secondTxName, secondNonce, destination, value, creator, secondConfirmations, '', safeAddress, safe.get('confirmations'))
|
storeTransaction(secondTxName, secondNonce, destination, value, creator, secondConfirmations, '', safeAddress, safe.get('threshold'), '0x')
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||||
|
@ -72,8 +72,8 @@ describe('Transactions Suite', () => {
|
||||||
if (!safeTxs) { throw new Error() }
|
if (!safeTxs) { throw new Error() }
|
||||||
testSizeOfTransactions(safeTxs, 2)
|
testSizeOfTransactions(safeTxs, 2)
|
||||||
|
|
||||||
testTransactionFrom(safeTxs, 0, firstTxName, firstNonce, value, 2, destination, 'foo', 'confirmationHash', owners.get(0), owners.get(1))
|
testTransactionFrom(safeTxs, 0, firstTxName, firstNonce, value, 2, destination, '0x', 'foo', 'confirmationHash', owners.get(0), owners.get(1))
|
||||||
testTransactionFrom(safeTxs, 1, secondTxName, secondNonce, value, 2, destination, 'foo', 'confirmationHash', owners.get(0), owners.get(1))
|
testTransactionFrom(safeTxs, 1, secondTxName, secondNonce, value, 2, destination, '0x', 'foo', 'confirmationHash', owners.get(0), owners.get(1))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds second confirmation to stored safe having two safes with one confirmation each', async () => {
|
it('adds second confirmation to stored safe having two safes with one confirmation each', async () => {
|
||||||
|
@ -82,7 +82,7 @@ describe('Transactions Suite', () => {
|
||||||
const safeAddress = safe.address
|
const safeAddress = safe.address
|
||||||
const creator = 'foo'
|
const creator = 'foo'
|
||||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safeAddress, safe.get('confirmations'))
|
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safeAddress, safe.get('threshold'), '0x')
|
||||||
|
|
||||||
const secondSafe = SafeFactory.dailyLimitSafe(10, 2)
|
const secondSafe = SafeFactory.dailyLimitSafe(10, 2)
|
||||||
const txSecondName = 'Buy batteris for Beta project'
|
const txSecondName = 'Buy batteris for Beta project'
|
||||||
|
@ -92,7 +92,7 @@ describe('Transactions Suite', () => {
|
||||||
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(secondSafe.get('owners'), secondCreator, 'confirmationHash')
|
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(secondSafe.get('owners'), secondCreator, 'confirmationHash')
|
||||||
storeTransaction(
|
storeTransaction(
|
||||||
txSecondName, txSecondNonce, destination, value, secondCreator,
|
txSecondName, txSecondNonce, destination, value, secondCreator,
|
||||||
secondConfirmations, '', secondSafeAddress, secondSafe.get('confirmations'),
|
secondConfirmations, '', secondSafeAddress, secondSafe.get('threshold'), '0x',
|
||||||
)
|
)
|
||||||
|
|
||||||
let transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
let transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||||
|
@ -112,7 +112,7 @@ describe('Transactions Suite', () => {
|
||||||
const txConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'secondConfirmationHash')
|
const txConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'secondConfirmationHash')
|
||||||
storeTransaction(
|
storeTransaction(
|
||||||
txFirstName, txFirstNonce, destination, value, creator,
|
txFirstName, txFirstNonce, destination, value, creator,
|
||||||
txConfirmations, '', safe.get('address'), safe.get('confirmations'),
|
txConfirmations, '', safe.get('address'), safe.get('threshold'), '0x',
|
||||||
)
|
)
|
||||||
|
|
||||||
transactions = loadSafeTransactions()
|
transactions = loadSafeTransactions()
|
||||||
|
@ -125,19 +125,19 @@ describe('Transactions Suite', () => {
|
||||||
// Test 2 transactions of first safe
|
// Test 2 transactions of first safe
|
||||||
testTransactionFrom(
|
testTransactionFrom(
|
||||||
transactions.get(safe.address), 0,
|
transactions.get(safe.address), 0,
|
||||||
txName, nonce, value, 2, destination,
|
txName, nonce, value, 2, destination, '0x',
|
||||||
'foo', 'confirmationHash', owners.get(0), owners.get(1),
|
'foo', 'confirmationHash', owners.get(0), owners.get(1),
|
||||||
)
|
)
|
||||||
testTransactionFrom(
|
testTransactionFrom(
|
||||||
transactions.get(safe.address), 1,
|
transactions.get(safe.address), 1,
|
||||||
txFirstName, txFirstNonce, value, 2, destination,
|
txFirstName, txFirstNonce, value, 2, destination, '0x',
|
||||||
'foo', 'secondConfirmationHash', owners.get(0), owners.get(1),
|
'foo', 'secondConfirmationHash', owners.get(0), owners.get(1),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test one transaction of second safe
|
// Test one transaction of second safe
|
||||||
testTransactionFrom(
|
testTransactionFrom(
|
||||||
transactions.get(secondSafe.address), 0,
|
transactions.get(secondSafe.address), 0,
|
||||||
txSecondName, txSecondNonce, value, 2, destination,
|
txSecondName, txSecondNonce, value, 2, destination, '0x',
|
||||||
'0x03db1a8b26d08df23337e9276a36b474510f0023', 'confirmationHash', secondSafe.get('owners').get(0), secondSafe.get('owners').get(1),
|
'0x03db1a8b26d08df23337e9276a36b474510f0023', 'confirmationHash', secondSafe.get('owners').get(0), secondSafe.get('owners').get(1),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -148,10 +148,10 @@ describe('Transactions Suite', () => {
|
||||||
const nonce: number = 10
|
const nonce: number = 10
|
||||||
const creator = 'foo'
|
const creator = 'foo'
|
||||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'))
|
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'))
|
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
|
||||||
expect(createTxFnc).toThrow(/Transaction with same nonce/)
|
expect(createTxFnc).toThrow(/Transaction with same nonce/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ describe('Transactions Suite', () => {
|
||||||
const nonce: number = 10
|
const nonce: number = 10
|
||||||
const creator = 'foo'
|
const creator = 'foo'
|
||||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'))
|
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||||
|
@ -185,7 +185,7 @@ describe('Transactions Suite', () => {
|
||||||
const nonce: number = 10
|
const nonce: number = 10
|
||||||
const tx = ''
|
const tx = ''
|
||||||
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
|
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
|
||||||
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('confirmations'))
|
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('threshold'), '0x')
|
||||||
|
|
||||||
expect(createTxFnc).toThrow(/The tx should be mined before storing it in safes with one owner/)
|
expect(createTxFnc).toThrow(/The tx should be mined before storing it in safes with one owner/)
|
||||||
})
|
})
|
||||||
|
@ -197,7 +197,7 @@ describe('Transactions Suite', () => {
|
||||||
const nonce: number = 10
|
const nonce: number = 10
|
||||||
const tx = 'validTxHash'
|
const tx = 'validTxHash'
|
||||||
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
|
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
|
||||||
storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('confirmations'))
|
storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('threshold'), '0x')
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const safeTransactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
const safeTransactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const testSizeOfTransactions = (safeTxs: List<Transaction> | typeof undef
|
||||||
export const testTransactionFrom = (
|
export const testTransactionFrom = (
|
||||||
safeTxs: List<Transaction> | typeof undefined, pos: number, name: string,
|
safeTxs: List<Transaction> | typeof undefined, pos: number, name: string,
|
||||||
nonce: number, value: number, threshold: number, destination: string,
|
nonce: number, value: number, threshold: number, destination: string,
|
||||||
creator: string, txHash: string,
|
data: string, creator: string, txHash: string,
|
||||||
firstOwner: Owner | typeof undefined, secondOwner: Owner | typeof undefined,
|
firstOwner: Owner | typeof undefined, secondOwner: Owner | typeof undefined,
|
||||||
) => {
|
) => {
|
||||||
if (!safeTxs) { throw new Error() }
|
if (!safeTxs) { throw new Error() }
|
||||||
|
@ -33,6 +33,7 @@ export const testTransactionFrom = (
|
||||||
expect(tx.get('destination')).toBe(destination)
|
expect(tx.get('destination')).toBe(destination)
|
||||||
expect(tx.get('confirmations').count()).toBe(2)
|
expect(tx.get('confirmations').count()).toBe(2)
|
||||||
expect(tx.get('nonce')).toBe(nonce)
|
expect(tx.get('nonce')).toBe(nonce)
|
||||||
|
expect(tx.get('data')).toBe(data)
|
||||||
|
|
||||||
const confirmations: List<Confirmation> = tx.get('confirmations')
|
const confirmations: List<Confirmation> = tx.get('confirmations')
|
||||||
const firstConfirmation: Confirmation | typeof undefined = confirmations.get(0)
|
const firstConfirmation: Confirmation | typeof undefined = confirmations.get(0)
|
||||||
|
|
|
@ -4,12 +4,16 @@ import { ListItem } from 'material-ui/List'
|
||||||
import Avatar from 'material-ui/Avatar'
|
import Avatar from 'material-ui/Avatar'
|
||||||
import DoneAll from 'material-ui-icons/DoneAll'
|
import DoneAll from 'material-ui-icons/DoneAll'
|
||||||
import ListItemText from '~/components/List/ListItemText'
|
import ListItemText from '~/components/List/ListItemText'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
confirmations: number,
|
confirmations: number,
|
||||||
|
onEditThreshold: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Confirmations = ({ confirmations }: Props) => (
|
const EDIT_THRESHOLD_BUTTON_TEXT = 'EDIT'
|
||||||
|
|
||||||
|
const Confirmations = ({ confirmations, onEditThreshold }: Props) => (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<DoneAll />
|
<DoneAll />
|
||||||
|
@ -19,6 +23,13 @@ const Confirmations = ({ confirmations }: Props) => (
|
||||||
secondary={`${confirmations} required confirmations per transaction`}
|
secondary={`${confirmations} required confirmations per transaction`}
|
||||||
cut
|
cut
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
variant="raised"
|
||||||
|
color="primary"
|
||||||
|
onClick={onEditThreshold}
|
||||||
|
>
|
||||||
|
{EDIT_THRESHOLD_BUTTON_TEXT}
|
||||||
|
</Button>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Collapse from 'material-ui/transitions/Collapse'
|
||||||
import ListItemText from '~/components/List/ListItemText'
|
import ListItemText from '~/components/List/ListItemText'
|
||||||
import List, { ListItem, ListItemIcon } from 'material-ui/List'
|
import List, { ListItem, ListItemIcon } from 'material-ui/List'
|
||||||
import Avatar from 'material-ui/Avatar'
|
import Avatar from 'material-ui/Avatar'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
import Group from 'material-ui-icons/Group'
|
import Group from 'material-ui-icons/Group'
|
||||||
import Person from 'material-ui-icons/Person'
|
import Person from 'material-ui-icons/Person'
|
||||||
import ExpandLess from 'material-ui-icons/ExpandLess'
|
import ExpandLess from 'material-ui-icons/ExpandLess'
|
||||||
|
@ -21,10 +22,13 @@ const styles = {
|
||||||
|
|
||||||
type Props = Open & WithStyles & {
|
type Props = Open & WithStyles & {
|
||||||
owners: List<OwnerProps>,
|
owners: List<OwnerProps>,
|
||||||
|
onAddOwner: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ADD_OWNER_BUTTON_TEXT = 'Add'
|
||||||
|
|
||||||
const Owners = openHoc(({
|
const Owners = openHoc(({
|
||||||
open, toggle, owners, classes,
|
open, toggle, owners, classes, onAddOwner,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ListItem onClick={toggle}>
|
<ListItem onClick={toggle}>
|
||||||
|
@ -35,6 +39,13 @@ const Owners = openHoc(({
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
{open ? <ExpandLess /> : <ExpandMore />}
|
{open ? <ExpandLess /> : <ExpandMore />}
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
<Button
|
||||||
|
variant="raised"
|
||||||
|
color="primary"
|
||||||
|
onClick={onAddOwner}
|
||||||
|
>
|
||||||
|
{ADD_OWNER_BUTTON_TEXT}
|
||||||
|
</Button>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||||
<List component="div" disablePadding>
|
<List component="div" disablePadding>
|
||||||
|
|
|
@ -12,6 +12,8 @@ import List from 'material-ui/List'
|
||||||
import Withdrawn from '~/routes/safe/component/Withdrawn'
|
import Withdrawn from '~/routes/safe/component/Withdrawn'
|
||||||
import Transactions from '~/routes/safe/component/Transactions'
|
import Transactions from '~/routes/safe/component/Transactions'
|
||||||
import AddTransaction from '~/routes/safe/component/AddTransaction'
|
import AddTransaction from '~/routes/safe/component/AddTransaction'
|
||||||
|
import Threshold from '~/routes/safe/component/Threshold'
|
||||||
|
import AddOwner from '~/routes/safe/component/AddOwner'
|
||||||
|
|
||||||
import Address from './Address'
|
import Address from './Address'
|
||||||
import Balance from './Balance'
|
import Balance from './Balance'
|
||||||
|
@ -59,6 +61,18 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
this.setState({ component: <Transactions safeName={safe.get('name')} safeAddress={safe.get('address')} onAddTx={this.onAddTx} /> })
|
this.setState({ component: <Transactions safeName={safe.get('name')} safeAddress={safe.get('address')} onAddTx={this.onAddTx} /> })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEditThreshold = () => {
|
||||||
|
const { safe } = this.props
|
||||||
|
|
||||||
|
this.setState({ component: <Threshold numOwners={safe.get('owners').count()} safe={safe} /> })
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddOwner = (e: SyntheticEvent<HTMLButtonElement>) => {
|
||||||
|
const { safe } = this.props
|
||||||
|
e.stopPropagation()
|
||||||
|
this.setState({ component: <AddOwner threshold={safe.get('threshold')} safe={safe} /> })
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { safe, balance } = this.props
|
const { safe, balance } = this.props
|
||||||
const { component } = this.state
|
const { component } = this.state
|
||||||
|
@ -68,8 +82,8 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
||||||
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
||||||
<List style={listStyle}>
|
<List style={listStyle}>
|
||||||
<Balance balance={balance} />
|
<Balance balance={balance} />
|
||||||
<Owners owners={safe.owners} />
|
<Owners owners={safe.owners} onAddOwner={this.onAddOwner} />
|
||||||
<Confirmations confirmations={safe.get('confirmations')} />
|
<Confirmations confirmations={safe.get('threshold')} onEditThreshold={this.onEditThreshold} />
|
||||||
<Address address={safe.get('address')} />
|
<Address address={safe.get('address')} />
|
||||||
<DailyLimit balance={balance} dailyLimit={safe.get('dailyLimit')} onWithdrawn={this.onWithdrawn} />
|
<DailyLimit balance={balance} dailyLimit={safe.get('dailyLimit')} onWithdrawn={this.onWithdrawn} />
|
||||||
<MultisigTx balance={balance} onAddTx={this.onAddTx} onSeeTxs={this.onListTransactions} />
|
<MultisigTx balance={balance} onAddTx={this.onAddTx} onSeeTxs={this.onListTransactions} />
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import { CircularProgress } from 'material-ui/Progress'
|
||||||
|
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 { THRESHOLD_PARAM } from '~/routes/safe/component/Threshold/ThresholdForm'
|
||||||
|
|
||||||
|
type FormProps = {
|
||||||
|
values: Object,
|
||||||
|
submitting: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinnerStyle = {
|
||||||
|
minHeight: '50px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Review = () => ({ values, submitting }: FormProps) => (
|
||||||
|
<Block>
|
||||||
|
<Heading tag="h2">Review the Threshold operation</Heading>
|
||||||
|
<Paragraph align="left">
|
||||||
|
<Bold>The new threshold will be: </Bold> {values[THRESHOLD_PARAM]}
|
||||||
|
</Paragraph>
|
||||||
|
<Block style={spinnerStyle}>
|
||||||
|
{ submitting && <CircularProgress size={50} /> }
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Review
|
|
@ -0,0 +1,43 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Heading from '~/components/layout/Heading'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
import TextField from '~/components/forms/TextField'
|
||||||
|
import { composeValidators, minValue, maxValue, mustBeInteger, required } from '~/components/forms/validator'
|
||||||
|
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||||
|
|
||||||
|
export const THRESHOLD_PARAM = 'threshold'
|
||||||
|
|
||||||
|
type ThresholdProps = {
|
||||||
|
numOwners: number,
|
||||||
|
safe: Safe,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThresholdForm = ({ numOwners, safe }: ThresholdProps) => () => (
|
||||||
|
<Block margin="md">
|
||||||
|
<Heading tag="h2" margin="lg">
|
||||||
|
{'Change safe\'s threshold'}
|
||||||
|
</Heading>
|
||||||
|
<Heading tag="h4" margin="lg">
|
||||||
|
{`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('threshold')}`}
|
||||||
|
</Heading>
|
||||||
|
<Block margin="md">
|
||||||
|
<Field
|
||||||
|
name={THRESHOLD_PARAM}
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(
|
||||||
|
required,
|
||||||
|
mustBeInteger,
|
||||||
|
minValue(1),
|
||||||
|
maxValue(numOwners),
|
||||||
|
)}
|
||||||
|
placeholder="New threshold"
|
||||||
|
text="Safe's threshold"
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ThresholdForm
|
|
@ -0,0 +1,12 @@
|
||||||
|
// @flow
|
||||||
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
|
|
||||||
|
type FetchTransactions = typeof fetchTransactions
|
||||||
|
|
||||||
|
export type Actions = {
|
||||||
|
fetchTransactions: FetchTransactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchTransactions,
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import Stepper from '~/components/Stepper'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { getSafeEthereumInstance, createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||||
|
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||||
|
import ThresholdForm, { THRESHOLD_PARAM } from './ThresholdForm'
|
||||||
|
import selector, { type SelectorProps } from './selector'
|
||||||
|
import actions, { type Actions } from './actions'
|
||||||
|
import Review from './Review'
|
||||||
|
|
||||||
|
type Props = SelectorProps & Actions & {
|
||||||
|
numOwners: number,
|
||||||
|
safe: Safe,
|
||||||
|
onReset: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSteps = () => [
|
||||||
|
'Fill Change threshold Form', 'Review change threshold operation',
|
||||||
|
]
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
done: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CHANGE_THRESHOLD_RESET_BUTTON_TEXT = 'RESET'
|
||||||
|
|
||||||
|
class Threshold extends React.PureComponent<Props, State> {
|
||||||
|
state = {
|
||||||
|
done: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
onThreshold = async (values: Object) => {
|
||||||
|
try {
|
||||||
|
const { safe, userAddress } = this.props // , fetchThreshold } = this.props
|
||||||
|
const newThreshold = values[THRESHOLD_PARAM]
|
||||||
|
const gnosisSafe = await getSafeEthereumInstance(safe.get('address'))
|
||||||
|
const nonce = Date.now()
|
||||||
|
const data = gnosisSafe.contract.changeThreshold.getData(newThreshold)
|
||||||
|
await createTransaction(safe, `Change Safe's threshold [${nonce}]`, safe.get('address'), 0, nonce, userAddress, data)
|
||||||
|
await this.props.fetchTransactions()
|
||||||
|
this.setState({ done: true })
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ done: false })
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error while changing threshold ' + error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset = () => {
|
||||||
|
this.setState({ done: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { numOwners, safe } = this.props
|
||||||
|
const { done } = this.state
|
||||||
|
const steps = getSteps()
|
||||||
|
const finishedButton = <Stepper.FinishButton title={CHANGE_THRESHOLD_RESET_BUTTON_TEXT} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Stepper
|
||||||
|
finishedTransaction={done}
|
||||||
|
finishedButton={finishedButton}
|
||||||
|
onSubmit={this.onThreshold}
|
||||||
|
steps={steps}
|
||||||
|
onReset={this.onReset}
|
||||||
|
>
|
||||||
|
<Stepper.Page numOwners={numOwners} safe={safe}>
|
||||||
|
{ ThresholdForm }
|
||||||
|
</Stepper.Page>
|
||||||
|
<Stepper.Page>
|
||||||
|
{ Review }
|
||||||
|
</Stepper.Page>
|
||||||
|
</Stepper>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(selector, actions)(Threshold)
|
|
@ -0,0 +1,11 @@
|
||||||
|
// @flow
|
||||||
|
import { createStructuredSelector } from 'reselect'
|
||||||
|
import { userAccountSelector } from '~/wallets/store/selectors/index'
|
||||||
|
|
||||||
|
export type SelectorProps = {
|
||||||
|
userAddress: userAccountSelector,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createStructuredSelector({
|
||||||
|
userAddress: userAccountSelector,
|
||||||
|
})
|
|
@ -1,8 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
|
|
||||||
|
type FetchTransactions = typeof fetchTransactions
|
||||||
|
|
||||||
export type Actions = {
|
export type Actions = {
|
||||||
fetchTransactions: typeof fetchTransactions,
|
fetchTransactions: FetchTransactions,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { connect } from 'react-redux'
|
||||||
import { type Transaction } from '~/routes/safe/store/model/transaction'
|
import { type Transaction } from '~/routes/safe/store/model/transaction'
|
||||||
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
|
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
|
||||||
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
|
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
|
||||||
import { sleep } from '~/utils/timer'
|
|
||||||
import { processTransaction } from './processTransactions'
|
import { processTransaction } from './processTransactions'
|
||||||
import selector, { type SelectorProps } from './selector'
|
import selector, { type SelectorProps } from './selector'
|
||||||
import actions, { type Actions } from './actions'
|
import actions, { type Actions } from './actions'
|
||||||
|
@ -17,9 +16,11 @@ type Props = SelectorProps & Actions & {
|
||||||
}
|
}
|
||||||
class Transactions extends React.Component<Props, {}> {
|
class Transactions extends React.Component<Props, {}> {
|
||||||
onProcessTx = async (tx: Transaction, alreadyConfirmed: number) => {
|
onProcessTx = async (tx: Transaction, alreadyConfirmed: number) => {
|
||||||
const { fetchTransactions, safeAddress, userAddress } = this.props
|
const {
|
||||||
|
fetchTransactions, safeAddress, userAddress,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress)
|
await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress)
|
||||||
await sleep(1200)
|
|
||||||
fetchTransactions()
|
fetchTransactions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,10 @@ export const updateTransaction = (
|
||||||
tx: string,
|
tx: string,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
safeThreshold: number,
|
safeThreshold: number,
|
||||||
|
data: string,
|
||||||
) => {
|
) => {
|
||||||
const transaction: Transaction = makeTransaction({
|
const transaction: Transaction = makeTransaction({
|
||||||
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx,
|
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx, data,
|
||||||
})
|
})
|
||||||
|
|
||||||
const safeTransactions = load(TX_KEY) || {}
|
const safeTransactions = load(TX_KEY) || {}
|
||||||
|
@ -36,7 +37,6 @@ export const updateTransaction = (
|
||||||
localStorage.setItem(TX_KEY, JSON.stringify(safeTransactions))
|
localStorage.setItem(TX_KEY, JSON.stringify(safeTransactions))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getData = () => '0x'
|
|
||||||
const getOperation = () => 0
|
const getOperation = () => 0
|
||||||
|
|
||||||
const execTransaction = async (
|
const execTransaction = async (
|
||||||
|
@ -45,8 +45,8 @@ const execTransaction = async (
|
||||||
txValue: number,
|
txValue: number,
|
||||||
nonce: number,
|
nonce: number,
|
||||||
executor: string,
|
executor: string,
|
||||||
|
data: string,
|
||||||
) => {
|
) => {
|
||||||
const data = getData()
|
|
||||||
const CALL = getOperation()
|
const CALL = getOperation()
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const valueInWei = web3.toWei(txValue, 'ether')
|
const valueInWei = web3.toWei(txValue, 'ether')
|
||||||
|
@ -61,8 +61,8 @@ const execConfirmation = async (
|
||||||
txValue: number,
|
txValue: number,
|
||||||
nonce: number,
|
nonce: number,
|
||||||
executor: string,
|
executor: string,
|
||||||
|
data: string,
|
||||||
) => {
|
) => {
|
||||||
const data = getData()
|
|
||||||
const CALL = getOperation()
|
const CALL = getOperation()
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const valueInWei = web3.toWei(txValue, 'ether')
|
const valueInWei = web3.toWei(txValue, 'ether')
|
||||||
|
@ -110,10 +110,11 @@ export const processTransaction = async (
|
||||||
const txName = tx.get('name')
|
const txName = tx.get('name')
|
||||||
const txValue = tx.get('value')
|
const txValue = tx.get('value')
|
||||||
const txDestination = tx.get('destination')
|
const txDestination = tx.get('destination')
|
||||||
|
const data = tx.get('data')
|
||||||
|
|
||||||
const txHash = thresholdReached
|
const txHash = thresholdReached
|
||||||
? await execTransaction(gnosisSafe, txDestination, txValue, nonce, userAddress)
|
? await execTransaction(gnosisSafe, txDestination, txValue, nonce, userAddress, data)
|
||||||
: await execConfirmation(gnosisSafe, txDestination, txValue, nonce, userAddress)
|
: await execConfirmation(gnosisSafe, txDestination, txValue, nonce, userAddress, data)
|
||||||
|
|
||||||
checkReceiptStatus(txHash)
|
checkReceiptStatus(txHash)
|
||||||
|
|
||||||
|
@ -130,5 +131,6 @@ export const processTransaction = async (
|
||||||
thresholdReached ? txHash : '',
|
thresholdReached ? txHash : '',
|
||||||
safeAddress,
|
safeAddress,
|
||||||
threshold,
|
threshold,
|
||||||
|
data,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
// @flow
|
|
||||||
import fetchDailyLimit from '~/routes/safe/store/actions/fetchDailyLimit'
|
|
||||||
|
|
||||||
export type Actions = {
|
|
||||||
fetchDailyLimit: typeof fetchDailyLimit,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
fetchDailyLimit,
|
|
||||||
}
|
|
|
@ -2,9 +2,7 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import Stepper from '~/components/Stepper'
|
import Stepper from '~/components/Stepper'
|
||||||
import { sleep } from '~/utils/timer'
|
|
||||||
import { type DailyLimit } from '~/routes/safe/store/model/dailyLimit'
|
import { type DailyLimit } from '~/routes/safe/store/model/dailyLimit'
|
||||||
import actions, { type Actions } from './actions'
|
|
||||||
import selector, { type SelectorProps } from './selector'
|
import selector, { type SelectorProps } from './selector'
|
||||||
import withdrawn from './withdrawn'
|
import withdrawn from './withdrawn'
|
||||||
import WithdrawnForm from './WithdrawnForm'
|
import WithdrawnForm from './WithdrawnForm'
|
||||||
|
@ -14,7 +12,7 @@ const getSteps = () => [
|
||||||
'Fill Withdrawn Form', 'Review Withdrawn',
|
'Fill Withdrawn Form', 'Review Withdrawn',
|
||||||
]
|
]
|
||||||
|
|
||||||
type Props = SelectorProps & Actions & {
|
type Props = SelectorProps & {
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
dailyLimit: DailyLimit,
|
dailyLimit: DailyLimit,
|
||||||
}
|
}
|
||||||
|
@ -34,8 +32,6 @@ class Withdrawn extends React.Component<Props, State> {
|
||||||
try {
|
try {
|
||||||
const { safeAddress, userAddress } = this.props
|
const { safeAddress, userAddress } = this.props
|
||||||
await withdrawn(values, safeAddress, userAddress)
|
await withdrawn(values, safeAddress, userAddress)
|
||||||
await sleep(3500)
|
|
||||||
this.props.fetchDailyLimit(safeAddress)
|
|
||||||
this.setState({ done: true })
|
this.setState({ done: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ done: false })
|
this.setState({ done: false })
|
||||||
|
@ -75,5 +71,5 @@ class Withdrawn extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(selector, actions)(Withdrawn)
|
export default connect(selector)(Withdrawn)
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
||||||
import fetchBalance from '~/routes/safe/store/actions/fetchBalance'
|
import fetchBalance from '~/routes/safe/store/actions/fetchBalance'
|
||||||
import fetchDailyLimit from '~/routes/safe/store/actions/fetchDailyLimit'
|
|
||||||
|
|
||||||
export type Actions = {
|
export type Actions = {
|
||||||
|
fetchSafe: typeof fetchSafe,
|
||||||
fetchBalance: typeof fetchBalance,
|
fetchBalance: typeof fetchBalance,
|
||||||
fetchDailyLimit: typeof fetchDailyLimit,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
fetchSafe,
|
||||||
fetchBalance,
|
fetchBalance,
|
||||||
fetchDailyLimit,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,17 +14,12 @@ type Props = Actions & SelectorProps & {
|
||||||
class SafeView extends React.PureComponent<Props> {
|
class SafeView extends React.PureComponent<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
const { safe, fetchBalance } = this.props
|
const { safe, fetchSafe, fetchBalance } = this.props
|
||||||
if (!safe) { return }
|
if (!safe) { return }
|
||||||
|
|
||||||
const safeAddress: string = safe.get('address')
|
const safeAddress: string = safe.get('address')
|
||||||
fetchBalance(safeAddress)
|
fetchBalance(safeAddress)
|
||||||
|
fetchSafe(safe)
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
const { fetchDailyLimit, safe } = this.props
|
|
||||||
if (safe) {
|
|
||||||
fetchDailyLimit(safe.get('address'))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|
|
@ -20,14 +20,14 @@ const addSafe = createAction(
|
||||||
ADD_SAFE,
|
ADD_SAFE,
|
||||||
(
|
(
|
||||||
name: string, address: string,
|
name: string, address: string,
|
||||||
confirmations: number, limit: number,
|
threshold: number, limit: number,
|
||||||
ownersName: string[], ownersAddress: string[],
|
ownersName: string[], ownersAddress: string[],
|
||||||
): SafeProps => {
|
): SafeProps => {
|
||||||
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
|
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
|
||||||
const dailyLimit: DailyLimit = buildDailyLimitFrom(limit)
|
const dailyLimit: DailyLimit = buildDailyLimitFrom(limit)
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
address, name, confirmations, owners, dailyLimit,
|
address, name, threshold, owners, dailyLimit,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
// @flow
|
|
||||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
|
||||||
import { type GlobalState } from '~/store/index'
|
|
||||||
import { getDailyLimitFrom } from '~/routes/safe/component/Withdrawn/withdrawn'
|
|
||||||
import { type DailyLimitProps } from '~/routes/safe/store/model/dailyLimit'
|
|
||||||
import updateDailyLimit from './updateDailyLimit'
|
|
||||||
|
|
||||||
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
|
||||||
const ethAddress = 0
|
|
||||||
const dailyLimit: DailyLimitProps = await getDailyLimitFrom(safeAddress, ethAddress)
|
|
||||||
|
|
||||||
return dispatch(updateDailyLimit(safeAddress, dailyLimit))
|
|
||||||
}
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
// @flow
|
||||||
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
|
import { List, Map } from 'immutable'
|
||||||
|
import { type GlobalState } from '~/store/index'
|
||||||
|
import { makeOwner } from '~/routes/safe/store/model/owner'
|
||||||
|
import { type SafeProps, type Safe, makeSafe } from '~/routes/safe/store/model/safe'
|
||||||
|
import { makeDailyLimit } from '~/routes/safe/store/model/dailyLimit'
|
||||||
|
import { getDailyLimitFrom } from '~/routes/safe/component/Withdrawn/withdrawn'
|
||||||
|
import { getGnosisSafeInstanceAt } from '~/wallets/safeContracts'
|
||||||
|
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||||
|
import { getOwners } from '~/utils/localStorage'
|
||||||
|
|
||||||
|
const buildOwnersFrom = (safeOwners: string[], storedOwners: Map<string, string>) => (
|
||||||
|
safeOwners.map((ownerAddress: string) => {
|
||||||
|
const ownerName = storedOwners.get(ownerAddress.toLowerCase()) || 'UNKNOWN'
|
||||||
|
return makeOwner({ name: ownerName, address: ownerAddress })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const buildSafe = async (storedSafe: Object) => {
|
||||||
|
const safeAddress = storedSafe.address
|
||||||
|
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
|
||||||
|
const dailyLimit = makeDailyLimit(await getDailyLimitFrom(safeAddress, 0))
|
||||||
|
const threshold = Number(await gnosisSafe.getThreshold())
|
||||||
|
const owners = List(buildOwnersFrom(await gnosisSafe.getOwners(), getOwners(safeAddress)))
|
||||||
|
|
||||||
|
const safe: SafeProps = {
|
||||||
|
address: safeAddress,
|
||||||
|
dailyLimit,
|
||||||
|
name: storedSafe.name,
|
||||||
|
threshold,
|
||||||
|
owners,
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeSafe(safe)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (safe: Safe) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
|
const safeRecord = await buildSafe(safe.toJSON())
|
||||||
|
|
||||||
|
return dispatch(updateSafe(safeRecord))
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// @flow
|
||||||
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
|
import { Map } from 'immutable'
|
||||||
|
import { type GlobalState } from '~/store/index'
|
||||||
|
import { load, SAFES_KEY } from '~/utils/localStorage'
|
||||||
|
import updateSafes from '~/routes/safe/store/actions/updateSafes'
|
||||||
|
import { buildSafe } from '~/routes/safe/store/actions/fetchSafe'
|
||||||
|
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||||
|
|
||||||
|
const buildSafesFrom = async (loadedSafes: Object): Promise<Map<string, Safe>> => {
|
||||||
|
const safes = Map()
|
||||||
|
|
||||||
|
const keys = Object.keys(loadedSafes)
|
||||||
|
const safeRecords = await Promise.all(keys.map((address: string) => buildSafe(loadedSafes[address])))
|
||||||
|
|
||||||
|
return safes.withMutations(async (map) => {
|
||||||
|
safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
|
const storedSafes = load(SAFES_KEY)
|
||||||
|
const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map()
|
||||||
|
|
||||||
|
return dispatch(updateSafes(safes))
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
import { type DailyLimitProps } from '~/routes/safe/store/model/dailyLimit'
|
|
||||||
|
|
||||||
export const UPDATE_DAILY_LIMIT = 'UPDATE_DAILY_LIMIT'
|
|
||||||
|
|
||||||
type SpentTodayProps = {
|
|
||||||
safeAddress: string,
|
|
||||||
dailyLimit: DailyLimitProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateDailyLimit = createAction(
|
|
||||||
UPDATE_DAILY_LIMIT,
|
|
||||||
(safeAddress: string, dailyLimit: DailyLimitProps): SpentTodayProps => ({
|
|
||||||
safeAddress,
|
|
||||||
dailyLimit,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export default updateDailyLimit
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const UPDATE_SAFE = 'UPDATE_SAFE'
|
||||||
|
|
||||||
|
const updateSafe = createAction(UPDATE_SAFE)
|
||||||
|
|
||||||
|
export default updateSafe
|
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const UPDATE_SAFES = 'UPDATE_SAFES'
|
||||||
|
|
||||||
|
const updateSafesInBatch = createAction(UPDATE_SAFES)
|
||||||
|
|
||||||
|
export default updateSafesInBatch
|
|
@ -7,7 +7,7 @@ import type { Owner } from '~/routes/safe/store/model/owner'
|
||||||
export type SafeProps = {
|
export type SafeProps = {
|
||||||
name: string,
|
name: string,
|
||||||
address: string,
|
address: string,
|
||||||
confirmations: number,
|
threshold: number,
|
||||||
owners: List<Owner>,
|
owners: List<Owner>,
|
||||||
dailyLimit: DailyLimit,
|
dailyLimit: DailyLimit,
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export type SafeProps = {
|
||||||
export const makeSafe: RecordFactory<SafeProps> = Record({
|
export const makeSafe: RecordFactory<SafeProps> = Record({
|
||||||
name: '',
|
name: '',
|
||||||
address: '',
|
address: '',
|
||||||
confirmations: 0,
|
threshold: 0,
|
||||||
owners: List([]),
|
owners: List([]),
|
||||||
dailyLimit: makeDailyLimit(),
|
dailyLimit: makeDailyLimit(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,7 @@ export type TransactionProps = {
|
||||||
confirmations: List<Confirmation>,
|
confirmations: List<Confirmation>,
|
||||||
destination: string,
|
destination: string,
|
||||||
tx: string,
|
tx: string,
|
||||||
|
data: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||||
|
@ -21,6 +22,7 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||||
destination: '',
|
destination: '',
|
||||||
tx: '',
|
tx: '',
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
|
data: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Transaction = RecordOf<TransactionProps>
|
export type Transaction = RecordOf<TransactionProps>
|
||||||
|
|
|
@ -1,36 +1,16 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { Map, List } from 'immutable'
|
import { Map } from 'immutable'
|
||||||
import { handleActions, type ActionType } from 'redux-actions'
|
import { handleActions, type ActionType } from 'redux-actions'
|
||||||
import addSafe, { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
import addSafe, { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
||||||
import updateDailyLimit, { UPDATE_DAILY_LIMIT } from '~/routes/safe/store/actions/updateDailyLimit'
|
|
||||||
import { makeOwner } from '~/routes/safe/store/model/owner'
|
|
||||||
import { type Safe, makeSafe } from '~/routes/safe/store/model/safe'
|
import { type Safe, makeSafe } from '~/routes/safe/store/model/safe'
|
||||||
import { load, saveSafes, SAFES_KEY } from '~/utils/localStorage'
|
import { saveSafes, setOwners } from '~/utils/localStorage'
|
||||||
import { makeDailyLimit } from '~/routes/safe/store/model/dailyLimit'
|
import updateSafes, { UPDATE_SAFES } from '~/routes/safe/store/actions/updateSafes'
|
||||||
|
import updateSafe, { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
||||||
|
|
||||||
export const SAFE_REDUCER_ID = 'safes'
|
export const SAFE_REDUCER_ID = 'safes'
|
||||||
|
|
||||||
export type State = Map<string, Safe>
|
export type State = Map<string, Safe>
|
||||||
|
|
||||||
const buildSafesFrom = (loadedSafes: Object): State => {
|
|
||||||
const safes: State = Map()
|
|
||||||
|
|
||||||
return safes.withMutations((map: State) => {
|
|
||||||
Object.keys(loadedSafes).forEach((address) => {
|
|
||||||
const safe = loadedSafes[address]
|
|
||||||
safe.owners = List(safe.owners.map((owner => makeOwner(owner))))
|
|
||||||
safe.dailyLimit = makeDailyLimit({ value: safe.dailyLimit.value, spentToday: safe.dailyLimit.spentToday })
|
|
||||||
return map.set(address, makeSafe(safe))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const safeInitialState = (): State => {
|
|
||||||
const storedSafes = load(SAFES_KEY)
|
|
||||||
|
|
||||||
return storedSafes ? buildSafesFrom(storedSafes) : Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
type Action<T> = {
|
type Action<T> = {
|
||||||
key: string,
|
key: string,
|
||||||
|
@ -43,11 +23,16 @@ action: AddSafeType
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default handleActions({
|
export default handleActions({
|
||||||
|
[UPDATE_SAFE]: (state: State, action: ActionType<typeof updateSafe>): State =>
|
||||||
|
state.set(action.payload.get('address'), action.payload),
|
||||||
|
[UPDATE_SAFES]: (state: State, action: ActionType<typeof updateSafes>): State =>
|
||||||
|
action.payload,
|
||||||
[ADD_SAFE]: (state: State, action: ActionType<typeof addSafe>): State => {
|
[ADD_SAFE]: (state: State, action: ActionType<typeof addSafe>): State => {
|
||||||
const safes = state.set(action.payload.address, makeSafe(action.payload))
|
const safe: Safe = makeSafe(action.payload)
|
||||||
|
setOwners(safe.get('address'), safe.get('owners'))
|
||||||
|
|
||||||
|
const safes = state.set(action.payload.address, safe)
|
||||||
saveSafes(safes.toJSON())
|
saveSafes(safes.toJSON())
|
||||||
return safes
|
return safes
|
||||||
},
|
},
|
||||||
[UPDATE_DAILY_LIMIT]: (state: State, action: ActionType<typeof updateDailyLimit>): State =>
|
|
||||||
state.updateIn([action.payload.safeAddress, 'dailyLimit'], () => makeDailyLimit(action.payload.dailyLimit)),
|
|
||||||
}, Map())
|
}, Map())
|
||||||
|
|
|
@ -20,7 +20,7 @@ class SafeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
withConfirmations(confirmations: number) {
|
withConfirmations(confirmations: number) {
|
||||||
this.safe = this.safe.set('confirmations', confirmations)
|
this.safe = this.safe.set('threshold', confirmations)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
|
||||||
import fetchDailyLimit from '~/routes/safe/store/actions/fetchDailyLimit'
|
|
||||||
import { aNewStore } from '~/store'
|
|
||||||
import { addEtherTo } from '~/test/addEtherTo'
|
|
||||||
import { aDeployedSafe, executeWithdrawnOn } from './builder/deployedSafe.builder'
|
|
||||||
|
|
||||||
const updateDailyLimitReducerTests = () => {
|
|
||||||
describe('Safe Actions[updateDailyLimit]', () => {
|
|
||||||
let store
|
|
||||||
beforeEach(async () => {
|
|
||||||
store = aNewStore()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('reducer should return 0 as spentToday value from just deployed safe', async () => {
|
|
||||||
// GIVEN
|
|
||||||
const dailyLimitValue = 0.5
|
|
||||||
const safeAddress = await aDeployedSafe(store, 0.5)
|
|
||||||
// WHEN
|
|
||||||
await store.dispatch(fetchDailyLimit(safeAddress))
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
const safes = store.getState()[SAFE_REDUCER_ID]
|
|
||||||
const dailyLimit = safes.get(safeAddress).get('dailyLimit')
|
|
||||||
expect(dailyLimit).not.toBe(undefined)
|
|
||||||
expect(dailyLimit.value).toBe(dailyLimitValue)
|
|
||||||
expect(dailyLimit.spentToday).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('reducer should return 0.1456 ETH as spentToday if the user has withdrawn 0.1456 from MAX of 0.3 ETH', async () => {
|
|
||||||
// GIVEN
|
|
||||||
const dailyLimitValue = 0.3
|
|
||||||
const safeAddress = await aDeployedSafe(store, dailyLimitValue)
|
|
||||||
await addEtherTo(safeAddress, '0.5')
|
|
||||||
const value = 0.1456
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
await executeWithdrawnOn(safeAddress, value)
|
|
||||||
await store.dispatch(fetchDailyLimit(safeAddress))
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
const safes = store.getState()[SAFE_REDUCER_ID]
|
|
||||||
const dailyLimit = safes.get(safeAddress).get('dailyLimit')
|
|
||||||
expect(dailyLimit).not.toBe(undefined)
|
|
||||||
expect(dailyLimit.value).toBe(dailyLimitValue)
|
|
||||||
expect(dailyLimit.spentToday).toBe(value)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateDailyLimitReducerTests
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
|
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
|
||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
import safeReducer, { safeInitialState, SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
import safeReducer, { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
||||||
import addSafe from '~/routes/safe/store/actions/addSafe'
|
import addSafe from '~/routes/safe/store/actions/addSafe'
|
||||||
import * as SafeFields from '~/routes/open/components/fields'
|
import * as SafeFields from '~/routes/open/components/fields'
|
||||||
import { getAccountsFrom, getNamesFrom } from '~/routes/open/utils/safeDataExtractor'
|
import { getAccountsFrom, getNamesFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||||
|
@ -56,26 +56,6 @@ const providerReducerTests = () => {
|
||||||
// THEN
|
// THEN
|
||||||
expect(safes.get(address)).toEqual(SafeFactory.oneOwnerSafe())
|
expect(safes.get(address)).toEqual(SafeFactory.oneOwnerSafe())
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reducer loads information from localStorage', async () => {
|
|
||||||
// GIVEN in beforeEach method
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
store.dispatch(addSafe(
|
|
||||||
formValues[SafeFields.FIELD_NAME],
|
|
||||||
formValues.address,
|
|
||||||
formValues[SafeFields.FIELD_CONFIRMATIONS],
|
|
||||||
formValues[SafeFields.FIELD_DAILY_LIMIT],
|
|
||||||
getNamesFrom(formValues),
|
|
||||||
getAccountsFrom(formValues),
|
|
||||||
))
|
|
||||||
|
|
||||||
const anotherStore = aStore({ [SAFE_REDUCER_ID]: safeInitialState() })
|
|
||||||
const safes = anotherStore.getState()[SAFE_REDUCER_ID]
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
expect(safeInitialState()).toEqual(safes)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import balanceReducerTests from './balance.reducer'
|
import balanceReducerTests from './balance.reducer'
|
||||||
import safeReducerTests from './safe.reducer'
|
import safeReducerTests from './safe.reducer'
|
||||||
import dailyLimitReducerTests from './dailyLimit.reducer'
|
|
||||||
import balanceSelectorTests from './balance.selector'
|
import balanceSelectorTests from './balance.selector'
|
||||||
import safeSelectorTests from './safe.selector'
|
import safeSelectorTests from './safe.selector'
|
||||||
import grantedSelectorTests from './granted.selector'
|
import grantedSelectorTests from './granted.selector'
|
||||||
|
@ -12,7 +11,6 @@ describe('Safe Test suite', () => {
|
||||||
// ACTIONS AND REDUCERS
|
// ACTIONS AND REDUCERS
|
||||||
safeReducerTests()
|
safeReducerTests()
|
||||||
balanceReducerTests()
|
balanceReducerTests()
|
||||||
dailyLimitReducerTests()
|
|
||||||
|
|
||||||
// SAFE SELECTOR
|
// SAFE SELECTOR
|
||||||
safeSelectorTests()
|
safeSelectorTests()
|
||||||
|
|
|
@ -45,7 +45,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||||
const addTxButton = buttons[1]
|
const addTxButton = buttons[3]
|
||||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||||
await sleep(1800) // Give time to enable Add button
|
await sleep(1800) // Give time to enable Add button
|
||||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(addTxButton, 'button')[0])
|
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(addTxButton, 'button')[0])
|
||||||
|
|
|
@ -53,6 +53,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 1 thresh
|
||||||
|
|
||||||
const confirmed = paragraphs[3].innerHTML
|
const confirmed = paragraphs[3].innerHTML
|
||||||
const tx = getTransactionFromReduxStore(store, address)
|
const tx = getTransactionFromReduxStore(store, address)
|
||||||
|
if (!tx) throw new Error()
|
||||||
expect(confirmed).toBe(tx.get('tx'))
|
expect(confirmed).toBe(tx.get('tx'))
|
||||||
|
|
||||||
const ownerTx = paragraphs[6].innerHTML
|
const ownerTx = paragraphs[6].innerHTML
|
||||||
|
|
|
@ -45,6 +45,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 3 thresh
|
||||||
|
|
||||||
const getAlreadyConfirmed = () => {
|
const getAlreadyConfirmed = () => {
|
||||||
const tx = getTransactionFromReduxStore(store, address)
|
const tx = getTransactionFromReduxStore(store, address)
|
||||||
|
if (!tx) throw new Error()
|
||||||
const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx })
|
const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx })
|
||||||
|
|
||||||
return confirmed
|
return confirmed
|
||||||
|
@ -53,6 +54,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 3 thresh
|
||||||
const makeConfirmation = async (executor) => {
|
const makeConfirmation = async (executor) => {
|
||||||
const alreadyConfirmed = getAlreadyConfirmed()
|
const alreadyConfirmed = getAlreadyConfirmed()
|
||||||
const tx = getTransactionFromReduxStore(store, address)
|
const tx = getTransactionFromReduxStore(store, address)
|
||||||
|
if (!tx) throw new Error()
|
||||||
await processTransaction(address, tx, alreadyConfirmed, executor)
|
await processTransaction(address, tx, alreadyConfirmed, executor)
|
||||||
await sleep(800)
|
await sleep(800)
|
||||||
store.dispatch(fetchTransactions())
|
store.dispatch(fetchTransactions())
|
||||||
|
@ -96,6 +98,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 3 thresh
|
||||||
|
|
||||||
const confirmedExecuted = paragraphsExecuted[3].innerHTML
|
const confirmedExecuted = paragraphsExecuted[3].innerHTML
|
||||||
const tx = getTransactionFromReduxStore(store, address)
|
const tx = getTransactionFromReduxStore(store, address)
|
||||||
|
if (!tx) throw new Error()
|
||||||
expect(confirmedExecuted).toBe(tx.get('tx'))
|
expect(confirmedExecuted).toBe(tx.get('tx'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
// @flow
|
||||||
|
import { aNewStore } from '~/store'
|
||||||
|
import { aDeployedSafe } from '~/routes/safe/store/test/builder/deployedSafe.builder'
|
||||||
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
|
import { sleep } from '~/utils/timer'
|
||||||
|
import { type Match } from 'react-router-dom'
|
||||||
|
import { promisify } from '~/utils/promisify'
|
||||||
|
import { processTransaction } from '~/routes/safe/component/Transactions/processTransactions'
|
||||||
|
import { confirmationsTransactionSelector, safeSelector, safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||||
|
import { getTransactionFromReduxStore } from '~/routes/safe/test/testMultisig'
|
||||||
|
import { buildMathPropsFrom } from '~/test/buildReactRouterProps'
|
||||||
|
import { createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||||
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
|
import { type GlobalState } from '~/store/index'
|
||||||
|
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/model/transaction'
|
||||||
|
import { getGnosisSafeInstanceAt } from '~/wallets/safeContracts'
|
||||||
|
|
||||||
|
const getSafeFrom = (state: GlobalState, safeAddress: string): Safe => {
|
||||||
|
const match: Match = buildMathPropsFrom(safeAddress)
|
||||||
|
const safe = safeSelector(state, { match })
|
||||||
|
if (!safe) throw new Error()
|
||||||
|
|
||||||
|
return safe
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('React DOM TESTS > Add and remove owners', () => {
|
||||||
|
const assureExecuted = (transaction: Transaction) => {
|
||||||
|
expect(transaction.get('tx')).not.toBe(null)
|
||||||
|
expect(transaction.get('tx')).not.toBe(undefined)
|
||||||
|
expect(transaction.get('tx')).not.toBe('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const assureThresholdIs = async (gnosisSafe, threshold: number) => {
|
||||||
|
const safeThreshold = await gnosisSafe.getThreshold()
|
||||||
|
expect(Number(safeThreshold)).toEqual(threshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
const assureOwnersAre = async (gnosisSafe, ...owners) => {
|
||||||
|
const safeOwners = await gnosisSafe.getOwners()
|
||||||
|
expect(safeOwners.length).toEqual(owners.length)
|
||||||
|
for (let i = 0; i < owners.length; i += 1) {
|
||||||
|
expect(safeOwners[i]).toBe(owners[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('adds owner without increasing the threshold', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const numOwners = 2
|
||||||
|
const threshold = 1
|
||||||
|
const store = aNewStore()
|
||||||
|
const address = await aDeployedSafe(store, 10, threshold, numOwners)
|
||||||
|
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||||
|
const safe = getSafeFrom(store.getState(), address)
|
||||||
|
const gnosisSafe = await getGnosisSafeInstanceAt(address)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
await assureThresholdIs(gnosisSafe, 1)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
|
||||||
|
const nonce = Date.now()
|
||||||
|
const accountIndex = 5
|
||||||
|
const data = gnosisSafe.contract.addOwnerWithThreshold.getData(accounts[accountIndex], 1)
|
||||||
|
await createTransaction(safe, `Add Owner with index ${accountIndex}`, address, 0, nonce, accounts[0], data)
|
||||||
|
await sleep(1500)
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||||
|
expect(transactions.count()).toBe(1)
|
||||||
|
const tx = transactions.get(0)
|
||||||
|
if (!tx) throw new Error()
|
||||||
|
assureExecuted(tx)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[5], accounts[0], accounts[1])
|
||||||
|
await assureThresholdIs(gnosisSafe, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds owner increasing the threshold', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const numOwners = 2
|
||||||
|
const threshold = 1
|
||||||
|
const store = aNewStore()
|
||||||
|
const address = await aDeployedSafe(store, 10, threshold, numOwners)
|
||||||
|
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||||
|
const safe = getSafeFrom(store.getState(), address)
|
||||||
|
const gnosisSafe = await getGnosisSafeInstanceAt(address)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
await assureThresholdIs(gnosisSafe, 1)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
|
||||||
|
const nonce = Date.now()
|
||||||
|
const accountIndex = 5
|
||||||
|
const data = gnosisSafe.contract.addOwnerWithThreshold.getData(accounts[accountIndex], 2)
|
||||||
|
await createTransaction(safe, `Add Owner with index ${accountIndex}`, address, 0, nonce, accounts[0], data)
|
||||||
|
await sleep(1500)
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||||
|
expect(transactions.count()).toBe(1)
|
||||||
|
const tx = transactions.get(0)
|
||||||
|
if (!tx) throw new Error()
|
||||||
|
assureExecuted(tx)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[accountIndex], accounts[0], accounts[1])
|
||||||
|
await assureThresholdIs(gnosisSafe, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
const processOwnerModification = async (store, safeAddress, executor) => {
|
||||||
|
const tx = getTransactionFromReduxStore(store, safeAddress)
|
||||||
|
if (!tx) throw new Error()
|
||||||
|
const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx })
|
||||||
|
const data = tx.get('data')
|
||||||
|
expect(data).not.toBe(null)
|
||||||
|
expect(data).not.toBe(undefined)
|
||||||
|
expect(data).not.toBe('')
|
||||||
|
|
||||||
|
await processTransaction(safeAddress, tx, confirmed, executor)
|
||||||
|
await sleep(1800)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('remove owner without decreasing the threshold', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const numOwners = 3
|
||||||
|
const threshold = 2
|
||||||
|
const store = aNewStore()
|
||||||
|
const address = await aDeployedSafe(store, 10, threshold, numOwners)
|
||||||
|
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||||
|
const safe = getSafeFrom(store.getState(), address)
|
||||||
|
const gnosisSafe = await getGnosisSafeInstanceAt(address)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
await assureThresholdIs(gnosisSafe, 2)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1], accounts[2])
|
||||||
|
const nonce = Date.now()
|
||||||
|
const accountIndex = 2
|
||||||
|
const data = gnosisSafe.contract.removeOwner.getData(accounts[accountIndex - 1], accounts[accountIndex], 2)
|
||||||
|
await createTransaction(safe, `Remove owner Address 3 ${nonce}`, address, 0, nonce, accounts[0], data)
|
||||||
|
await sleep(1500)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1], accounts[2])
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
|
||||||
|
|
||||||
|
processOwnerModification(store, address, accounts[1])
|
||||||
|
await sleep(3000)
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
await sleep(3000)
|
||||||
|
const tx = getTransactionFromReduxStore(store, address)
|
||||||
|
if (!tx) throw new Error()
|
||||||
|
const txHash = tx.get('tx')
|
||||||
|
expect(txHash).not.toBe('')
|
||||||
|
await assureThresholdIs(gnosisSafe, 2)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('remove owner decreasing the threshold', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const numOwners = 2
|
||||||
|
const threshold = 2
|
||||||
|
const store = aNewStore()
|
||||||
|
const address = await aDeployedSafe(store, 10, threshold, numOwners)
|
||||||
|
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||||
|
const safe = getSafeFrom(store.getState(), address)
|
||||||
|
const gnosisSafe = await getGnosisSafeInstanceAt(address)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
await assureThresholdIs(gnosisSafe, 2)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
|
||||||
|
const nonce = Date.now()
|
||||||
|
const accountIndex = 1
|
||||||
|
const data = gnosisSafe.contract.removeOwner.getData(accounts[accountIndex - 1], accounts[accountIndex], 1)
|
||||||
|
await createTransaction(safe, `Remove owner Address 2 ${nonce}`, address, 0, nonce, accounts[0], data)
|
||||||
|
await sleep(1500)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
|
||||||
|
|
||||||
|
processOwnerModification(store, address, accounts[1])
|
||||||
|
await sleep(3000)
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
await sleep(3000)
|
||||||
|
const tx = getTransactionFromReduxStore(store, address)
|
||||||
|
if (!tx) throw new Error()
|
||||||
|
const txHash = tx.get('tx')
|
||||||
|
expect(txHash).not.toBe('')
|
||||||
|
await assureThresholdIs(gnosisSafe, 1)
|
||||||
|
await assureOwnersAre(gnosisSafe, accounts[0])
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,125 @@
|
||||||
|
// @flow
|
||||||
|
import { aNewStore } from '~/store'
|
||||||
|
import { aDeployedSafe } from '~/routes/safe/store/test/builder/deployedSafe.builder'
|
||||||
|
import { getWeb3 } from '~/wallets/getWeb3'
|
||||||
|
import { sleep } from '~/utils/timer'
|
||||||
|
import { type Match } from 'react-router-dom'
|
||||||
|
import { promisify } from '~/utils/promisify'
|
||||||
|
import { processTransaction } from '~/routes/safe/component/Transactions/processTransactions'
|
||||||
|
import { confirmationsTransactionSelector, safeSelector, safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||||
|
import { getTransactionFromReduxStore } from '~/routes/safe/test/testMultisig'
|
||||||
|
import { buildMathPropsFrom } from '~/test/buildReactRouterProps'
|
||||||
|
import { createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||||
|
import { getGnosisSafeContract } from '~/wallets/safeContracts'
|
||||||
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
|
|
||||||
|
describe('React DOM TESTS > Change threshold', () => {
|
||||||
|
it('should update the threshold directly if safe has 1 threshold', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const numOwners = 2
|
||||||
|
const threshold = 1
|
||||||
|
const store = aNewStore()
|
||||||
|
const address = await aDeployedSafe(store, 10, threshold, numOwners)
|
||||||
|
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||||
|
const match: Match = buildMathPropsFrom(address)
|
||||||
|
const safe = safeSelector(store.getState(), { match })
|
||||||
|
if (!safe) throw new Error()
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||||
|
const gnosisSafe = GnosisSafe.at(address)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
const nonce = Date.now()
|
||||||
|
const data = gnosisSafe.contract.changeThreshold.getData(2)
|
||||||
|
await createTransaction(safe, "Change Safe's threshold", address, 0, nonce, accounts[0], data)
|
||||||
|
await sleep(1500)
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||||
|
expect(transactions.count()).toBe(1)
|
||||||
|
|
||||||
|
const thresholdTx = transactions.get(0)
|
||||||
|
if (!thresholdTx) throw new Error()
|
||||||
|
expect(thresholdTx.get('tx')).not.toBe(null)
|
||||||
|
expect(thresholdTx.get('tx')).not.toBe(undefined)
|
||||||
|
expect(thresholdTx.get('tx')).not.toBe('')
|
||||||
|
|
||||||
|
const safeThreshold = await gnosisSafe.getThreshold()
|
||||||
|
expect(Number(safeThreshold)).toEqual(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
const changeThreshold = async (store, safeAddress, executor) => {
|
||||||
|
const tx = getTransactionFromReduxStore(store, safeAddress)
|
||||||
|
if (!tx) throw new Error()
|
||||||
|
const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx })
|
||||||
|
const data = tx.get('data')
|
||||||
|
expect(data).not.toBe(null)
|
||||||
|
expect(data).not.toBe(undefined)
|
||||||
|
expect(data).not.toBe('')
|
||||||
|
await processTransaction(safeAddress, tx, confirmed, executor)
|
||||||
|
await sleep(1800)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should wait for confirmation to update threshold when safe has 1+ threshold', async () => {
|
||||||
|
// GIVEN
|
||||||
|
const numOwners = 3
|
||||||
|
const threshold = 2
|
||||||
|
const store = aNewStore()
|
||||||
|
const address = await aDeployedSafe(store, 10, threshold, numOwners)
|
||||||
|
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||||
|
const match: Match = buildMathPropsFrom(address)
|
||||||
|
const safe = safeSelector(store.getState(), { match })
|
||||||
|
if (!safe) throw new Error()
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||||
|
const gnosisSafe = GnosisSafe.at(address)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
const nonce = Date.now()
|
||||||
|
const data = gnosisSafe.contract.changeThreshold.getData(3)
|
||||||
|
await createTransaction(safe, "Change Safe's threshold", address, 0, nonce, accounts[0], data)
|
||||||
|
await sleep(1500)
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
|
||||||
|
let transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||||
|
if (!transactions) throw new Error()
|
||||||
|
expect(transactions.count()).toBe(1)
|
||||||
|
|
||||||
|
let thresholdTx = transactions.get(0)
|
||||||
|
if (!thresholdTx) throw new Error()
|
||||||
|
expect(thresholdTx.get('tx')).toBe('')
|
||||||
|
let firstOwnerConfirmation = thresholdTx.get('confirmations').get(0)
|
||||||
|
if (!firstOwnerConfirmation) throw new Error()
|
||||||
|
expect(firstOwnerConfirmation.get('status')).toBe(true)
|
||||||
|
let secondOwnerConfirmation = thresholdTx.get('confirmations').get(1)
|
||||||
|
if (!secondOwnerConfirmation) throw new Error()
|
||||||
|
expect(secondOwnerConfirmation.get('status')).toBe(false)
|
||||||
|
|
||||||
|
let safeThreshold = await gnosisSafe.getThreshold()
|
||||||
|
expect(Number(safeThreshold)).toEqual(2)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await changeThreshold(store, address, accounts[1])
|
||||||
|
safeThreshold = await gnosisSafe.getThreshold()
|
||||||
|
expect(Number(safeThreshold)).toEqual(3)
|
||||||
|
|
||||||
|
await store.dispatch(fetchTransactions())
|
||||||
|
sleep(1200)
|
||||||
|
transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||||
|
expect(transactions.count()).toBe(1)
|
||||||
|
|
||||||
|
thresholdTx = transactions.get(0)
|
||||||
|
if (!thresholdTx) throw new Error()
|
||||||
|
expect(thresholdTx.get('tx')).not.toBe(undefined)
|
||||||
|
expect(thresholdTx.get('tx')).not.toBe(null)
|
||||||
|
expect(thresholdTx.get('tx')).not.toBe('')
|
||||||
|
|
||||||
|
firstOwnerConfirmation = thresholdTx.get('confirmations').get(0)
|
||||||
|
if (!firstOwnerConfirmation) throw new Error()
|
||||||
|
expect(firstOwnerConfirmation.get('status')).toBe(true)
|
||||||
|
secondOwnerConfirmation = thresholdTx.get('confirmations').get(1)
|
||||||
|
if (!secondOwnerConfirmation) throw new Error()
|
||||||
|
expect(secondOwnerConfirmation.get('status')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -46,7 +46,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||||
const withdrawnButton = buttons[0]
|
const withdrawnButton = buttons[2]
|
||||||
expect(withdrawnButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
|
expect(withdrawnButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
|
||||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(withdrawnButton, 'button')[0])
|
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(withdrawnButton, 'button')[0])
|
||||||
await sleep(4000)
|
await sleep(4000)
|
||||||
|
@ -96,7 +96,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
||||||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||||
const addTxButton = buttons[1]
|
const addTxButton = buttons[3]
|
||||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||||
expect(addTxButton.props.disabled).toBe(true)
|
expect(addTxButton.props.disabled).toBe(true)
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
||||||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||||
const addTxButton = buttons[0]
|
const addTxButton = buttons[2]
|
||||||
expect(addTxButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
|
expect(addTxButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
|
||||||
expect(addTxButton.props.disabled).toBe(true)
|
expect(addTxButton.props.disabled).toBe(true)
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,13 @@ import SafeView from '~/routes/safe/component/Safe'
|
||||||
import TransactionsComponent from '~/routes/safe/component/Transactions'
|
import TransactionsComponent from '~/routes/safe/component/Transactions'
|
||||||
import TransactionComponent from '~/routes/safe/component/Transactions/Transaction'
|
import TransactionComponent from '~/routes/safe/component/Transactions/Transaction'
|
||||||
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||||
|
import { type GlobalState } from '~/store/index'
|
||||||
|
|
||||||
export const createMultisigTxFilling = async (SafeDom, AddTransactionComponent, store) => {
|
export const createMultisigTxFilling = async (
|
||||||
|
SafeDom: React$Component<any, any>,
|
||||||
|
AddTransactionComponent: React$ElementType,
|
||||||
|
store: Store<GlobalState>,
|
||||||
|
) => {
|
||||||
// Get AddTransaction form component
|
// Get AddTransaction form component
|
||||||
const AddTransaction = TestUtils.findRenderedComponentWithType(SafeDom, AddTransactionComponent)
|
const AddTransaction = TestUtils.findRenderedComponentWithType(SafeDom, AddTransactionComponent)
|
||||||
|
|
||||||
|
@ -36,30 +41,30 @@ export const checkBalanceOf = async (addressToTest: string, value: string) => {
|
||||||
expect(safeBalance).toBe(value)
|
expect(safeBalance).toBe(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addFundsTo = async (SafeDom, destination: string) => {
|
export const addFundsTo = async (SafeDom: React$Component<any, any>, destination: string) => {
|
||||||
// add funds to safe
|
// add funds to safe
|
||||||
await addEtherTo(destination, '0.1')
|
await addEtherTo(destination, '0.1')
|
||||||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||||
const addTxButton = buttons[1]
|
const addTxButton = buttons[3]
|
||||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||||
await sleep(1800) // Give time to enable Add button
|
await sleep(1800) // Give time to enable Add button
|
||||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(addTxButton, 'button')[0])
|
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(addTxButton, 'button')[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listTxsOf = (SafeDom) => {
|
export const listTxsOf = (SafeDom: React$Component<any, any>) => {
|
||||||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||||
const seeTx = buttons[2]
|
const seeTx = buttons[4]
|
||||||
expect(seeTx.props.children).toEqual(SEE_MULTISIG_BUTTON_TEXT)
|
expect(seeTx.props.children).toEqual(SEE_MULTISIG_BUTTON_TEXT)
|
||||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(seeTx, 'button')[0])
|
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(seeTx, 'button')[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTagFromTransaction = (SafeDom, tag: string) => {
|
export const getTagFromTransaction = (SafeDom: React$Component<any, any>, tag: string) => {
|
||||||
const Transactions = TestUtils.findRenderedComponentWithType(SafeDom, TransactionsComponent)
|
const Transactions = TestUtils.findRenderedComponentWithType(SafeDom, TransactionsComponent)
|
||||||
if (!Transactions) throw new Error()
|
if (!Transactions) throw new Error()
|
||||||
const Transaction = TestUtils.findRenderedComponentWithType(Transactions, TransactionComponent)
|
const Transaction = TestUtils.findRenderedComponentWithType(Transactions, TransactionComponent)
|
||||||
|
@ -68,7 +73,11 @@ export const getTagFromTransaction = (SafeDom, tag: string) => {
|
||||||
return TestUtils.scryRenderedDOMComponentsWithTag(Transaction, tag)
|
return TestUtils.scryRenderedDOMComponentsWithTag(Transaction, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const expandTransactionOf = async (SafeDom, numOwners, safeThreshold) => {
|
export const expandTransactionOf = async (
|
||||||
|
SafeDom: React$Component<any, any>,
|
||||||
|
numOwners: number,
|
||||||
|
safeThreshold: number,
|
||||||
|
) => {
|
||||||
const paragraphs = getTagFromTransaction(SafeDom, 'p')
|
const paragraphs = getTagFromTransaction(SafeDom, 'p')
|
||||||
TestUtils.Simulate.click(paragraphs[2]) // expanded
|
TestUtils.Simulate.click(paragraphs[2]) // expanded
|
||||||
await sleep(1000) // Time to expand
|
await sleep(1000) // Time to expand
|
||||||
|
@ -80,13 +89,13 @@ export const expandTransactionOf = async (SafeDom, numOwners, safeThreshold) =>
|
||||||
expect(paragraphsExpanded.length).toBe(paragraphs.length + numOwners)
|
expect(paragraphsExpanded.length).toBe(paragraphs.length + numOwners)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTransactionFromReduxStore = (store, address) => {
|
export const getTransactionFromReduxStore = (store: Store<GlobalState>, address: string, index: number = 0) => {
|
||||||
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||||
|
|
||||||
return transactions.get(0)
|
return transactions.get(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const confirmOwners = async (SafeDom, ...statusses: string[]) => {
|
export const confirmOwners = async (SafeDom: React$Component<any, any>, ...statusses: string[]) => {
|
||||||
const paragraphsWithOwners = getTagFromTransaction(SafeDom, 'h3')
|
const paragraphsWithOwners = getTagFromTransaction(SafeDom, 'h3')
|
||||||
for (let i = 0; i < statusses.length; i += 1) {
|
for (let i = 0; i < statusses.length; i += 1) {
|
||||||
const ownerIndex = i + 6
|
const ownerIndex = i + 6
|
||||||
|
|
|
@ -32,7 +32,7 @@ const SafeTable = ({ safes }: Props) => (
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell padding="none">{safe.get('name')}</TableCell>
|
<TableCell padding="none">{safe.get('name')}</TableCell>
|
||||||
<TableCell padding="none">{safe.get('address')}</TableCell>
|
<TableCell padding="none">{safe.get('address')}</TableCell>
|
||||||
<TableCell padding="none" numeric>{safe.get('confirmations')}</TableCell>
|
<TableCell padding="none" numeric>{safe.get('threshold')}</TableCell>
|
||||||
<TableCell padding="none" numeric>{safe.get('owners').count()}</TableCell>
|
<TableCell padding="none" numeric>{safe.get('owners').count()}</TableCell>
|
||||||
<TableCell padding="none" numeric>{`${safe.get('dailyLimit').get('value')} ETH`}</TableCell>
|
<TableCell padding="none" numeric>{`${safe.get('dailyLimit').get('value')} ETH`}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { routerMiddleware, routerReducer } from 'react-router-redux'
|
||||||
import { combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store } from 'redux'
|
import { combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store } from 'redux'
|
||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/wallets/store/reducer/provider'
|
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/wallets/store/reducer/provider'
|
||||||
import safe, { SAFE_REDUCER_ID, safeInitialState, type State as SafeState } from '~/routes/safe/store/reducer/safe'
|
import safe, { SAFE_REDUCER_ID, type State as SafeState } from '~/routes/safe/store/reducer/safe'
|
||||||
import balances, { BALANCE_REDUCER_ID, type State as BalancesState } from '~/routes/safe/store/reducer/balances'
|
import balances, { BALANCE_REDUCER_ID, type State as BalancesState } from '~/routes/safe/store/reducer/balances'
|
||||||
import transactions, { type State as TransactionsState, transactionsInitialState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
|
import transactions, { type State as TransactionsState, transactionsInitialState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ const reducers: Reducer<GlobalState> = combineReducers({
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
[SAFE_REDUCER_ID]: safeInitialState(),
|
|
||||||
[TRANSACTIONS_REDUCER_ID]: transactionsInitialState(),
|
[TRANSACTIONS_REDUCER_ID]: transactionsInitialState(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { List, Map } from 'immutable'
|
||||||
|
import { type Owner } from '~/routes/safe/store/model/owner'
|
||||||
|
|
||||||
export const SAFES_KEY = 'SAFES'
|
export const SAFES_KEY = 'SAFES'
|
||||||
export const TX_KEY = 'TX'
|
export const TX_KEY = 'TX'
|
||||||
|
export const OWNERS_KEY = 'OWNERS'
|
||||||
|
|
||||||
export const load = (key: string) => {
|
export const load = (key: string) => {
|
||||||
try {
|
try {
|
||||||
|
@ -27,3 +31,19 @@ export const saveSafes = (safes: Object) => {
|
||||||
// Ignore write errors
|
// Ignore write errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const setOwners = (safeAddress: string, owners: List<Owner>) => {
|
||||||
|
try {
|
||||||
|
const ownersAsMap = Map(owners.map((owner: Owner) => [owner.get('address').toLowerCase(), owner.get('name')]))
|
||||||
|
const serializedState = JSON.stringify(ownersAsMap)
|
||||||
|
localStorage.setItem(`${OWNERS_KEY}-${safeAddress}`, serializedState)
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore write errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOwners = (safeAddress: string): Map<string, string> => {
|
||||||
|
const data = load(`${OWNERS_KEY}-${safeAddress}`)
|
||||||
|
|
||||||
|
return data ? Map(data) : Map()
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { ProviderProps } from '~/wallets/store/model/provider'
|
||||||
import { promisify } from '~/utils/promisify'
|
import { promisify } from '~/utils/promisify'
|
||||||
|
|
||||||
let web3
|
let web3
|
||||||
export const getWeb3 = () => web3
|
export const getWeb3 = () => web3 || new Web3(window.web3.currentProvider)
|
||||||
|
|
||||||
const isMetamask: Function = (web3Provider): boolean => {
|
const isMetamask: Function = (web3Provider): boolean => {
|
||||||
const isMetamaskConstructor = web3Provider.currentProvider.constructor.name === 'MetamaskInpageProvider'
|
const isMetamaskConstructor = web3Provider.currentProvider.constructor.name === 'MetamaskInpageProvider'
|
||||||
|
|
|
@ -136,3 +136,11 @@ export const deploySafeContract = async (
|
||||||
|
|
||||||
return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, { from: userAccount, gas, gasPrice })
|
return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, { from: userAccount, gas, gasPrice })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getGnosisSafeInstanceAt = async (safeAddress: string) => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||||
|
const gnosisSafe = GnosisSafe.at(safeAddress)
|
||||||
|
|
||||||
|
return gnosisSafe
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue