Merge pull request #38 from gnosis/feature/WA-234-add-owners
WA-234 - Add owners to a safe
This commit is contained in:
commit
f2421e5575
|
@ -1276,8 +1276,14 @@
|
|||
"links": {},
|
||||
"address": "0x5fd674bc2873513f8e5a19d69637d0211e476380",
|
||||
"transactionHash": "0x288775644c087eed5e41b96fecebdb23be4e6d40bef5b6fb9a2876c2a3145157"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0x194371bd036c34314ded31a7ffe7e66f5461a62d",
|
||||
"transactionHash": "0x288775644c087eed5e41b96fecebdb23be4e6d40bef5b6fb9a2876c2a3145157"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.138Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.695Z"
|
||||
}
|
|
@ -6699,8 +6699,14 @@
|
|||
"links": {},
|
||||
"address": "0x3bdceb07fddd50d259a059ca9a75ecda561d4afc",
|
||||
"transactionHash": "0xf501438a4ec967e2928d922e4af568a2a5365002f8b3f9e32117bbacfaa49331"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0x0fb244fb862c95c66250c6c72e7e91a3d41a47d5",
|
||||
"transactionHash": "0xf501438a4ec967e2928d922e4af568a2a5365002f8b3f9e32117bbacfaa49331"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.129Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.688Z"
|
||||
}
|
|
@ -9864,8 +9864,14 @@
|
|||
"links": {},
|
||||
"address": "0x8c55b458a53e8c6e9efa7f54e7be9ca76b43dd9b",
|
||||
"transactionHash": "0x67117c1452ee2f4b904621b6f30790ff998d1f1a72f11c6b71ef47e3dd254724"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0xc3d42147f9b51c9749d3362e8b9ee6cb94861778",
|
||||
"transactionHash": "0x67117c1452ee2f4b904621b6f30790ff998d1f1a72f11c6b71ef47e3dd254724"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.124Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.672Z"
|
||||
}
|
|
@ -6876,8 +6876,14 @@
|
|||
"links": {},
|
||||
"address": "0xd4edae2f2d5718d1798deb48c062b939d6e9d4f4",
|
||||
"transactionHash": "0xa71d3b0b3752acc18733fa881f70c256d63562f28ccca9af910fad3beee9181a"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0xf2bc99498b610a01d76358d8e2fe251c9783a216",
|
||||
"transactionHash": "0xa71d3b0b3752acc18733fa881f70c256d63562f28ccca9af910fad3beee9181a"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.114Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.677Z"
|
||||
}
|
|
@ -1398,8 +1398,14 @@
|
|||
"links": {},
|
||||
"address": "0x2ebea54cbbd4f5491deba7a37605f8f0be3e3c9b",
|
||||
"transactionHash": "0xb6a19a7a679a1474c09c651e4151421f210afa3f47effed019d4c0206144ee5f"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0x4aa39923aa66871debec9b8aa9008a3b220eb1df",
|
||||
"transactionHash": "0xb6a19a7a679a1474c09c651e4151421f210afa3f47effed019d4c0206144ee5f"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.139Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.697Z"
|
||||
}
|
|
@ -360,8 +360,14 @@
|
|||
"links": {},
|
||||
"address": "0x3946fcaaa0ba21aaffc5e06a3cc45debc9e07f7f",
|
||||
"transactionHash": "0xd044f1662e339061a8cabf2b06ac94a9f86fcccf3f5d80ebd1bea2a7542d4021"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0xef9e7829f057e4d640bf66e17e06b3ab5cae508d",
|
||||
"transactionHash": "0xd044f1662e339061a8cabf2b06ac94a9f86fcccf3f5d80ebd1bea2a7542d4021"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.139Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.696Z"
|
||||
}
|
|
@ -1011,8 +1011,14 @@
|
|||
"links": {},
|
||||
"address": "0xbefd9f4a40b1bec8ec730969a3508d1739fb2742",
|
||||
"transactionHash": "0x75ad1066b44cd801ac66a316dbe4c09e72636d72b70fd62eb647295a0fc5e285"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0xaa973df8ec251cf67ec387d5627d42dbb738605f",
|
||||
"transactionHash": "0x75ad1066b44cd801ac66a316dbe4c09e72636d72b70fd62eb647295a0fc5e285"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.112Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.668Z"
|
||||
}
|
|
@ -7310,8 +7310,14 @@
|
|||
"links": {},
|
||||
"address": "0xfb1771240bb7edf209c70bd520a5d5424d23b084",
|
||||
"transactionHash": "0xf0cd95843453bdac02ad8018ef507479ea62989e56d69ad0ac1aad9d3a8515d2"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0x9be4b89520d1dd6f2d115192587689c6c9bd1a99",
|
||||
"transactionHash": "0xf0cd95843453bdac02ad8018ef507479ea62989e56d69ad0ac1aad9d3a8515d2"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.135Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.692Z"
|
||||
}
|
|
@ -5881,8 +5881,14 @@
|
|||
"links": {},
|
||||
"address": "0x589fd9eea7cca488a80e17a5105befff9616f11d",
|
||||
"transactionHash": "0x0396e1c9da4fa7bd313286e6033446dbb6e491f267956f8cf13202ce534fd0e6"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0xe8abcdb37db8e7c563de32c5d207433ce44d1445",
|
||||
"transactionHash": "0x0396e1c9da4fa7bd313286e6033446dbb6e491f267956f8cf13202ce534fd0e6"
|
||||
}
|
||||
},
|
||||
"schemaVersion": "2.0.0",
|
||||
"updatedAt": "2018-06-04T10:56:37.118Z"
|
||||
"updatedAt": "2018-06-06T14:51:43.684Z"
|
||||
}
|
|
@ -4354,8 +4354,14 @@
|
|||
"links": {},
|
||||
"address": "0xca574a31a4cf1eeabeecaffc555bdc7f91c5caf9",
|
||||
"transactionHash": "0x463374c2fbc7eaff5b87e65c6a8fdc1177ef82c66084df6e7b88b506f99b193c"
|
||||
},
|
||||
"1528296677763": {
|
||||
"events": {},
|
||||
"links": {},
|
||||
"address": "0x66c535e20f0c90530431ebab626da0ebdd55ec2d",
|
||||
"transactionHash": "0x463374c2fbc7eaff5b87e65c6a8fdc1177ef82c66084df6e7b88b506f99b193c"
|
||||
}
|
||||
},
|
||||
"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
|
|
@ -10,8 +10,12 @@ import PageFrame from '~/components/layout/PageFrame'
|
|||
import { history, store } from '~/store'
|
||||
import theme from '~/theme/mui'
|
||||
import AppRoutes from '~/routes'
|
||||
import fetchSafes from '~/routes/safe/store/actions/fetchSafes'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
store.dispatch(fetchSafes())
|
||||
|
||||
const Root = () => (
|
||||
<Provider store={store}>
|
||||
<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,
|
||||
})
|
|
@ -80,7 +80,7 @@ const hasOneOwner = (safe: Safe) => {
|
|||
return owners.count() === 1
|
||||
}
|
||||
|
||||
export const getSafeEthereumInstance = async (safeAddress) => {
|
||||
export const getSafeEthereumInstance = async (safeAddress: string) => {
|
||||
const web3 = getWeb3()
|
||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||
return GnosisSafe.at(safeAddress)
|
||||
|
@ -101,7 +101,7 @@ export const createTransaction = async (
|
|||
const valueInWei = web3.toWei(txValue, 'ether')
|
||||
const CALL = 0
|
||||
|
||||
const thresholdIsOne = safe.get('confirmations') === 1
|
||||
const thresholdIsOne = safe.get('threshold') === 1
|
||||
if (hasOneOwner(safe) || thresholdIsOne) {
|
||||
const txConfirmationData =
|
||||
gnosisSafe.contract.execTransactionIfApproved.getData(txDest, valueInWei, data, CALL, nonce)
|
||||
|
@ -109,7 +109,7 @@ export const createTransaction = async (
|
|||
checkReceiptStatus(txHash)
|
||||
|
||||
const executedConfirmations: List<Confirmation> = buildExecutedConfirmationFrom(safe.get('owners'), user)
|
||||
return storeTransaction(txName, nonce, txDest, txValue, user, executedConfirmations, txHash, safeAddress, safe.get('confirmations'), data)
|
||||
return storeTransaction(txName, nonce, txDest, txValue, user, executedConfirmations, txHash, safeAddress, safe.get('threshold'), data)
|
||||
}
|
||||
|
||||
const txConfirmationData =
|
||||
|
@ -119,5 +119,5 @@ export const createTransaction = async (
|
|||
|
||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(safe.get('owners'), user, txConfirmationHash)
|
||||
|
||||
return storeTransaction(txName, nonce, txDest, txValue, user, confirmations, '', safeAddress, safe.get('confirmations'), data)
|
||||
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 nonce: number = 10
|
||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, 'foo', 'confirmationHash')
|
||||
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('confirmations'), '0x')
|
||||
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
|
||||
|
||||
// WHEN
|
||||
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||
|
@ -55,12 +55,12 @@ describe('Transactions Suite', () => {
|
|||
const safeAddress = safe.get('address')
|
||||
const creator = 'foo'
|
||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||
storeTransaction(firstTxName, firstNonce, destination, value, creator, confirmations, '', safeAddress, safe.get('confirmations'), '0x')
|
||||
storeTransaction(firstTxName, firstNonce, destination, value, creator, confirmations, '', safeAddress, safe.get('threshold'), '0x')
|
||||
|
||||
const secondTxName = 'Buy printers for project'
|
||||
const secondNonce: number = firstNonce + 100
|
||||
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||
storeTransaction(secondTxName, secondNonce, destination, value, creator, secondConfirmations, '', safeAddress, safe.get('confirmations'), '0x')
|
||||
storeTransaction(secondTxName, secondNonce, destination, value, creator, secondConfirmations, '', safeAddress, safe.get('threshold'), '0x')
|
||||
|
||||
// WHEN
|
||||
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||
|
@ -82,7 +82,7 @@ describe('Transactions Suite', () => {
|
|||
const safeAddress = safe.address
|
||||
const creator = 'foo'
|
||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safeAddress, safe.get('confirmations'), '0x')
|
||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safeAddress, safe.get('threshold'), '0x')
|
||||
|
||||
const secondSafe = SafeFactory.dailyLimitSafe(10, 2)
|
||||
const txSecondName = 'Buy batteris for Beta project'
|
||||
|
@ -92,7 +92,7 @@ describe('Transactions Suite', () => {
|
|||
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(secondSafe.get('owners'), secondCreator, 'confirmationHash')
|
||||
storeTransaction(
|
||||
txSecondName, txSecondNonce, destination, value, secondCreator,
|
||||
secondConfirmations, '', secondSafeAddress, secondSafe.get('confirmations'), '0x',
|
||||
secondConfirmations, '', secondSafeAddress, secondSafe.get('threshold'), '0x',
|
||||
)
|
||||
|
||||
let transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||
|
@ -112,7 +112,7 @@ describe('Transactions Suite', () => {
|
|||
const txConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'secondConfirmationHash')
|
||||
storeTransaction(
|
||||
txFirstName, txFirstNonce, destination, value, creator,
|
||||
txConfirmations, '', safe.get('address'), safe.get('confirmations'), '0x',
|
||||
txConfirmations, '', safe.get('address'), safe.get('threshold'), '0x',
|
||||
)
|
||||
|
||||
transactions = loadSafeTransactions()
|
||||
|
@ -148,10 +148,10 @@ describe('Transactions Suite', () => {
|
|||
const nonce: number = 10
|
||||
const creator = 'foo'
|
||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'), '0x')
|
||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
|
||||
|
||||
// WHEN
|
||||
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'), '0x')
|
||||
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
|
||||
expect(createTxFnc).toThrow(/Transaction with same nonce/)
|
||||
})
|
||||
|
||||
|
@ -161,7 +161,7 @@ describe('Transactions Suite', () => {
|
|||
const nonce: number = 10
|
||||
const creator = 'foo'
|
||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
|
||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'), '0x')
|
||||
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
|
||||
|
||||
// WHEN
|
||||
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||
|
@ -185,7 +185,7 @@ describe('Transactions Suite', () => {
|
|||
const nonce: number = 10
|
||||
const tx = ''
|
||||
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'), '0x')
|
||||
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/)
|
||||
})
|
||||
|
@ -197,7 +197,7 @@ describe('Transactions Suite', () => {
|
|||
const nonce: number = 10
|
||||
const tx = 'validTxHash'
|
||||
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
|
||||
storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('confirmations'), '0x')
|
||||
storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('threshold'), '0x')
|
||||
|
||||
// WHEN
|
||||
const safeTransactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||
|
|
|
@ -6,6 +6,7 @@ import Collapse from 'material-ui/transitions/Collapse'
|
|||
import ListItemText from '~/components/List/ListItemText'
|
||||
import List, { ListItem, ListItemIcon } from 'material-ui/List'
|
||||
import Avatar from 'material-ui/Avatar'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Group from 'material-ui-icons/Group'
|
||||
import Person from 'material-ui-icons/Person'
|
||||
import ExpandLess from 'material-ui-icons/ExpandLess'
|
||||
|
@ -21,10 +22,13 @@ const styles = {
|
|||
|
||||
type Props = Open & WithStyles & {
|
||||
owners: List<OwnerProps>,
|
||||
onAddOwner: () => void,
|
||||
}
|
||||
|
||||
export const ADD_OWNER_BUTTON_TEXT = 'Add'
|
||||
|
||||
const Owners = openHoc(({
|
||||
open, toggle, owners, classes,
|
||||
open, toggle, owners, classes, onAddOwner,
|
||||
}: Props) => (
|
||||
<React.Fragment>
|
||||
<ListItem onClick={toggle}>
|
||||
|
@ -35,6 +39,13 @@ const Owners = openHoc(({
|
|||
<ListItemIcon>
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemIcon>
|
||||
<Button
|
||||
variant="raised"
|
||||
color="primary"
|
||||
onClick={onAddOwner}
|
||||
>
|
||||
{ADD_OWNER_BUTTON_TEXT}
|
||||
</Button>
|
||||
</ListItem>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
|
|
|
@ -13,6 +13,7 @@ import Withdrawn from '~/routes/safe/component/Withdrawn'
|
|||
import Transactions from '~/routes/safe/component/Transactions'
|
||||
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 Balance from './Balance'
|
||||
|
@ -63,7 +64,13 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
|||
onEditThreshold = () => {
|
||||
const { safe } = this.props
|
||||
|
||||
this.setState({ component: <Threshold numOwners={safe.get('owners').count()} safe={safe} onReset={this.onListTransactions} /> })
|
||||
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() {
|
||||
|
@ -75,8 +82,8 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
|||
<Col sm={12} top="xs" md={5} margin="xl" overflow>
|
||||
<List style={listStyle}>
|
||||
<Balance balance={balance} />
|
||||
<Owners owners={safe.owners} />
|
||||
<Confirmations confirmations={safe.get('confirmations')} onEditThreshold={this.onEditThreshold} />
|
||||
<Owners owners={safe.owners} onAddOwner={this.onAddOwner} />
|
||||
<Confirmations confirmations={safe.get('threshold')} onEditThreshold={this.onEditThreshold} />
|
||||
<Address address={safe.get('address')} />
|
||||
<DailyLimit balance={balance} dailyLimit={safe.get('dailyLimit')} onWithdrawn={this.onWithdrawn} />
|
||||
<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
|
|
@ -1,16 +1,12 @@
|
|||
// @flow
|
||||
import fetchThreshold from '~/routes/safe/store/actions/fetchThreshold'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
||||
type FetchThreshold = typeof fetchThreshold
|
||||
type FetchTransactions = typeof fetchTransactions
|
||||
|
||||
export type Actions = {
|
||||
fetchThreshold: FetchThreshold,
|
||||
fetchTransactions: FetchTransactions,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchThreshold,
|
||||
fetchTransactions,
|
||||
}
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
// @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 GnoForm from '~/components/forms/GnoForm'
|
||||
import Stepper from '~/components/Stepper'
|
||||
import { connect } from 'react-redux'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { composeValidators, minValue, maxValue, mustBeInteger, required } from '~/components/forms/validator'
|
||||
import { getSafeEthereumInstance, createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||
import { sleep } from '~/utils/timer'
|
||||
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,
|
||||
|
@ -21,81 +15,65 @@ type Props = SelectorProps & Actions & {
|
|||
onReset: () => void,
|
||||
}
|
||||
|
||||
const THRESHOLD_PARAM = 'threshold'
|
||||
|
||||
const ThresholdComponent = ({ numOwners, safe }: Props) => () => (
|
||||
<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('confirmations')}`}
|
||||
</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>
|
||||
)
|
||||
const getSteps = () => [
|
||||
'Fill Change threshold Form', 'Review change threshold operation',
|
||||
]
|
||||
|
||||
type State = {
|
||||
initialValues: Object,
|
||||
done: boolean,
|
||||
}
|
||||
|
||||
export const CHANGE_THRESHOLD_RESET_BUTTON_TEXT = 'RESET'
|
||||
|
||||
class Threshold extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
initialValues: {},
|
||||
done: false,
|
||||
}
|
||||
|
||||
onThreshold = async (values: Object) => {
|
||||
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 sleep(1500)
|
||||
this.props.fetchTransactions()
|
||||
this.props.fetchThreshold(safe.get('address'))
|
||||
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, onReset, safe } = this.props
|
||||
const { numOwners, safe } = this.props
|
||||
const { done } = this.state
|
||||
const steps = getSteps()
|
||||
const finishedButton = <Stepper.FinishButton title={CHANGE_THRESHOLD_RESET_BUTTON_TEXT} />
|
||||
|
||||
return (
|
||||
<GnoForm
|
||||
onSubmit={this.onThreshold}
|
||||
render={ThresholdComponent({ numOwners, safe })}
|
||||
padding={15}
|
||||
initialValues={this.state.initialValues}
|
||||
>
|
||||
{(submitting: boolean, submitSucceeded: boolean) => (
|
||||
<Row align="end" margin="lg" grow>
|
||||
<Col xs={12} center="xs">
|
||||
<Button
|
||||
variant="raised"
|
||||
color="primary"
|
||||
onClick={submitSucceeded ? onReset : undefined}
|
||||
type={submitSucceeded ? 'button' : 'submit'}
|
||||
disabled={submitting}
|
||||
>
|
||||
{ submitSucceeded ? 'VISIT TXs' : 'FINISH' }
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</GnoForm>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
// @flow
|
||||
import fetchThreshold from '~/routes/safe/store/actions/fetchThreshold'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
||||
type FetchThreshold = typeof fetchThreshold
|
||||
type FetchTransactions = typeof fetchTransactions
|
||||
|
||||
export type Actions = {
|
||||
fetchThreshold: FetchThreshold,
|
||||
fetchTransactions: FetchTransactions,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchThreshold,
|
||||
fetchTransactions,
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { connect } from 'react-redux'
|
|||
import { type Transaction } from '~/routes/safe/store/model/transaction'
|
||||
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
|
||||
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
|
||||
import { sleep } from '~/utils/timer'
|
||||
import { processTransaction } from './processTransactions'
|
||||
import selector, { type SelectorProps } from './selector'
|
||||
import actions, { type Actions } from './actions'
|
||||
|
@ -18,13 +17,11 @@ type Props = SelectorProps & Actions & {
|
|||
class Transactions extends React.Component<Props, {}> {
|
||||
onProcessTx = async (tx: Transaction, alreadyConfirmed: number) => {
|
||||
const {
|
||||
fetchTransactions, safeAddress, userAddress, fetchThreshold,
|
||||
fetchTransactions, safeAddress, userAddress,
|
||||
} = this.props
|
||||
|
||||
await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress)
|
||||
await sleep(1200)
|
||||
fetchTransactions()
|
||||
fetchThreshold(safeAddress)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -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 { connect } from 'react-redux'
|
||||
import Stepper from '~/components/Stepper'
|
||||
import { sleep } from '~/utils/timer'
|
||||
import { type DailyLimit } from '~/routes/safe/store/model/dailyLimit'
|
||||
import actions, { type Actions } from './actions'
|
||||
import selector, { type SelectorProps } from './selector'
|
||||
import withdrawn from './withdrawn'
|
||||
import WithdrawnForm from './WithdrawnForm'
|
||||
|
@ -14,7 +12,7 @@ const getSteps = () => [
|
|||
'Fill Withdrawn Form', 'Review Withdrawn',
|
||||
]
|
||||
|
||||
type Props = SelectorProps & Actions & {
|
||||
type Props = SelectorProps & {
|
||||
safeAddress: string,
|
||||
dailyLimit: DailyLimit,
|
||||
}
|
||||
|
@ -34,8 +32,6 @@ class Withdrawn extends React.Component<Props, State> {
|
|||
try {
|
||||
const { safeAddress, userAddress } = this.props
|
||||
await withdrawn(values, safeAddress, userAddress)
|
||||
await sleep(3500)
|
||||
this.props.fetchDailyLimit(safeAddress)
|
||||
this.setState({ done: true })
|
||||
} catch (error) {
|
||||
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
|
||||
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
||||
import fetchBalance from '~/routes/safe/store/actions/fetchBalance'
|
||||
import fetchDailyLimit from '~/routes/safe/store/actions/fetchDailyLimit'
|
||||
|
||||
export type Actions = {
|
||||
fetchSafe: typeof fetchSafe,
|
||||
fetchBalance: typeof fetchBalance,
|
||||
fetchDailyLimit: typeof fetchDailyLimit,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchSafe,
|
||||
fetchBalance,
|
||||
fetchDailyLimit,
|
||||
}
|
||||
|
|
|
@ -14,17 +14,12 @@ type Props = Actions & SelectorProps & {
|
|||
class SafeView extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.intervalId = setInterval(() => {
|
||||
const { safe, fetchBalance } = this.props
|
||||
const { safe, fetchSafe, fetchBalance } = this.props
|
||||
if (!safe) { return }
|
||||
|
||||
const safeAddress: string = safe.get('address')
|
||||
fetchBalance(safeAddress)
|
||||
fetchSafe(safe)
|
||||
}, 1500)
|
||||
|
||||
const { fetchDailyLimit, safe } = this.props
|
||||
if (safe) {
|
||||
fetchDailyLimit(safe.get('address'))
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -20,14 +20,14 @@ const addSafe = createAction(
|
|||
ADD_SAFE,
|
||||
(
|
||||
name: string, address: string,
|
||||
confirmations: number, limit: number,
|
||||
threshold: number, limit: number,
|
||||
ownersName: string[], ownersAddress: string[],
|
||||
): SafeProps => {
|
||||
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
|
||||
const dailyLimit: DailyLimit = buildDailyLimitFrom(limit)
|
||||
|
||||
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,12 +0,0 @@
|
|||
// @flow
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { getSafeEthereumInstance } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||
import updateThreshold from './updateThreshold'
|
||||
|
||||
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||
const actualThreshold = await gnosisSafe.getThreshold()
|
||||
|
||||
return dispatch(updateThreshold(safeAddress, actualThreshold))
|
||||
}
|
|
@ -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
|
|
@ -1,19 +0,0 @@
|
|||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const UPDATE_THRESHOLD = 'UPDATE_THRESHOLD'
|
||||
|
||||
type ThresholdProps = {
|
||||
safeAddress: string,
|
||||
threshold: number,
|
||||
}
|
||||
|
||||
const updateDailyLimit = createAction(
|
||||
UPDATE_THRESHOLD,
|
||||
(safeAddress: string, threshold: number): ThresholdProps => ({
|
||||
safeAddress,
|
||||
threshold: Number(threshold),
|
||||
}),
|
||||
)
|
||||
|
||||
export default updateDailyLimit
|
|
@ -7,7 +7,7 @@ import type { Owner } from '~/routes/safe/store/model/owner'
|
|||
export type SafeProps = {
|
||||
name: string,
|
||||
address: string,
|
||||
confirmations: number,
|
||||
threshold: number,
|
||||
owners: List<Owner>,
|
||||
dailyLimit: DailyLimit,
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export type SafeProps = {
|
|||
export const makeSafe: RecordFactory<SafeProps> = Record({
|
||||
name: '',
|
||||
address: '',
|
||||
confirmations: 0,
|
||||
threshold: 0,
|
||||
owners: List([]),
|
||||
dailyLimit: makeDailyLimit(),
|
||||
})
|
||||
|
|
|
@ -1,37 +1,16 @@
|
|||
// @flow
|
||||
import { Map, List } from 'immutable'
|
||||
import { Map } from 'immutable'
|
||||
import { handleActions, type ActionType } from 'redux-actions'
|
||||
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 { load, saveSafes, SAFES_KEY } from '~/utils/localStorage'
|
||||
import { makeDailyLimit } from '~/routes/safe/store/model/dailyLimit'
|
||||
import updateThreshold, { UPDATE_THRESHOLD } from '~/routes/safe/store/actions/updateThreshold'
|
||||
import { saveSafes, setOwners } from '~/utils/localStorage'
|
||||
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 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> = {
|
||||
key: string,
|
||||
|
@ -44,13 +23,16 @@ action: AddSafeType
|
|||
*/
|
||||
|
||||
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 => {
|
||||
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())
|
||||
return safes
|
||||
},
|
||||
[UPDATE_DAILY_LIMIT]: (state: State, action: ActionType<typeof updateDailyLimit>): State =>
|
||||
state.updateIn([action.payload.safeAddress, 'dailyLimit'], () => makeDailyLimit(action.payload.dailyLimit)),
|
||||
[UPDATE_THRESHOLD]: (state: State, action: ActionType<typeof updateThreshold>): State =>
|
||||
state.updateIn([action.payload.safeAddress, 'confirmations'], () => action.payload.threshold),
|
||||
}, Map())
|
||||
|
|
|
@ -20,7 +20,7 @@ class SafeBuilder {
|
|||
}
|
||||
|
||||
withConfirmations(confirmations: number) {
|
||||
this.safe = this.safe.set('confirmations', confirmations)
|
||||
this.safe = this.safe.set('threshold', confirmations)
|
||||
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
|
||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
|
||||
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 * as SafeFields from '~/routes/open/components/fields'
|
||||
import { getAccountsFrom, getNamesFrom } from '~/routes/open/utils/safeDataExtractor'
|
||||
|
@ -56,26 +56,6 @@ const providerReducerTests = () => {
|
|||
// THEN
|
||||
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,8 +1,6 @@
|
|||
// @flow
|
||||
import balanceReducerTests from './balance.reducer'
|
||||
import safeReducerTests from './safe.reducer'
|
||||
import dailyLimitReducerTests from './dailyLimit.reducer'
|
||||
import thresholdReducerTests from './threshold.reducer'
|
||||
import balanceSelectorTests from './balance.selector'
|
||||
import safeSelectorTests from './safe.selector'
|
||||
import grantedSelectorTests from './granted.selector'
|
||||
|
@ -13,8 +11,6 @@ describe('Safe Test suite', () => {
|
|||
// ACTIONS AND REDUCERS
|
||||
safeReducerTests()
|
||||
balanceReducerTests()
|
||||
dailyLimitReducerTests()
|
||||
thresholdReducerTests()
|
||||
|
||||
// SAFE SELECTOR
|
||||
safeSelectorTests()
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
// @flow
|
||||
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
||||
import { aNewStore } from '~/store'
|
||||
import updateThreshold from '~/routes/safe/store/actions/updateThreshold'
|
||||
import { aDeployedSafe } from './builder/deployedSafe.builder'
|
||||
|
||||
const thresholdReducerTests = () => {
|
||||
describe('Safe Actions[updateThreshold]', () => {
|
||||
let store
|
||||
beforeEach(async () => {
|
||||
store = aNewStore()
|
||||
})
|
||||
|
||||
it('reducer should return 3 when a safe of 3 threshold has just been created', async () => {
|
||||
// GIVEN
|
||||
const safeThreshold = 3
|
||||
const numOwners = 3
|
||||
|
||||
// WHEN
|
||||
const safeAddress = await aDeployedSafe(store, 0.5, safeThreshold, numOwners)
|
||||
|
||||
// THEN
|
||||
const safes = store.getState()[SAFE_REDUCER_ID]
|
||||
const threshold = safes.get(safeAddress).get('confirmations')
|
||||
expect(threshold).not.toBe(undefined)
|
||||
expect(threshold).toBe(safeThreshold)
|
||||
})
|
||||
|
||||
it('reducer should change correctly', async () => {
|
||||
// GIVEN
|
||||
const safeThreshold = 3
|
||||
const numOwners = 3
|
||||
const safeAddress = await aDeployedSafe(store, 0.5, safeThreshold, numOwners)
|
||||
|
||||
// WHEN
|
||||
const newThreshold = 1
|
||||
await store.dispatch(updateThreshold(safeAddress, newThreshold))
|
||||
|
||||
// THEN
|
||||
const safes = store.getState()[SAFE_REDUCER_ID]
|
||||
const threshold = safes.get(safeAddress).get('confirmations')
|
||||
expect(threshold).not.toBe(undefined)
|
||||
expect(threshold).toBe(newThreshold)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default thresholdReducerTests
|
|
@ -45,7 +45,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
|||
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const addTxButton = buttons[2]
|
||||
const addTxButton = buttons[3]
|
||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||
await sleep(1800) // Give time to enable Add button
|
||||
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 tx = getTransactionFromReduxStore(store, address)
|
||||
if (!tx) throw new Error()
|
||||
expect(confirmed).toBe(tx.get('tx'))
|
||||
|
||||
const ownerTx = paragraphs[6].innerHTML
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 3 thresh
|
|||
|
||||
const getAlreadyConfirmed = () => {
|
||||
const tx = getTransactionFromReduxStore(store, address)
|
||||
if (!tx) throw new Error()
|
||||
const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx })
|
||||
|
||||
return confirmed
|
||||
|
@ -53,6 +54,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 3 thresh
|
|||
const makeConfirmation = async (executor) => {
|
||||
const alreadyConfirmed = getAlreadyConfirmed()
|
||||
const tx = getTransactionFromReduxStore(store, address)
|
||||
if (!tx) throw new Error()
|
||||
await processTransaction(address, tx, alreadyConfirmed, executor)
|
||||
await sleep(800)
|
||||
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 tx = getTransactionFromReduxStore(store, address)
|
||||
if (!tx) throw new Error()
|
||||
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])
|
||||
})
|
||||
})
|
|
@ -3,6 +3,7 @@ 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'
|
||||
|
@ -22,6 +23,7 @@ describe('React DOM TESTS > Change threshold', () => {
|
|||
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)
|
||||
|
@ -37,7 +39,8 @@ describe('React DOM TESTS > Change threshold', () => {
|
|||
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||
expect(transactions.count()).toBe(1)
|
||||
|
||||
const thresholdTx: Transaction = transactions.get(0)
|
||||
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('')
|
||||
|
@ -48,6 +51,7 @@ describe('React DOM TESTS > Change threshold', () => {
|
|||
|
||||
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)
|
||||
|
@ -66,6 +70,7 @@ describe('React DOM TESTS > Change threshold', () => {
|
|||
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)
|
||||
|
@ -78,9 +83,11 @@ describe('React DOM TESTS > Change threshold', () => {
|
|||
await store.dispatch(fetchTransactions())
|
||||
|
||||
let transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||
if (!transactions) throw new Error()
|
||||
expect(transactions.count()).toBe(1)
|
||||
|
||||
let thresholdTx: Transaction = transactions.get(0)
|
||||
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()
|
||||
|
@ -103,6 +110,7 @@ describe('React DOM TESTS > Change threshold', () => {
|
|||
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('')
|
||||
|
|
|
@ -46,7 +46,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
|||
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const withdrawnButton = buttons[1]
|
||||
const withdrawnButton = buttons[2]
|
||||
expect(withdrawnButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
|
||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(withdrawnButton, 'button')[0])
|
||||
await sleep(4000)
|
||||
|
@ -96,7 +96,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
|||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const addTxButton = buttons[2]
|
||||
const addTxButton = buttons[3]
|
||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||
expect(addTxButton.props.disabled).toBe(true)
|
||||
|
||||
|
@ -110,7 +110,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
|||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const addTxButton = buttons[1]
|
||||
const addTxButton = buttons[2]
|
||||
expect(addTxButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
|
||||
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 TransactionComponent from '~/routes/safe/component/Transactions/Transaction'
|
||||
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
|
||||
const AddTransaction = TestUtils.findRenderedComponentWithType(SafeDom, AddTransactionComponent)
|
||||
|
||||
|
@ -36,30 +41,30 @@ export const checkBalanceOf = async (addressToTest: string, value: string) => {
|
|||
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
|
||||
await addEtherTo(destination, '0.1')
|
||||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const addTxButton = buttons[2]
|
||||
const addTxButton = buttons[3]
|
||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||
await sleep(1800) // Give time to enable Add button
|
||||
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)
|
||||
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const seeTx = buttons[3]
|
||||
const seeTx = buttons[4]
|
||||
expect(seeTx.props.children).toEqual(SEE_MULTISIG_BUTTON_TEXT)
|
||||
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)
|
||||
if (!Transactions) throw new Error()
|
||||
const Transaction = TestUtils.findRenderedComponentWithType(Transactions, TransactionComponent)
|
||||
|
@ -68,7 +73,11 @@ export const getTagFromTransaction = (SafeDom, tag: string) => {
|
|||
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')
|
||||
TestUtils.Simulate.click(paragraphs[2]) // expanded
|
||||
await sleep(1000) // Time to expand
|
||||
|
@ -80,13 +89,13 @@ export const expandTransactionOf = async (SafeDom, numOwners, safeThreshold) =>
|
|||
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 })
|
||||
|
||||
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')
|
||||
for (let i = 0; i < statusses.length; i += 1) {
|
||||
const ownerIndex = i + 6
|
||||
|
|
|
@ -32,7 +32,7 @@ const SafeTable = ({ safes }: Props) => (
|
|||
</TableCell>
|
||||
<TableCell padding="none">{safe.get('name')}</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('dailyLimit').get('value')} ETH`}</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { routerMiddleware, routerReducer } from 'react-router-redux'
|
|||
import { combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
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 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 = {
|
||||
[SAFE_REDUCER_ID]: safeInitialState(),
|
||||
[TRANSACTIONS_REDUCER_ID]: transactionsInitialState(),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import { type Owner } from '~/routes/safe/store/model/owner'
|
||||
|
||||
export const SAFES_KEY = 'SAFES'
|
||||
export const TX_KEY = 'TX'
|
||||
export const OWNERS_KEY = 'OWNERS'
|
||||
|
||||
export const load = (key: string) => {
|
||||
try {
|
||||
|
@ -27,3 +31,19 @@ export const saveSafes = (safes: Object) => {
|
|||
// 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'
|
||||
|
||||
let web3
|
||||
export const getWeb3 = () => web3
|
||||
export const getWeb3 = () => web3 || new Web3(window.web3.currentProvider)
|
||||
|
||||
const isMetamask: Function = (web3Provider): boolean => {
|
||||
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 })
|
||||
}
|
||||
|
||||
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