Merge pull request #39 from gnosis/development

WA-234 & WA-235 - Threshold modifications & Add owners
This commit is contained in:
Adolfo Panizo 2018-06-08 10:50:59 +02:00 committed by GitHub
commit 90920b619c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 1090 additions and 237 deletions

View File

@ -1,2 +1,2 @@
// @flow
jest.setTimeout(30000)
jest.setTimeout(45000)

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -0,0 +1,27 @@
// @flow
import React from 'react'
import Checkbox, { type CheckoxProps } from 'material-ui/Checkbox'
class GnoCheckbox extends React.PureComponent<CheckoxProps> {
render() {
const {
input: {
checked, name, onChange, ...restInput
},
meta,
...rest
} = this.props
return (
<Checkbox
{...rest}
name={name}
inputProps={restInput}
onChange={onChange}
checked={!!checked}
/>
)
}
}
export default GnoCheckbox

View File

@ -29,7 +29,7 @@ const GnoForm = ({
render={({ handleSubmit, ...rest }) => (
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
{render(rest)}
{children(rest.submitting)}
{children(rest.submitting, rest.submitSucceeded)}
</form>
)}
/>

View File

@ -10,8 +10,12 @@ import PageFrame from '~/components/layout/PageFrame'
import { history, store } from '~/store'
import theme from '~/theme/mui'
import AppRoutes from '~/routes'
import fetchSafes from '~/routes/safe/store/actions/fetchSafes'
import './index.scss'
store.dispatch(fetchSafes())
const Root = () => (
<Provider store={store}>
<MuiThemeProvider theme={theme}>

View File

@ -0,0 +1,71 @@
// @flow
import * as React from 'react'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import Checkbox from '~/components/forms/Checkbox'
import { composeValidators, required, mustBeEthereumAddress, uniqueAddress } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading'
export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners'
export const NAME_PARAM = 'name'
export const OWNER_ADDRESS_PARAM = 'ownerAddress'
export const INCREASE_PARAM = 'increase'
export const safeFieldsValidation = (values: Object) => {
const errors = {}
if (Number.parseInt(values.owners, 10) < Number.parseInt(values.confirmations, 10)) {
errors.confirmations = CONFIRMATIONS_ERROR
}
return errors
}
type Props = {
numOwners: number,
threshold: number,
addresses: string[]
}
const AddOwnerForm = ({ addresses, numOwners, threshold }: Props) => () => (
<Block margin="md">
<Heading tag="h2" margin="lg">
Add Owner
</Heading>
<Heading tag="h4" margin="lg">
{`Actual number of owners: ${numOwners}, with threshold: ${threshold}`}
</Heading>
<Block margin="md">
<Field
name={NAME_PARAM}
component={TextField}
type="text"
validate={required}
placeholder="Owner Name*"
text="Owner Name*"
/>
</Block>
<Block margin="md">
<Field
name={OWNER_ADDRESS_PARAM}
component={TextField}
type="text"
validate={composeValidators(required, mustBeEthereumAddress, uniqueAddress(addresses))}
placeholder="Owner address*"
text="Owner address*"
/>
</Block>
<Block margin="md">
<Field
name={INCREASE_PARAM}
component={Checkbox}
type="checkbox"
/>
<Block>Increase owner?</Block>
</Block>
</Block>
)
export default AddOwnerForm

View File

@ -0,0 +1,43 @@
// @flow
import * as React from 'react'
import { CircularProgress } from 'material-ui/Progress'
import Block from '~/components/layout/Block'
import Bold from '~/components/layout/Bold'
import Heading from '~/components/layout/Heading'
import Paragraph from '~/components/layout/Paragraph'
import { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from '~/routes/safe/component/AddOwner/AddOwnerForm'
type FormProps = {
values: Object,
submitting: boolean,
}
const spinnerStyle = {
minHeight: '50px',
}
const Review = () => ({ values, submitting }: FormProps) => {
const text = values[INCREASE_PARAM]
? 'This operation will increase the threshold of the safe'
: 'This operation will not modify the threshold of the safe'
return (
<Block>
<Heading tag="h2">Review the Add Owner operation</Heading>
<Paragraph align="left">
<Bold>Owner Name: </Bold> {values[NAME_PARAM]}
</Paragraph>
<Paragraph align="left">
<Bold>Owner Address: </Bold> {values[OWNER_ADDRESS_PARAM]}
</Paragraph>
<Paragraph align="left">
<Bold>{text}</Bold>
</Paragraph>
<Block style={spinnerStyle}>
{ submitting && <CircularProgress size={50} /> }
</Block>
</Block>
)
}
export default Review

View File

@ -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,
}

View File

@ -0,0 +1,98 @@
// @flow
import * as React from 'react'
import { List } from 'immutable'
import Stepper from '~/components/Stepper'
import { connect } from 'react-redux'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Owner, makeOwner } from '~/routes/safe/store/model/owner'
import { getSafeEthereumInstance, createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
import { setOwners } from '~/utils/localStorage'
import AddOwnerForm, { NAME_PARAM, OWNER_ADDRESS_PARAM, INCREASE_PARAM } from './AddOwnerForm'
import Review from './Review'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'
const getSteps = () => [
'Fill Owner Form', 'Review Add order operation',
]
type Props = SelectorProps & Actions & {
safe: Safe,
threshold: number,
}
type State = {
done: boolean,
}
export const ADD_OWNER_RESET_BUTTON_TEXT = 'RESET'
const getOwnerAddressesFrom = (owners: List<Owner>) => {
if (!owners) {
return []
}
return owners.map((owner: Owner) => owner.get('address'))
}
class AddOwner extends React.Component<Props, State> {
state = {
done: false,
}
onAddOwner = async (values: Object) => {
try {
const {
safe, threshold, userAddress, fetchTransactions,
} = this.props
const nonce = Date.now()
const newThreshold = values[INCREASE_PARAM] ? threshold + 1 : threshold
const newOwnerAddress = values[OWNER_ADDRESS_PARAM]
const newOwnerName = values[NAME_PARAM]
const safeAddress = safe.get('address')
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
const data = gnosisSafe.contract.addOwnerWithThreshold.getData(newOwnerAddress, newThreshold)
await createTransaction(safe, `Add Owner ${newOwnerName}`, safeAddress, 0, nonce, userAddress, data)
setOwners(safeAddress, safe.get('owners').push(makeOwner({ name: newOwnerName, address: newOwnerAddress })))
fetchTransactions()
this.setState({ done: true })
} catch (error) {
this.setState({ done: false })
// eslint-disable-next-line
console.log('Error while adding owner ' + error)
}
}
onReset = () => {
this.setState({ done: false })
}
render() {
const { safe } = this.props
const { done } = this.state
const steps = getSteps()
const finishedButton = <Stepper.FinishButton title={ADD_OWNER_RESET_BUTTON_TEXT} />
const addresses = getOwnerAddressesFrom(safe.get('owners'))
return (
<React.Fragment>
<Stepper
finishedTransaction={done}
finishedButton={finishedButton}
onSubmit={this.onAddOwner}
steps={steps}
onReset={this.onReset}
>
<Stepper.Page numOwners={safe.get('owners').count()} threshold={safe.get('threshold')} addresses={addresses}>
{ AddOwnerForm }
</Stepper.Page>
<Stepper.Page>
{ Review }
</Stepper.Page>
</Stepper>
</React.Fragment>
)
}
}
export default connect(selector, actions)(AddOwner)

View File

@ -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,
})

View File

@ -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<Confirmation> = buildExecutedConfirmationFrom(safe.get('owners'), user)
return storeTransaction(txName, nonce, txDestination, txValue, user, executedConfirmations, txHash, safeAddress, safe.get('confirmations'))
return storeTransaction(txName, nonce, txDest, txValue, user, executedConfirmations, txHash, safeAddress, safe.get('threshold'), data)
}
const txConfirmationData = gnosisSafe.contract.approveTransactionWithParameters.getData(txDestination, valueInWei, '0x', CALL, nonce)
const txConfirmationData =
gnosisSafe.contract.approveTransactionWithParameters.getData(txDest, valueInWei, data, CALL, nonce)
const txConfirmationHash = await executeTransaction(txConfirmationData, user, safeAddress)
checkReceiptStatus(txConfirmationHash)
const confirmations: List<Confirmation> = buildConfirmationsFrom(safe.get('owners'), user, txConfirmationHash)
return storeTransaction(txName, nonce, txDestination, txValue, user, confirmations, '', safeAddress, safe.get('confirmations'))
return storeTransaction(txName, nonce, txDest, txValue, user, confirmations, '', safeAddress, safe.get('threshold'), data)
}

