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/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 }) => (
)}
/>
diff --git a/src/routes/safe/component/AddTransaction/createTransactions.js b/src/routes/safe/component/AddTransaction/createTransactions.js
index 7ac3956b..2bc545cc 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) => {
+ 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
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('confirmations'), 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('confirmations'), 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..19c1fdeb 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('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '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/index.jsx b/src/routes/safe/component/Safe/index.jsx
index 8e88fff2..788b2561 100644
--- a/src/routes/safe/component/Safe/index.jsx
+++ b/src/routes/safe/component/Safe/index.jsx
@@ -12,6 +12,7 @@ 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 Address from './Address'
import Balance from './Balance'
@@ -59,6 +60,12 @@ class GnoSafe extends React.PureComponent {
this.setState({ component: })
}
+ onEditThreshold = () => {
+ const { safe } = this.props
+
+ this.setState({ component: })
+ }
+
render() {
const { safe, balance } = this.props
const { component } = this.state
@@ -69,7 +76,7 @@ class GnoSafe extends React.PureComponent {
-
+
diff --git a/src/routes/safe/component/Threshold/actions.js b/src/routes/safe/component/Threshold/actions.js
new file mode 100644
index 00000000..e0b10a5a
--- /dev/null
+++ b/src/routes/safe/component/Threshold/actions.js
@@ -0,0 +1,16 @@
+// @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,
+}
diff --git a/src/routes/safe/component/Threshold/index.jsx b/src/routes/safe/component/Threshold/index.jsx
new file mode 100644
index 00000000..b2b66b66
--- /dev/null
+++ b/src/routes/safe/component/Threshold/index.jsx
@@ -0,0 +1,103 @@
+// @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 { 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 selector, { type SelectorProps } from './selector'
+import actions, { type Actions } from './actions'
+
+type Props = SelectorProps & Actions & {
+ numOwners: number,
+ safe: Safe,
+ onReset: () => void,
+}
+
+const THRESHOLD_PARAM = 'threshold'
+
+const ThresholdComponent = ({ numOwners, safe }: Props) => () => (
+
+
+ {'Change safe\'s threshold'}
+
+
+ {`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('confirmations')}`}
+
+
+
+
+
+)
+
+type State = {
+ initialValues: Object,
+}
+
+class Threshold extends React.PureComponent {
+ state = {
+ initialValues: {},
+ }
+
+ 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'))
+ }
+
+ render() {
+ const { numOwners, onReset, safe } = this.props
+
+ return (
+
+ {(submitting: boolean, submitSucceeded: boolean) => (
+
+
+
+
+
+ )}
+
+ )
+ }
+}
+
+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..e0b10a5a 100644
--- a/src/routes/safe/component/Transactions/actions.js
+++ b/src/routes/safe/component/Transactions/actions.js
@@ -1,10 +1,16 @@
// @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 = {
- fetchTransactions: typeof fetchTransactions,
+ fetchThreshold: FetchThreshold,
+ fetchTransactions: FetchTransactions,
}
export default {
+ fetchThreshold,
fetchTransactions,
}
diff --git a/src/routes/safe/component/Transactions/index.jsx b/src/routes/safe/component/Transactions/index.jsx
index 5dac8846..e8a22e87 100644
--- a/src/routes/safe/component/Transactions/index.jsx
+++ b/src/routes/safe/component/Transactions/index.jsx
@@ -17,10 +17,14 @@ 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, fetchThreshold,
+ } = this.props
+
await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress)
await sleep(1200)
fetchTransactions()
+ fetchThreshold(safeAddress)
}
render() {
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/store/actions/fetchThreshold.js b/src/routes/safe/store/actions/fetchThreshold.js
new file mode 100644
index 00000000..2ae28e69
--- /dev/null
+++ b/src/routes/safe/store/actions/fetchThreshold.js
@@ -0,0 +1,12 @@
+// @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) => {
+ const gnosisSafe = await getSafeEthereumInstance(safeAddress)
+ const actualThreshold = await gnosisSafe.getThreshold()
+
+ return dispatch(updateThreshold(safeAddress, actualThreshold))
+}
diff --git a/src/routes/safe/store/actions/updateThreshold.js b/src/routes/safe/store/actions/updateThreshold.js
new file mode 100644
index 00000000..b24f41fd
--- /dev/null
+++ b/src/routes/safe/store/actions/updateThreshold.js
@@ -0,0 +1,19 @@
+// @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
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..f8f64485 100644
--- a/src/routes/safe/store/reducer/safe.js
+++ b/src/routes/safe/store/reducer/safe.js
@@ -7,6 +7,7 @@ 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'
export const SAFE_REDUCER_ID = 'safes'
@@ -50,4 +51,6 @@ export default handleActions({
},
[UPDATE_DAILY_LIMIT]: (state: State, action: ActionType): State =>
state.updateIn([action.payload.safeAddress, 'dailyLimit'], () => makeDailyLimit(action.payload.dailyLimit)),
+ [UPDATE_THRESHOLD]: (state: State, action: ActionType): State =>
+ state.updateIn([action.payload.safeAddress, 'confirmations'], () => action.payload.threshold),
}, Map())
diff --git a/src/routes/safe/store/test/safe.spec.js b/src/routes/safe/store/test/safe.spec.js
index 3c431b4b..8a0ebd4d 100644
--- a/src/routes/safe/store/test/safe.spec.js
+++ b/src/routes/safe/store/test/safe.spec.js
@@ -2,6 +2,7 @@
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,6 +14,7 @@ describe('Safe Test suite', () => {
safeReducerTests()
balanceReducerTests()
dailyLimitReducerTests()
+ thresholdReducerTests()
// SAFE SELECTOR
safeSelectorTests()
diff --git a/src/routes/safe/store/test/threshold.reducer.js b/src/routes/safe/store/test/threshold.reducer.js
new file mode 100644
index 00000000..b1d483c3
--- /dev/null
+++ b/src/routes/safe/store/test/threshold.reducer.js
@@ -0,0 +1,48 @@
+// @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
diff --git a/src/routes/safe/test/Safe.multisig.1owners1threshold.test.js b/src/routes/safe/test/Safe.multisig.1owners1threshold.test.js
index 1567f58c..80d8b697 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[2]
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.threshold.test.js b/src/routes/safe/test/Safe.threshold.test.js
new file mode 100644
index 00000000..1ca38066
--- /dev/null
+++ b/src/routes/safe/test/Safe.threshold.test.js
@@ -0,0 +1,117 @@
+// @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 { 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 })
+ 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: Transaction = transactions.get(0)
+ 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)
+ 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 })
+ 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 })
+ expect(transactions.count()).toBe(1)
+
+ let thresholdTx: Transaction = transactions.get(0)
+ 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)
+ 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..a985b3cc 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[1]
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[2]
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[1]
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..272ce34d 100644
--- a/src/routes/safe/test/testMultisig.js
+++ b/src/routes/safe/test/testMultisig.js
@@ -43,7 +43,7 @@ export const addFundsTo = async (SafeDom, destination: string) => {
// $FlowFixMe
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
- const addTxButton = buttons[1]
+ const addTxButton = buttons[2]
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])
@@ -54,7 +54,7 @@ export const listTxsOf = (SafeDom) => {
// $FlowFixMe
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
- const seeTx = buttons[2]
+ const seeTx = buttons[3]
expect(seeTx.props.children).toEqual(SEE_MULTISIG_BUTTON_TEXT)
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(seeTx, 'button')[0])
}