diff --git a/config/jest/jest.setup.js b/config/jest/jest.setup.js index 8d9161a7..c97ef811 100644 --- a/config/jest/jest.setup.js +++ b/config/jest/jest.setup.js @@ -1,2 +1,2 @@ // @flow -jest.setTimeout(30000) +jest.setTimeout(45000) diff --git a/safe-contracts/build/contracts/CreateAndAddModules.json b/safe-contracts/build/contracts/CreateAndAddModules.json index 1e7d5e36..62751b26 100644 --- a/safe-contracts/build/contracts/CreateAndAddModules.json +++ b/safe-contracts/build/contracts/CreateAndAddModules.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/DailyLimitModule.json b/safe-contracts/build/contracts/DailyLimitModule.json index e9bcb94c..5b0be962 100644 --- a/safe-contracts/build/contracts/DailyLimitModule.json +++ b/safe-contracts/build/contracts/DailyLimitModule.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/GnosisSafePersonalEdition.json b/safe-contracts/build/contracts/GnosisSafePersonalEdition.json index 03ba515f..e3ea4668 100644 --- a/safe-contracts/build/contracts/GnosisSafePersonalEdition.json +++ b/safe-contracts/build/contracts/GnosisSafePersonalEdition.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/GnosisSafeTeamEdition.json b/safe-contracts/build/contracts/GnosisSafeTeamEdition.json index 14a19442..9cb27b58 100644 --- a/safe-contracts/build/contracts/GnosisSafeTeamEdition.json +++ b/safe-contracts/build/contracts/GnosisSafeTeamEdition.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/Migrations.json b/safe-contracts/build/contracts/Migrations.json index d9d7c21a..5d1fcd56 100644 --- a/safe-contracts/build/contracts/Migrations.json +++ b/safe-contracts/build/contracts/Migrations.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/MultiSend.json b/safe-contracts/build/contracts/MultiSend.json index 79da2b5d..41362d48 100644 --- a/safe-contracts/build/contracts/MultiSend.json +++ b/safe-contracts/build/contracts/MultiSend.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/ProxyFactory.json b/safe-contracts/build/contracts/ProxyFactory.json index 5d2b5bed..a218d2eb 100644 --- a/safe-contracts/build/contracts/ProxyFactory.json +++ b/safe-contracts/build/contracts/ProxyFactory.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/SocialRecoveryModule.json b/safe-contracts/build/contracts/SocialRecoveryModule.json index 8f32e0de..5e7d519b 100644 --- a/safe-contracts/build/contracts/SocialRecoveryModule.json +++ b/safe-contracts/build/contracts/SocialRecoveryModule.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/StateChannelModule.json b/safe-contracts/build/contracts/StateChannelModule.json index a7ce755f..2ffa0349 100644 --- a/safe-contracts/build/contracts/StateChannelModule.json +++ b/safe-contracts/build/contracts/StateChannelModule.json @@ -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" } \ No newline at end of file diff --git a/safe-contracts/build/contracts/WhitelistModule.json b/safe-contracts/build/contracts/WhitelistModule.json index 7eff5a28..c0307ad7 100644 --- a/safe-contracts/build/contracts/WhitelistModule.json +++ b/safe-contracts/build/contracts/WhitelistModule.json @@ -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" } \ No newline at end of file diff --git a/src/components/forms/Checkbox/index.jsx b/src/components/forms/Checkbox/index.jsx new file mode 100644 index 00000000..b20e101e --- /dev/null +++ b/src/components/forms/Checkbox/index.jsx @@ -0,0 +1,27 @@ +// @flow +import React from 'react' +import Checkbox, { type CheckoxProps } from 'material-ui/Checkbox' + +class GnoCheckbox extends React.PureComponent { + render() { + const { + input: { + checked, name, onChange, ...restInput + }, + meta, + ...rest + } = this.props + + return ( + + ) + } +} + +export default GnoCheckbox diff --git a/src/components/forms/GnoForm/index.jsx b/src/components/forms/GnoForm/index.jsx index d51637f1..7dc29a35 100644 --- a/src/components/forms/GnoForm/index.jsx +++ b/src/components/forms/GnoForm/index.jsx @@ -29,7 +29,7 @@ const GnoForm = ({ render={({ handleSubmit, ...rest }) => (
{render(rest)} - {children(rest.submitting)} + {children(rest.submitting, rest.submitSucceeded)}
)} /> diff --git a/src/index.js b/src/index.js index ec531904..e57c20bb 100644 --- a/src/index.js +++ b/src/index.js @@ -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 = () => ( diff --git a/src/routes/safe/component/AddOwner/AddOwnerForm/index.jsx b/src/routes/safe/component/AddOwner/AddOwnerForm/index.jsx new file mode 100644 index 00000000..721869f2 --- /dev/null +++ b/src/routes/safe/component/AddOwner/AddOwnerForm/index.jsx @@ -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) => () => ( + + + Add Owner + + + {`Actual number of owners: ${numOwners}, with threshold: ${threshold}`} + + + + + + + + + + Increase owner? + + +) + +export default AddOwnerForm diff --git a/src/routes/safe/component/AddOwner/Review/index.jsx b/src/routes/safe/component/AddOwner/Review/index.jsx new file mode 100644 index 00000000..6bc31b77 --- /dev/null +++ b/src/routes/safe/component/AddOwner/Review/index.jsx @@ -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 ( + + Review the Add Owner operation + + Owner Name: {values[NAME_PARAM]} + + + Owner Address: {values[OWNER_ADDRESS_PARAM]} + + + {text} + + + { submitting && } + + + ) +} + +export default Review diff --git a/src/routes/safe/component/AddOwner/actions.js b/src/routes/safe/component/AddOwner/actions.js new file mode 100644 index 00000000..32f51f38 --- /dev/null +++ b/src/routes/safe/component/AddOwner/actions.js @@ -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, +} diff --git a/src/routes/safe/component/AddOwner/index.jsx b/src/routes/safe/component/AddOwner/index.jsx new file mode 100644 index 00000000..ee31bcdd --- /dev/null +++ b/src/routes/safe/component/AddOwner/index.jsx @@ -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) => { + if (!owners) { + return [] + } + + return owners.map((owner: Owner) => owner.get('address')) +} + +class AddOwner extends React.Component { + 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 = + const addresses = getOwnerAddressesFrom(safe.get('owners')) + + return ( + + + + { AddOwnerForm } + + + { Review } + + + + ) + } +} + +export default connect(selector, actions)(AddOwner) diff --git a/src/routes/safe/component/AddOwner/selector.js b/src/routes/safe/component/AddOwner/selector.js new file mode 100644 index 00000000..9e7bfef1 --- /dev/null +++ b/src/routes/safe/component/AddOwner/selector.js @@ -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, +}) diff --git a/src/routes/safe/component/AddTransaction/createTransactions.js b/src/routes/safe/component/AddTransaction/createTransactions.js index 7ac3956b..f7c25503 100644 --- a/src/routes/safe/component/AddTransaction/createTransactions.js +++ b/src/routes/safe/component/AddTransaction/createTransactions.js @@ -47,6 +47,7 @@ export const storeTransaction = ( tx: string, safeAddress: string, safeThreshold: number, + data: string, ) => { const notMinedWhenOneOwnerSafe = confirmations.count() === 1 && !tx if (notMinedWhenOneOwnerSafe) { @@ -54,7 +55,7 @@ export const storeTransaction = ( } 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) || {} @@ -79,37 +80,44 @@ const hasOneOwner = (safe: Safe) => { 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 ( safe: Safe, txName: string, - txDestination: string, + txDest: string, txValue: number, nonce: number, user: string, + data: string = '0x', ) => { const web3 = getWeb3() - const GnosisSafe = await getGnosisSafeContract(web3) const safeAddress = safe.get('address') - const gnosisSafe = GnosisSafe.at(safeAddress) - + const gnosisSafe = await getSafeEthereumInstance(safeAddress) 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(txDestination, valueInWei, '0x', CALL, nonce) + const txConfirmationData = + gnosisSafe.contract.execTransactionIfApproved.getData(txDest, valueInWei, data, CALL, nonce) const txHash = await executeTransaction(txConfirmationData, user, safeAddress) checkReceiptStatus(txHash) const executedConfirmations: List = 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) checkReceiptStatus(txConfirmationHash) const confirmations: List = 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) } diff --git a/src/routes/safe/component/AddTransaction/test/transactions.test.js b/src/routes/safe/component/AddTransaction/test/transactions.test.js index 367403bf..39270c0f 100644 --- a/src/routes/safe/component/AddTransaction/test/transactions.test.js +++ b/src/routes/safe/component/AddTransaction/test/transactions.test.js @@ -33,7 +33,7 @@ describe('Transactions Suite', () => { const txName = 'Buy butteries for project' const nonce: number = 10 const confirmations: List = 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 const transactions: Map> = loadSafeTransactions() @@ -45,7 +45,7 @@ describe('Transactions Suite', () => { if (!safeTransactions) { throw new Error() } 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 () => { @@ -55,12 +55,12 @@ describe('Transactions Suite', () => { const safeAddress = safe.get('address') const creator = 'foo' const confirmations: List = 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 secondNonce: number = firstNonce + 100 const secondConfirmations: List = 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 const transactions: Map> = loadSafeTransactions() @@ -72,8 +72,8 @@ describe('Transactions Suite', () => { if (!safeTxs) { throw new Error() } testSizeOfTransactions(safeTxs, 2) - testTransactionFrom(safeTxs, 0, firstTxName, firstNonce, value, 2, destination, '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, 0, firstTxName, firstNonce, value, 2, destination, '0x', '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 () => { @@ -82,7 +82,7 @@ describe('Transactions Suite', () => { const safeAddress = safe.address const creator = 'foo' const confirmations: List = 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 txSecondName = 'Buy batteris for Beta project' @@ -92,7 +92,7 @@ describe('Transactions Suite', () => { const secondConfirmations: List = buildConfirmationsFrom(secondSafe.get('owners'), secondCreator, 'confirmationHash') storeTransaction( txSecondName, txSecondNonce, destination, value, secondCreator, - secondConfirmations, '', secondSafeAddress, secondSafe.get('confirmations'), + secondConfirmations, '', secondSafeAddress, secondSafe.get('threshold'), '0x', ) let transactions: Map> = loadSafeTransactions() @@ -112,7 +112,7 @@ describe('Transactions Suite', () => { const txConfirmations: List = buildConfirmationsFrom(owners, creator, 'secondConfirmationHash') storeTransaction( txFirstName, txFirstNonce, destination, value, creator, - txConfirmations, '', safe.get('address'), safe.get('confirmations'), + txConfirmations, '', safe.get('address'), safe.get('threshold'), '0x', ) transactions = loadSafeTransactions() @@ -125,19 +125,19 @@ describe('Transactions Suite', () => { // Test 2 transactions of first safe testTransactionFrom( transactions.get(safe.address), 0, - txName, nonce, value, 2, destination, + txName, nonce, value, 2, destination, '0x', 'foo', 'confirmationHash', owners.get(0), owners.get(1), ) testTransactionFrom( transactions.get(safe.address), 1, - txFirstName, txFirstNonce, value, 2, destination, + txFirstName, txFirstNonce, value, 2, destination, '0x', 'foo', 'secondConfirmationHash', owners.get(0), owners.get(1), ) // Test one transaction of second safe testTransactionFrom( 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), ) }) @@ -148,10 +148,10 @@ describe('Transactions Suite', () => { const nonce: number = 10 const creator = 'foo' const confirmations: List = 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 - 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/) }) @@ -161,7 +161,7 @@ describe('Transactions Suite', () => { const nonce: number = 10 const creator = 'foo' const confirmations: List = 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 const transactions: Map> = loadSafeTransactions() @@ -185,7 +185,7 @@ describe('Transactions Suite', () => { const nonce: number = 10 const tx = '' const confirmations: List = 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/) }) @@ -197,7 +197,7 @@ describe('Transactions Suite', () => { const nonce: number = 10 const tx = 'validTxHash' const confirmations: List = 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 const safeTransactions: Map> = loadSafeTransactions() diff --git a/src/routes/safe/component/AddTransaction/test/transactionsHelper.js b/src/routes/safe/component/AddTransaction/test/transactionsHelper.js index 9b92df35..8ad22983 100644 --- a/src/routes/safe/component/AddTransaction/test/transactionsHelper.js +++ b/src/routes/safe/component/AddTransaction/test/transactionsHelper.js @@ -20,7 +20,7 @@ export const testSizeOfTransactions = (safeTxs: List | typeof undef export const testTransactionFrom = ( safeTxs: List | typeof undefined, pos: number, name: 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, ) => { if (!safeTxs) { throw new Error() } @@ -33,6 +33,7 @@ export const testTransactionFrom = ( expect(tx.get('destination')).toBe(destination) expect(tx.get('confirmations').count()).toBe(2) expect(tx.get('nonce')).toBe(nonce) + expect(tx.get('data')).toBe(data) const confirmations: List = tx.get('confirmations') const firstConfirmation: Confirmation | typeof undefined = confirmations.get(0) diff --git a/src/routes/safe/component/Safe/Confirmations.jsx b/src/routes/safe/component/Safe/Confirmations.jsx index 91c192d9..faf23d45 100644 --- a/src/routes/safe/component/Safe/Confirmations.jsx +++ b/src/routes/safe/component/Safe/Confirmations.jsx @@ -4,12 +4,16 @@ import { ListItem } from 'material-ui/List' import Avatar from 'material-ui/Avatar' import DoneAll from 'material-ui-icons/DoneAll' import ListItemText from '~/components/List/ListItemText' +import Button from '~/components/layout/Button' type Props = { confirmations: number, + onEditThreshold: () => void, } -const Confirmations = ({ confirmations }: Props) => ( +const EDIT_THRESHOLD_BUTTON_TEXT = 'EDIT' + +const Confirmations = ({ confirmations, onEditThreshold }: Props) => ( @@ -19,6 +23,13 @@ const Confirmations = ({ confirmations }: Props) => ( secondary={`${confirmations} required confirmations per transaction`} cut /> + ) diff --git a/src/routes/safe/component/Safe/Owners.jsx b/src/routes/safe/component/Safe/Owners.jsx index e4801fb0..4386d7ab 100644 --- a/src/routes/safe/component/Safe/Owners.jsx +++ b/src/routes/safe/component/Safe/Owners.jsx @@ -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, + onAddOwner: () => void, } +export const ADD_OWNER_BUTTON_TEXT = 'Add' + const Owners = openHoc(({ - open, toggle, owners, classes, + open, toggle, owners, classes, onAddOwner, }: Props) => ( @@ -35,6 +39,13 @@ const Owners = openHoc(({ {open ? : } + diff --git a/src/routes/safe/component/Safe/index.jsx b/src/routes/safe/component/Safe/index.jsx index 8e88fff2..23588ec9 100644 --- a/src/routes/safe/component/Safe/index.jsx +++ b/src/routes/safe/component/Safe/index.jsx @@ -12,6 +12,8 @@ import List from 'material-ui/List' 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' @@ -59,6 +61,18 @@ class GnoSafe extends React.PureComponent { this.setState({ component: }) } + onEditThreshold = () => { + const { safe } = this.props + + this.setState({ component: }) + } + + onAddOwner = (e: SyntheticEvent) => { + const { safe } = this.props + e.stopPropagation() + this.setState({ component: }) + } + render() { const { safe, balance } = this.props const { component } = this.state @@ -68,8 +82,8 @@ class GnoSafe extends React.PureComponent { - - + +
diff --git a/src/routes/safe/component/Threshold/Review/index.jsx b/src/routes/safe/component/Threshold/Review/index.jsx new file mode 100644 index 00000000..0483e876 --- /dev/null +++ b/src/routes/safe/component/Threshold/Review/index.jsx @@ -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) => ( + + Review the Threshold operation + + The new threshold will be: {values[THRESHOLD_PARAM]} + + + { submitting && } + + +) + +export default Review diff --git a/src/routes/safe/component/Threshold/ThresholdForm/index.jsx b/src/routes/safe/component/Threshold/ThresholdForm/index.jsx new file mode 100644 index 00000000..66a1e89d --- /dev/null +++ b/src/routes/safe/component/Threshold/ThresholdForm/index.jsx @@ -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) => () => ( + + + {'Change safe\'s threshold'} + + + {`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('threshold')}`} + + + + + +) + +export default ThresholdForm diff --git a/src/routes/safe/component/Threshold/actions.js b/src/routes/safe/component/Threshold/actions.js new file mode 100644 index 00000000..32f51f38 --- /dev/null +++ b/src/routes/safe/component/Threshold/actions.js @@ -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, +} diff --git a/src/routes/safe/component/Threshold/index.jsx b/src/routes/safe/component/Threshold/index.jsx new file mode 100644 index 00000000..297b8d50 --- /dev/null +++ b/src/routes/safe/component/Threshold/index.jsx @@ -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 { + 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 = + + return ( + + + + { ThresholdForm } + + + { Review } + + + + ) + } +} + +export default connect(selector, actions)(Threshold) diff --git a/src/routes/safe/component/Threshold/selector.js b/src/routes/safe/component/Threshold/selector.js new file mode 100644 index 00000000..9e7bfef1 --- /dev/null +++ b/src/routes/safe/component/Threshold/selector.js @@ -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, +}) diff --git a/src/routes/safe/component/Transactions/actions.js b/src/routes/safe/component/Transactions/actions.js index 681ad469..32f51f38 100644 --- a/src/routes/safe/component/Transactions/actions.js +++ b/src/routes/safe/component/Transactions/actions.js @@ -1,8 +1,10 @@ // @flow import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' +type FetchTransactions = typeof fetchTransactions + export type Actions = { - fetchTransactions: typeof fetchTransactions, + fetchTransactions: FetchTransactions, } export default { diff --git a/src/routes/safe/component/Transactions/index.jsx b/src/routes/safe/component/Transactions/index.jsx index 5dac8846..10c3be1f 100644 --- a/src/routes/safe/component/Transactions/index.jsx +++ b/src/routes/safe/component/Transactions/index.jsx @@ -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' @@ -17,9 +16,11 @@ type Props = SelectorProps & Actions & { } class Transactions extends React.Component { 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 sleep(1200) fetchTransactions() } diff --git a/src/routes/safe/component/Transactions/processTransactions.js b/src/routes/safe/component/Transactions/processTransactions.js index dfbc64d2..32f0d123 100644 --- a/src/routes/safe/component/Transactions/processTransactions.js +++ b/src/routes/safe/component/Transactions/processTransactions.js @@ -20,9 +20,10 @@ export const updateTransaction = ( tx: string, safeAddress: string, safeThreshold: number, + data: string, ) => { 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) || {} @@ -36,7 +37,6 @@ export const updateTransaction = ( localStorage.setItem(TX_KEY, JSON.stringify(safeTransactions)) } -const getData = () => '0x' const getOperation = () => 0 const execTransaction = async ( @@ -45,8 +45,8 @@ const execTransaction = async ( txValue: number, nonce: number, executor: string, + data: string, ) => { - const data = getData() const CALL = getOperation() const web3 = getWeb3() const valueInWei = web3.toWei(txValue, 'ether') @@ -61,8 +61,8 @@ const execConfirmation = async ( txValue: number, nonce: number, executor: string, + data: string, ) => { - const data = getData() const CALL = getOperation() const web3 = getWeb3() const valueInWei = web3.toWei(txValue, 'ether') @@ -110,10 +110,11 @@ export const processTransaction = async ( const txName = tx.get('name') const txValue = tx.get('value') const txDestination = tx.get('destination') + const data = tx.get('data') const txHash = thresholdReached - ? await execTransaction(gnosisSafe, txDestination, txValue, nonce, userAddress) - : await execConfirmation(gnosisSafe, txDestination, txValue, nonce, userAddress) + ? await execTransaction(gnosisSafe, txDestination, txValue, nonce, userAddress, data) + : await execConfirmation(gnosisSafe, txDestination, txValue, nonce, userAddress, data) checkReceiptStatus(txHash) @@ -130,5 +131,6 @@ export const processTransaction = async ( thresholdReached ? txHash : '', safeAddress, threshold, + data, ) } diff --git a/src/routes/safe/component/Withdrawn/actions.js b/src/routes/safe/component/Withdrawn/actions.js deleted file mode 100644 index 8caf6dff..00000000 --- a/src/routes/safe/component/Withdrawn/actions.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import fetchDailyLimit from '~/routes/safe/store/actions/fetchDailyLimit' - -export type Actions = { - fetchDailyLimit: typeof fetchDailyLimit, -} - -export default { - fetchDailyLimit, -} diff --git a/src/routes/safe/component/Withdrawn/index.jsx b/src/routes/safe/component/Withdrawn/index.jsx index c6ddba18..608792be 100644 --- a/src/routes/safe/component/Withdrawn/index.jsx +++ b/src/routes/safe/component/Withdrawn/index.jsx @@ -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 { 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 { } } -export default connect(selector, actions)(Withdrawn) +export default connect(selector)(Withdrawn) diff --git a/src/routes/safe/container/actions.js b/src/routes/safe/container/actions.js index 4c0e607f..6ffe7123 100644 --- a/src/routes/safe/container/actions.js +++ b/src/routes/safe/container/actions.js @@ -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, } diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index ea291ffc..d6579ead 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -14,17 +14,12 @@ type Props = Actions & SelectorProps & { class SafeView extends React.PureComponent { 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() { diff --git a/src/routes/safe/store/actions/addSafe.js b/src/routes/safe/store/actions/addSafe.js index fb36cf6b..60c560e4 100644 --- a/src/routes/safe/store/actions/addSafe.js +++ b/src/routes/safe/store/actions/addSafe.js @@ -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 = buildOwnersFrom(ownersName, ownersAddress) const dailyLimit: DailyLimit = buildDailyLimitFrom(limit) return ({ - address, name, confirmations, owners, dailyLimit, + address, name, threshold, owners, dailyLimit, }) }, ) diff --git a/src/routes/safe/store/actions/fetchDailyLimit.js b/src/routes/safe/store/actions/fetchDailyLimit.js deleted file mode 100644 index 87cb86b8..00000000 --- a/src/routes/safe/store/actions/fetchDailyLimit.js +++ /dev/null @@ -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) => { - const ethAddress = 0 - const dailyLimit: DailyLimitProps = await getDailyLimitFrom(safeAddress, ethAddress) - - return dispatch(updateDailyLimit(safeAddress, dailyLimit)) -} diff --git a/src/routes/safe/store/actions/fetchSafe.js b/src/routes/safe/store/actions/fetchSafe.js new file mode 100644 index 00000000..9102489a --- /dev/null +++ b/src/routes/safe/store/actions/fetchSafe.js @@ -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) => ( + 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) => { + const safeRecord = await buildSafe(safe.toJSON()) + + return dispatch(updateSafe(safeRecord)) +} diff --git a/src/routes/safe/store/actions/fetchSafes.js b/src/routes/safe/store/actions/fetchSafes.js new file mode 100644 index 00000000..196c1b40 --- /dev/null +++ b/src/routes/safe/store/actions/fetchSafes.js @@ -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> => { + 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) => { + const storedSafes = load(SAFES_KEY) + const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map() + + return dispatch(updateSafes(safes)) +} diff --git a/src/routes/safe/store/actions/updateDailyLimit.js b/src/routes/safe/store/actions/updateDailyLimit.js deleted file mode 100644 index 7e6ff7aa..00000000 --- a/src/routes/safe/store/actions/updateDailyLimit.js +++ /dev/null @@ -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 diff --git a/src/routes/safe/store/actions/updateSafe.js b/src/routes/safe/store/actions/updateSafe.js new file mode 100644 index 00000000..89e30306 --- /dev/null +++ b/src/routes/safe/store/actions/updateSafe.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const UPDATE_SAFE = 'UPDATE_SAFE' + +const updateSafe = createAction(UPDATE_SAFE) + +export default updateSafe diff --git a/src/routes/safe/store/actions/updateSafes.js b/src/routes/safe/store/actions/updateSafes.js new file mode 100644 index 00000000..bc0daf24 --- /dev/null +++ b/src/routes/safe/store/actions/updateSafes.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const UPDATE_SAFES = 'UPDATE_SAFES' + +const updateSafesInBatch = createAction(UPDATE_SAFES) + +export default updateSafesInBatch diff --git a/src/routes/safe/store/model/safe.js b/src/routes/safe/store/model/safe.js index 22e10581..94c068b7 100644 --- a/src/routes/safe/store/model/safe.js +++ b/src/routes/safe/store/model/safe.js @@ -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, dailyLimit: DailyLimit, } @@ -15,7 +15,7 @@ export type SafeProps = { export const makeSafe: RecordFactory = Record({ name: '', address: '', - confirmations: 0, + threshold: 0, owners: List([]), dailyLimit: makeDailyLimit(), }) diff --git a/src/routes/safe/store/model/transaction.js b/src/routes/safe/store/model/transaction.js index 83a664d0..e810ed00 100644 --- a/src/routes/safe/store/model/transaction.js +++ b/src/routes/safe/store/model/transaction.js @@ -11,6 +11,7 @@ export type TransactionProps = { confirmations: List, destination: string, tx: string, + data: string, } export const makeTransaction: RecordFactory = Record({ @@ -21,6 +22,7 @@ export const makeTransaction: RecordFactory = Record({ destination: '', tx: '', threshold: 0, + data: '', }) export type Transaction = RecordOf diff --git a/src/routes/safe/store/reducer/safe.js b/src/routes/safe/store/reducer/safe.js index d430c49d..dd941407 100644 --- a/src/routes/safe/store/reducer/safe.js +++ b/src/routes/safe/store/reducer/safe.js @@ -1,36 +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 { 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 -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 = { key: string, @@ -43,11 +23,16 @@ action: AddSafeType */ export default handleActions({ + [UPDATE_SAFE]: (state: State, action: ActionType): State => + state.set(action.payload.get('address'), action.payload), + [UPDATE_SAFES]: (state: State, action: ActionType): State => + action.payload, [ADD_SAFE]: (state: State, action: ActionType): 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): State => - state.updateIn([action.payload.safeAddress, 'dailyLimit'], () => makeDailyLimit(action.payload.dailyLimit)), }, Map()) diff --git a/src/routes/safe/store/test/builder/safe.builder.js b/src/routes/safe/store/test/builder/safe.builder.js index a1dcc2a7..55c442cd 100644 --- a/src/routes/safe/store/test/builder/safe.builder.js +++ b/src/routes/safe/store/test/builder/safe.builder.js @@ -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 } diff --git a/src/routes/safe/store/test/dailyLimit.reducer.js b/src/routes/safe/store/test/dailyLimit.reducer.js deleted file mode 100644 index 9ee1eb5a..00000000 --- a/src/routes/safe/store/test/dailyLimit.reducer.js +++ /dev/null @@ -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 diff --git a/src/routes/safe/store/test/safe.reducer.js b/src/routes/safe/store/test/safe.reducer.js index 57edaed7..e3e8af81 100644 --- a/src/routes/safe/store/test/safe.reducer.js +++ b/src/routes/safe/store/test/safe.reducer.js @@ -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) - }) }) } diff --git a/src/routes/safe/store/test/safe.spec.js b/src/routes/safe/store/test/safe.spec.js index 3c431b4b..0db061e1 100644 --- a/src/routes/safe/store/test/safe.spec.js +++ b/src/routes/safe/store/test/safe.spec.js @@ -1,7 +1,6 @@ // @flow import balanceReducerTests from './balance.reducer' import safeReducerTests from './safe.reducer' -import dailyLimitReducerTests from './dailyLimit.reducer' import balanceSelectorTests from './balance.selector' import safeSelectorTests from './safe.selector' import grantedSelectorTests from './granted.selector' @@ -12,7 +11,6 @@ describe('Safe Test suite', () => { // ACTIONS AND REDUCERS safeReducerTests() balanceReducerTests() - dailyLimitReducerTests() // SAFE SELECTOR safeSelectorTests() diff --git a/src/routes/safe/test/Safe.multisig.1owners1threshold.test.js b/src/routes/safe/test/Safe.multisig.1owners1threshold.test.js index 1567f58c..68ef3ed3 100644 --- a/src/routes/safe/test/Safe.multisig.1owners1threshold.test.js +++ b/src/routes/safe/test/Safe.multisig.1owners1threshold.test.js @@ -45,7 +45,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => { // $FlowFixMe const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button) - const addTxButton = buttons[1] + 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]) diff --git a/src/routes/safe/test/Safe.multisig.3owners1threshold.test.js b/src/routes/safe/test/Safe.multisig.3owners1threshold.test.js index 2227b94e..0a566f46 100644 --- a/src/routes/safe/test/Safe.multisig.3owners1threshold.test.js +++ b/src/routes/safe/test/Safe.multisig.3owners1threshold.test.js @@ -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 diff --git a/src/routes/safe/test/Safe.multisig.3owners3threshold.test.js b/src/routes/safe/test/Safe.multisig.3owners3threshold.test.js index f57014f8..d5ca92ac 100644 --- a/src/routes/safe/test/Safe.multisig.3owners3threshold.test.js +++ b/src/routes/safe/test/Safe.multisig.3owners3threshold.test.js @@ -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')) }) }) diff --git a/src/routes/safe/test/Safe.owners.test.js b/src/routes/safe/test/Safe.owners.test.js new file mode 100644 index 00000000..59a464c7 --- /dev/null +++ b/src/routes/safe/test/Safe.owners.test.js @@ -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]) + }) +}) diff --git a/src/routes/safe/test/Safe.threshold.test.js b/src/routes/safe/test/Safe.threshold.test.js new file mode 100644 index 00000000..edd5837c --- /dev/null +++ b/src/routes/safe/test/Safe.threshold.test.js @@ -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) + }) +}) diff --git a/src/routes/safe/test/Safe.withdrawn.test.js b/src/routes/safe/test/Safe.withdrawn.test.js index a0803c57..d3df01bc 100644 --- a/src/routes/safe/test/Safe.withdrawn.test.js +++ b/src/routes/safe/test/Safe.withdrawn.test.js @@ -46,7 +46,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => { // $FlowFixMe const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button) - const withdrawnButton = buttons[0] + 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[1] + 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[0] + const addTxButton = buttons[2] expect(addTxButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT) expect(addTxButton.props.disabled).toBe(true) diff --git a/src/routes/safe/test/testMultisig.js b/src/routes/safe/test/testMultisig.js index c4264cd5..8e469410 100644 --- a/src/routes/safe/test/testMultisig.js +++ b/src/routes/safe/test/testMultisig.js @@ -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, + AddTransactionComponent: React$ElementType, + store: Store, +) => { // 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, 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[1] + 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) => { const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) // $FlowFixMe const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button) - const seeTx = buttons[2] + 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, 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, + 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, 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, ...statusses: string[]) => { const paragraphsWithOwners = getTagFromTransaction(SafeDom, 'h3') for (let i = 0; i < statusses.length; i += 1) { const ownerIndex = i + 6 diff --git a/src/routes/safeList/components/SafeTable.jsx b/src/routes/safeList/components/SafeTable.jsx index 9706f66c..c13a0eeb 100644 --- a/src/routes/safeList/components/SafeTable.jsx +++ b/src/routes/safeList/components/SafeTable.jsx @@ -32,7 +32,7 @@ const SafeTable = ({ safes }: Props) => ( {safe.get('name')} {safe.get('address')} - {safe.get('confirmations')} + {safe.get('threshold')} {safe.get('owners').count()} {`${safe.get('dailyLimit').get('value')} ETH`} diff --git a/src/store/index.js b/src/store/index.js index 61528341..c968b44d 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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 = combineReducers({ }) const initialState = { - [SAFE_REDUCER_ID]: safeInitialState(), [TRANSACTIONS_REDUCER_ID]: transactionsInitialState(), } diff --git a/src/utils/localStorage.js b/src/utils/localStorage.js index e537d38a..072ed733 100644 --- a/src/utils/localStorage.js +++ b/src/utils/localStorage.js @@ -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) => { + 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 => { + const data = load(`${OWNERS_KEY}-${safeAddress}`) + + return data ? Map(data) : Map() +} diff --git a/src/wallets/getWeb3.js b/src/wallets/getWeb3.js index 5e9602c5..8b5e43d3 100644 --- a/src/wallets/getWeb3.js +++ b/src/wallets/getWeb3.js @@ -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' diff --git a/src/wallets/safeContracts.js b/src/wallets/safeContracts.js index 81399db8..60a589ce 100644 --- a/src/wallets/safeContracts.js +++ b/src/wallets/safeContracts.js @@ -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 +}