View File

@ -33,7 +33,7 @@ describe('Transactions Suite', () => {
const txName = 'Buy butteries for project'
const nonce: number = 10
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, 'foo', 'confirmationHash')
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('confirmations'))
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
// WHEN
const transactions: Map<string, List<Transaction>> = 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<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(firstTxName, firstNonce, destination, value, creator, confirmations, '', safeAddress, safe.get('confirmations'))
storeTransaction(firstTxName, firstNonce, destination, value, creator, confirmations, '', safeAddress, safe.get('threshold'), '0x')
const secondTxName = 'Buy printers for project'
const secondNonce: number = firstNonce + 100
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(secondTxName, secondNonce, destination, value, creator, secondConfirmations, '', safeAddress, safe.get('confirmations'))
storeTransaction(secondTxName, secondNonce, destination, value, creator, secondConfirmations, '', safeAddress, safe.get('threshold'), '0x')
// WHEN
const transactions: Map<string, List<Transaction>> = 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<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safeAddress, safe.get('confirmations'))
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safeAddress, safe.get('threshold'), '0x')
const secondSafe = SafeFactory.dailyLimitSafe(10, 2)
const txSecondName = 'Buy batteris for Beta project'
@ -92,7 +92,7 @@ describe('Transactions Suite', () => {
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(secondSafe.get('owners'), secondCreator, 'confirmationHash')
storeTransaction(
txSecondName, txSecondNonce, destination, value, secondCreator,
secondConfirmations, '', secondSafeAddress, secondSafe.get('confirmations'),
secondConfirmations, '', secondSafeAddress, secondSafe.get('threshold'), '0x',
)
let transactions: Map<string, List<Transaction>> = loadSafeTransactions()
@ -112,7 +112,7 @@ describe('Transactions Suite', () => {
const txConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'secondConfirmationHash')
storeTransaction(
txFirstName, txFirstNonce, destination, value, creator,
txConfirmations, '', safe.get('address'), safe.get('confirmations'),
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<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'))
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
// WHEN
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<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'))
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
// WHEN
const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
@ -185,7 +185,7 @@ describe('Transactions Suite', () => {
const nonce: number = 10
const tx = ''
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('confirmations'))
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('threshold'), '0x')
expect(createTxFnc).toThrow(/The tx should be mined before storing it in safes with one owner/)
})
@ -197,7 +197,7 @@ describe('Transactions Suite', () => {
const nonce: number = 10
const tx = 'validTxHash'
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('confirmations'))
storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('threshold'), '0x')
// WHEN
const safeTransactions: Map<string, List<Transaction>> = loadSafeTransactions()

View File

@ -20,7 +20,7 @@ export const testSizeOfTransactions = (safeTxs: List<Transaction> | typeof undef
export const testTransactionFrom = (
safeTxs: List<Transaction> | 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<Confirmation> = tx.get('confirmations')
const firstConfirmation: Confirmation | typeof undefined = confirmations.get(0)

View File

@ -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) => (
<ListItem>
<Avatar>
<DoneAll />
@ -19,6 +23,13 @@ const Confirmations = ({ confirmations }: Props) => (
secondary={`${confirmations} required confirmations per transaction`}
cut
/>
<Button
variant="raised"
color="primary"
onClick={onEditThreshold}
>
{EDIT_THRESHOLD_BUTTON_TEXT}
</Button>
</ListItem>
)

View File

@ -6,6 +6,7 @@ import Collapse from 'material-ui/transitions/Collapse'
import ListItemText from '~/components/List/ListItemText'
import List, { ListItem, ListItemIcon } from 'material-ui/List'
import Avatar from 'material-ui/Avatar'
import Button from '~/components/layout/Button'
import Group from 'material-ui-icons/Group'
import Person from 'material-ui-icons/Person'
import ExpandLess from 'material-ui-icons/ExpandLess'
@ -21,10 +22,13 @@ const styles = {
type Props = Open & WithStyles & {
owners: List<OwnerProps>,
onAddOwner: () => void,
}
export const ADD_OWNER_BUTTON_TEXT = 'Add'
const Owners = openHoc(({
open, toggle, owners, classes,
open, toggle, owners, classes, onAddOwner,
}: Props) => (
<React.Fragment>
<ListItem onClick={toggle}>
@ -35,6 +39,13 @@ const Owners = openHoc(({
<ListItemIcon>
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemIcon>
<Button
variant="raised"
color="primary"
onClick={onAddOwner}
>
{ADD_OWNER_BUTTON_TEXT}
</Button>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>

View File

@ -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<SafeProps, State> {
this.setState({ component: <Transactions safeName={safe.get('name')} safeAddress={safe.get('address')} onAddTx={this.onAddTx} /> })
}
onEditThreshold = () => {
const { safe } = this.props
this.setState({ component: <Threshold numOwners={safe.get('owners').count()} safe={safe} /> })
}
onAddOwner = (e: SyntheticEvent<HTMLButtonElement>) => {
const { safe } = this.props
e.stopPropagation()
this.setState({ component: <AddOwner threshold={safe.get('threshold')} safe={safe} /> })
}
render() {
const { safe, balance } = this.props
const { component } = this.state
@ -68,8 +82,8 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
<Col sm={12} top="xs" md={5} margin="xl" overflow>
<List style={listStyle}>
<Balance balance={balance} />
<Owners owners={safe.owners} />
<Confirmations confirmations={safe.get('confirmations')} />
<Owners owners={safe.owners} onAddOwner={this.onAddOwner} />
<Confirmations confirmations={safe.get('threshold')} onEditThreshold={this.onEditThreshold} />
<Address address={safe.get('address')} />
<DailyLimit balance={balance} dailyLimit={safe.get('dailyLimit')} onWithdrawn={this.onWithdrawn} />
<MultisigTx balance={balance} onAddTx={this.onAddTx} onSeeTxs={this.onListTransactions} />

View File

@ -0,0 +1,31 @@
// @flow
import * as React from 'react'
import { CircularProgress } from 'material-ui/Progress'
import Block from '~/components/layout/Block'
import Bold from '~/components/layout/Bold'
import Heading from '~/components/layout/Heading'
import Paragraph from '~/components/layout/Paragraph'
import { THRESHOLD_PARAM } from '~/routes/safe/component/Threshold/ThresholdForm'
type FormProps = {
values: Object,
submitting: boolean,
}
const spinnerStyle = {
minHeight: '50px',
}
const Review = () => ({ values, submitting }: FormProps) => (
<Block>
<Heading tag="h2">Review the Threshold operation</Heading>
<Paragraph align="left">
<Bold>The new threshold will be: </Bold> {values[THRESHOLD_PARAM]}
</Paragraph>
<Block style={spinnerStyle}>
{ submitting && <CircularProgress size={50} /> }
</Block>
</Block>
)
export default Review

View File

@ -0,0 +1,43 @@
// @flow
import * as React from 'react'
import Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { composeValidators, minValue, maxValue, mustBeInteger, required } from '~/components/forms/validator'
import { type Safe } from '~/routes/safe/store/model/safe'
export const THRESHOLD_PARAM = 'threshold'
type ThresholdProps = {
numOwners: number,
safe: Safe,
}
const ThresholdForm = ({ numOwners, safe }: ThresholdProps) => () => (
<Block margin="md">
<Heading tag="h2" margin="lg">
{'Change safe\'s threshold'}
</Heading>
<Heading tag="h4" margin="lg">
{`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('threshold')}`}
</Heading>
<Block margin="md">
<Field
name={THRESHOLD_PARAM}
component={TextField}
type="text"
validate={composeValidators(
required,
mustBeInteger,
minValue(1),
maxValue(numOwners),
)}
placeholder="New threshold"
text="Safe's threshold"
/>
</Block>
</Block>
)
export default ThresholdForm

View File

@ -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,
}

View File

@ -0,0 +1,81 @@
// @flow
import * as React from 'react'
import Stepper from '~/components/Stepper'
import { connect } from 'react-redux'
import { getSafeEthereumInstance, createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
import { type Safe } from '~/routes/safe/store/model/safe'
import ThresholdForm, { THRESHOLD_PARAM } from './ThresholdForm'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'
import Review from './Review'
type Props = SelectorProps & Actions & {
numOwners: number,
safe: Safe,
onReset: () => void,
}
const getSteps = () => [
'Fill Change threshold Form', 'Review change threshold operation',
]
type State = {
done: boolean,
}
export const CHANGE_THRESHOLD_RESET_BUTTON_TEXT = 'RESET'
class Threshold extends React.PureComponent<Props, State> {
state = {
done: false,
}
onThreshold = async (values: Object) => {
try {
const { safe, userAddress } = this.props // , fetchThreshold } = this.props
const newThreshold = values[THRESHOLD_PARAM]
const gnosisSafe = await getSafeEthereumInstance(safe.get('address'))
const nonce = Date.now()
const data = gnosisSafe.contract.changeThreshold.getData(newThreshold)
await createTransaction(safe, `Change Safe's threshold [${nonce}]`, safe.get('address'), 0, nonce, userAddress, data)
await this.props.fetchTransactions()
this.setState({ done: true })
} catch (error) {
this.setState({ done: false })
// eslint-disable-next-line
console.log('Error while changing threshold ' + error)
}
}
onReset = () => {
this.setState({ done: false })
}
render() {
const { numOwners, safe } = this.props
const { done } = this.state
const steps = getSteps()
const finishedButton = <Stepper.FinishButton title={CHANGE_THRESHOLD_RESET_BUTTON_TEXT} />
return (
<React.Fragment>
<Stepper
finishedTransaction={done}
finishedButton={finishedButton}
onSubmit={this.onThreshold}
steps={steps}
onReset={this.onReset}
>
<Stepper.Page numOwners={numOwners} safe={safe}>
{ ThresholdForm }
</Stepper.Page>
<Stepper.Page>
{ Review }
</Stepper.Page>
</Stepper>
</React.Fragment>
)
}
}
export default connect(selector, actions)(Threshold)

View File

@ -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,
})

View File

@ -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 {

View File

@ -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<Props, {}> {
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()
}

View File

@ -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,
)
}

View File

@ -1,10 +0,0 @@
// @flow
import fetchDailyLimit from '~/routes/safe/store/actions/fetchDailyLimit'
export type Actions = {
fetchDailyLimit: typeof fetchDailyLimit,
}
export default {
fetchDailyLimit,
}

View File

@ -2,9 +2,7 @@
import * as React from 'react'
import { connect } from 'react-redux'
import Stepper from '~/components/Stepper'
import { sleep } from '~/utils/timer'
import { type DailyLimit } from '~/routes/safe/store/model/dailyLimit'
import actions, { type Actions } from './actions'
import selector, { type SelectorProps } from './selector'
import withdrawn from './withdrawn'
import WithdrawnForm from './WithdrawnForm'
@ -14,7 +12,7 @@ const getSteps = () => [
'Fill Withdrawn Form', 'Review Withdrawn',
]
type Props = SelectorProps & Actions & {
type Props = SelectorProps & {
safeAddress: string,
dailyLimit: DailyLimit,
}
@ -34,8 +32,6 @@ class Withdrawn extends React.Component<Props, State> {
try {
const { safeAddress, userAddress } = this.props
await withdrawn(values, safeAddress, userAddress)
await sleep(3500)
this.props.fetchDailyLimit(safeAddress)
this.setState({ done: true })
} catch (error) {
this.setState({ done: false })
@ -75,5 +71,5 @@ class Withdrawn extends React.Component<Props, State> {
}
}
export default connect(selector, actions)(Withdrawn)
export default connect(selector)(Withdrawn)

View File

@ -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,
}

View File

@ -14,17 +14,12 @@ type Props = Actions & SelectorProps & {
class SafeView extends React.PureComponent<Props> {
componentDidMount() {
this.intervalId = setInterval(() => {
const { safe, fetchBalance } = this.props
const { safe, fetchSafe, fetchBalance } = this.props
if (!safe) { return }
const safeAddress: string = safe.get('address')
fetchBalance(safeAddress)
fetchSafe(safe)
}, 1500)
const { fetchDailyLimit, safe } = this.props
if (safe) {
fetchDailyLimit(safe.get('address'))
}
}
componentWillUnmount() {

View File

@ -20,14 +20,14 @@ const addSafe = createAction(
ADD_SAFE,
(
name: string, address: string,
confirmations: number, limit: number,
threshold: number, limit: number,
ownersName: string[], ownersAddress: string[],
): SafeProps => {
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
const dailyLimit: DailyLimit = buildDailyLimitFrom(limit)
return ({
address, name, confirmations, owners, dailyLimit,
address, name, threshold, owners, dailyLimit,
})
},
)

View File

@ -1,13 +0,0 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index'
import { getDailyLimitFrom } from '~/routes/safe/component/Withdrawn/withdrawn'
import { type DailyLimitProps } from '~/routes/safe/store/model/dailyLimit'
import updateDailyLimit from './updateDailyLimit'
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
const ethAddress = 0
const dailyLimit: DailyLimitProps = await getDailyLimitFrom(safeAddress, ethAddress)
return dispatch(updateDailyLimit(safeAddress, dailyLimit))
}

View File

@ -0,0 +1,43 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { List, Map } from 'immutable'
import { type GlobalState } from '~/store/index'
import { makeOwner } from '~/routes/safe/store/model/owner'
import { type SafeProps, type Safe, makeSafe } from '~/routes/safe/store/model/safe'
import { makeDailyLimit } from '~/routes/safe/store/model/dailyLimit'
import { getDailyLimitFrom } from '~/routes/safe/component/Withdrawn/withdrawn'
import { getGnosisSafeInstanceAt } from '~/wallets/safeContracts'
import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { getOwners } from '~/utils/localStorage'
const buildOwnersFrom = (safeOwners: string[], storedOwners: Map<string, string>) => (
safeOwners.map((ownerAddress: string) => {
const ownerName = storedOwners.get(ownerAddress.toLowerCase()) || 'UNKNOWN'
return makeOwner({ name: ownerName, address: ownerAddress })
})
)
export const buildSafe = async (storedSafe: Object) => {
const safeAddress = storedSafe.address
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const dailyLimit = makeDailyLimit(await getDailyLimitFrom(safeAddress, 0))
const threshold = Number(await gnosisSafe.getThreshold())
const owners = List(buildOwnersFrom(await gnosisSafe.getOwners(), getOwners(safeAddress)))
const safe: SafeProps = {
address: safeAddress,
dailyLimit,
name: storedSafe.name,
threshold,
owners,
}
return makeSafe(safe)
}
export default (safe: Safe) => async (dispatch: ReduxDispatch<GlobalState>) => {
const safeRecord = await buildSafe(safe.toJSON())
return dispatch(updateSafe(safeRecord))
}

View File

@ -0,0 +1,26 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { Map } from 'immutable'
import { type GlobalState } from '~/store/index'
import { load, SAFES_KEY } from '~/utils/localStorage'
import updateSafes from '~/routes/safe/store/actions/updateSafes'
import { buildSafe } from '~/routes/safe/store/actions/fetchSafe'
import { type Safe } from '~/routes/safe/store/model/safe'
const buildSafesFrom = async (loadedSafes: Object): Promise<Map<string, Safe>> => {
const safes = Map()
const keys = Object.keys(loadedSafes)
const safeRecords = await Promise.all(keys.map((address: string) => buildSafe(loadedSafes[address])))
return safes.withMutations(async (map) => {
safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe))
})
}
export default () => async (dispatch: ReduxDispatch<GlobalState>) => {
const storedSafes = load(SAFES_KEY)
const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map()
return dispatch(updateSafes(safes))
}

View File

@ -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

View File

@ -0,0 +1,8 @@
// @flow
import { createAction } from 'redux-actions'
export const UPDATE_SAFE = 'UPDATE_SAFE'
const updateSafe = createAction(UPDATE_SAFE)
export default updateSafe

View File

@ -0,0 +1,8 @@
// @flow
import { createAction } from 'redux-actions'
export const UPDATE_SAFES = 'UPDATE_SAFES'
const updateSafesInBatch = createAction(UPDATE_SAFES)
export default updateSafesInBatch

View File

@ -7,7 +7,7 @@ import type { Owner } from '~/routes/safe/store/model/owner'
export type SafeProps = {
name: string,
address: string,
confirmations: number,
threshold: number,
owners: List<Owner>,
dailyLimit: DailyLimit,
}
@ -15,7 +15,7 @@ export type SafeProps = {
export const makeSafe: RecordFactory<SafeProps> = Record({
name: '',
address: '',
confirmations: 0,
threshold: 0,
owners: List([]),
dailyLimit: makeDailyLimit(),
})

View File

@ -11,6 +11,7 @@ export type TransactionProps = {
confirmations: List<Confirmation>,
destination: string,
tx: string,
data: string,
}
export const makeTransaction: RecordFactory<TransactionProps> = Record({
@ -21,6 +22,7 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
destination: '',
tx: '',
threshold: 0,
data: '',
})
export type Transaction = RecordOf<TransactionProps>

View File

@ -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<string, Safe>
const buildSafesFrom = (loadedSafes: Object): State => {
const safes: State = Map()
return safes.withMutations((map: State) => {
Object.keys(loadedSafes).forEach((address) => {
const safe = loadedSafes[address]
safe.owners = List(safe.owners.map((owner => makeOwner(owner))))
safe.dailyLimit = makeDailyLimit({ value: safe.dailyLimit.value, spentToday: safe.dailyLimit.spentToday })
return map.set(address, makeSafe(safe))
})
})
}
export const safeInitialState = (): State => {
const storedSafes = load(SAFES_KEY)
return storedSafes ? buildSafesFrom(storedSafes) : Map()
}
/*
type Action<T> = {
key: string,
@ -43,11 +23,16 @@ action: AddSafeType
*/
export default handleActions({
[UPDATE_SAFE]: (state: State, action: ActionType<typeof updateSafe>): State =>
state.set(action.payload.get('address'), action.payload),
[UPDATE_SAFES]: (state: State, action: ActionType<typeof updateSafes>): State =>
action.payload,
[ADD_SAFE]: (state: State, action: ActionType<typeof addSafe>): State => {
const safes = state.set(action.payload.address, makeSafe(action.payload))
const safe: Safe = makeSafe(action.payload)
setOwners(safe.get('address'), safe.get('owners'))
const safes = state.set(action.payload.address, safe)
saveSafes(safes.toJSON())
return safes
},
[UPDATE_DAILY_LIMIT]: (state: State, action: ActionType<typeof updateDailyLimit>): State =>
state.updateIn([action.payload.safeAddress, 'dailyLimit'], () => makeDailyLimit(action.payload.dailyLimit)),
}, Map())

View File

@ -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
}

View File

@ -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

View File

@ -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)
})
})
}

View File

@ -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()

View File

@ -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])

View File

@ -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

View File

@ -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'))
})
})

View File

@ -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])
})
})

View File

@ -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)
})
})

View File

@ -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)

View File

@ -9,8 +9,13 @@ import SafeView from '~/routes/safe/component/Safe'
import TransactionsComponent from '~/routes/safe/component/Transactions'
import TransactionComponent from '~/routes/safe/component/Transactions/Transaction'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
import { type GlobalState } from '~/store/index'
export const createMultisigTxFilling = async (SafeDom, AddTransactionComponent, store) => {
export const createMultisigTxFilling = async (
SafeDom: React$Component<any, any>,
AddTransactionComponent: React$ElementType,
store: Store<GlobalState>,
) => {
// Get AddTransaction form component
const AddTransaction = TestUtils.findRenderedComponentWithType(SafeDom, AddTransactionComponent)
@ -36,30 +41,30 @@ export const checkBalanceOf = async (addressToTest: string, value: string) => {
expect(safeBalance).toBe(value)
}
export const addFundsTo = async (SafeDom, destination: string) => {
export const addFundsTo = async (SafeDom: React$Component<any, any>, destination: string) => {
// add funds to safe
await addEtherTo(destination, '0.1')
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
// $FlowFixMe
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
const addTxButton = buttons[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<any, any>) => {
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<any, any>, tag: string) => {
const Transactions = TestUtils.findRenderedComponentWithType(SafeDom, TransactionsComponent)
if (!Transactions) throw new Error()
const Transaction = TestUtils.findRenderedComponentWithType(Transactions, TransactionComponent)
@ -68,7 +73,11 @@ export const getTagFromTransaction = (SafeDom, tag: string) => {
return TestUtils.scryRenderedDOMComponentsWithTag(Transaction, tag)
}
export const expandTransactionOf = async (SafeDom, numOwners, safeThreshold) => {
export const expandTransactionOf = async (
SafeDom: React$Component<any, any>,
numOwners: number,
safeThreshold: number,
) => {
const paragraphs = getTagFromTransaction(SafeDom, 'p')
TestUtils.Simulate.click(paragraphs[2]) // expanded
await sleep(1000) // Time to expand
@ -80,13 +89,13 @@ export const expandTransactionOf = async (SafeDom, numOwners, safeThreshold) =>
expect(paragraphsExpanded.length).toBe(paragraphs.length + numOwners)
}
export const getTransactionFromReduxStore = (store, address) => {
export const getTransactionFromReduxStore = (store: Store<GlobalState>, address: string, index: number = 0) => {
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
return transactions.get(0)
return transactions.get(index)
}
export const confirmOwners = async (SafeDom, ...statusses: string[]) => {
export const confirmOwners = async (SafeDom: React$Component<any, any>, ...statusses: string[]) => {
const paragraphsWithOwners = getTagFromTransaction(SafeDom, 'h3')
for (let i = 0; i < statusses.length; i += 1) {
const ownerIndex = i + 6

View File

@ -32,7 +32,7 @@ const SafeTable = ({ safes }: Props) => (
</TableCell>
<TableCell padding="none">{safe.get('name')}</TableCell>
<TableCell padding="none">{safe.get('address')}</TableCell>
<TableCell padding="none" numeric>{safe.get('confirmations')}</TableCell>
<TableCell padding="none" numeric>{safe.get('threshold')}</TableCell>
<TableCell padding="none" numeric>{safe.get('owners').count()}</TableCell>
<TableCell padding="none" numeric>{`${safe.get('dailyLimit').get('value')} ETH`}</TableCell>
</TableRow>

View File

@ -4,7 +4,7 @@ import { routerMiddleware, routerReducer } from 'react-router-redux'
import { combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store } from 'redux'
import thunk from 'redux-thunk'
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/wallets/store/reducer/provider'
import safe, { SAFE_REDUCER_ID, safeInitialState, type State as SafeState } from '~/routes/safe/store/reducer/safe'
import safe, { SAFE_REDUCER_ID, type State as SafeState } from '~/routes/safe/store/reducer/safe'
import balances, { BALANCE_REDUCER_ID, type State as BalancesState } from '~/routes/safe/store/reducer/balances'
import transactions, { type State as TransactionsState, transactionsInitialState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
@ -33,7 +33,6 @@ const reducers: Reducer<GlobalState> = combineReducers({
})
const initialState = {
[SAFE_REDUCER_ID]: safeInitialState(),
[TRANSACTIONS_REDUCER_ID]: transactionsInitialState(),
}

View File

@ -1,6 +1,10 @@
// @flow
import { List, Map } from 'immutable'
import { type Owner } from '~/routes/safe/store/model/owner'
export const SAFES_KEY = 'SAFES'
export const TX_KEY = 'TX'
export const OWNERS_KEY = 'OWNERS'
export const load = (key: string) => {
try {
@ -27,3 +31,19 @@ export const saveSafes = (safes: Object) => {
// Ignore write errors
}
}
export const setOwners = (safeAddress: string, owners: List<Owner>) => {
try {
const ownersAsMap = Map(owners.map((owner: Owner) => [owner.get('address').toLowerCase(), owner.get('name')]))
const serializedState = JSON.stringify(ownersAsMap)
localStorage.setItem(`${OWNERS_KEY}-${safeAddress}`, serializedState)
} catch (err) {
// Ignore write errors
}
}
export const getOwners = (safeAddress: string): Map<string, string> => {
const data = load(`${OWNERS_KEY}-${safeAddress}`)
return data ? Map(data) : Map()
}

View File

@ -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'

View File

@ -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
}