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 // @flow
jest.setTimeout(30000) jest.setTimeout(45000)

View File

@ -1276,8 +1276,14 @@
"links": {}, "links": {},
"address": "0x5fd674bc2873513f8e5a19d69637d0211e476380", "address": "0x5fd674bc2873513f8e5a19d69637d0211e476380",
"transactionHash": "0x288775644c087eed5e41b96fecebdb23be4e6d40bef5b6fb9a2876c2a3145157" "transactionHash": "0x288775644c087eed5e41b96fecebdb23be4e6d40bef5b6fb9a2876c2a3145157"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0x194371bd036c34314ded31a7ffe7e66f5461a62d",
"transactionHash": "0x288775644c087eed5e41b96fecebdb23be4e6d40bef5b6fb9a2876c2a3145157"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.138Z" "updatedAt": "2018-06-06T14:51:43.695Z"
} }

View File

@ -6699,8 +6699,14 @@
"links": {}, "links": {},
"address": "0x3bdceb07fddd50d259a059ca9a75ecda561d4afc", "address": "0x3bdceb07fddd50d259a059ca9a75ecda561d4afc",
"transactionHash": "0xf501438a4ec967e2928d922e4af568a2a5365002f8b3f9e32117bbacfaa49331" "transactionHash": "0xf501438a4ec967e2928d922e4af568a2a5365002f8b3f9e32117bbacfaa49331"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0x0fb244fb862c95c66250c6c72e7e91a3d41a47d5",
"transactionHash": "0xf501438a4ec967e2928d922e4af568a2a5365002f8b3f9e32117bbacfaa49331"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.129Z" "updatedAt": "2018-06-06T14:51:43.688Z"
} }

View File

@ -9864,8 +9864,14 @@
"links": {}, "links": {},
"address": "0x8c55b458a53e8c6e9efa7f54e7be9ca76b43dd9b", "address": "0x8c55b458a53e8c6e9efa7f54e7be9ca76b43dd9b",
"transactionHash": "0x67117c1452ee2f4b904621b6f30790ff998d1f1a72f11c6b71ef47e3dd254724" "transactionHash": "0x67117c1452ee2f4b904621b6f30790ff998d1f1a72f11c6b71ef47e3dd254724"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0xc3d42147f9b51c9749d3362e8b9ee6cb94861778",
"transactionHash": "0x67117c1452ee2f4b904621b6f30790ff998d1f1a72f11c6b71ef47e3dd254724"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.124Z" "updatedAt": "2018-06-06T14:51:43.672Z"
} }

View File

@ -6876,8 +6876,14 @@
"links": {}, "links": {},
"address": "0xd4edae2f2d5718d1798deb48c062b939d6e9d4f4", "address": "0xd4edae2f2d5718d1798deb48c062b939d6e9d4f4",
"transactionHash": "0xa71d3b0b3752acc18733fa881f70c256d63562f28ccca9af910fad3beee9181a" "transactionHash": "0xa71d3b0b3752acc18733fa881f70c256d63562f28ccca9af910fad3beee9181a"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0xf2bc99498b610a01d76358d8e2fe251c9783a216",
"transactionHash": "0xa71d3b0b3752acc18733fa881f70c256d63562f28ccca9af910fad3beee9181a"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.114Z" "updatedAt": "2018-06-06T14:51:43.677Z"
} }

View File

@ -1398,8 +1398,14 @@
"links": {}, "links": {},
"address": "0x2ebea54cbbd4f5491deba7a37605f8f0be3e3c9b", "address": "0x2ebea54cbbd4f5491deba7a37605f8f0be3e3c9b",
"transactionHash": "0xb6a19a7a679a1474c09c651e4151421f210afa3f47effed019d4c0206144ee5f" "transactionHash": "0xb6a19a7a679a1474c09c651e4151421f210afa3f47effed019d4c0206144ee5f"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0x4aa39923aa66871debec9b8aa9008a3b220eb1df",
"transactionHash": "0xb6a19a7a679a1474c09c651e4151421f210afa3f47effed019d4c0206144ee5f"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.139Z" "updatedAt": "2018-06-06T14:51:43.697Z"
} }

View File

@ -360,8 +360,14 @@
"links": {}, "links": {},
"address": "0x3946fcaaa0ba21aaffc5e06a3cc45debc9e07f7f", "address": "0x3946fcaaa0ba21aaffc5e06a3cc45debc9e07f7f",
"transactionHash": "0xd044f1662e339061a8cabf2b06ac94a9f86fcccf3f5d80ebd1bea2a7542d4021" "transactionHash": "0xd044f1662e339061a8cabf2b06ac94a9f86fcccf3f5d80ebd1bea2a7542d4021"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0xef9e7829f057e4d640bf66e17e06b3ab5cae508d",
"transactionHash": "0xd044f1662e339061a8cabf2b06ac94a9f86fcccf3f5d80ebd1bea2a7542d4021"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.139Z" "updatedAt": "2018-06-06T14:51:43.696Z"
} }

View File

@ -1011,8 +1011,14 @@
"links": {}, "links": {},
"address": "0xbefd9f4a40b1bec8ec730969a3508d1739fb2742", "address": "0xbefd9f4a40b1bec8ec730969a3508d1739fb2742",
"transactionHash": "0x75ad1066b44cd801ac66a316dbe4c09e72636d72b70fd62eb647295a0fc5e285" "transactionHash": "0x75ad1066b44cd801ac66a316dbe4c09e72636d72b70fd62eb647295a0fc5e285"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0xaa973df8ec251cf67ec387d5627d42dbb738605f",
"transactionHash": "0x75ad1066b44cd801ac66a316dbe4c09e72636d72b70fd62eb647295a0fc5e285"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.112Z" "updatedAt": "2018-06-06T14:51:43.668Z"
} }

View File

@ -7310,8 +7310,14 @@
"links": {}, "links": {},
"address": "0xfb1771240bb7edf209c70bd520a5d5424d23b084", "address": "0xfb1771240bb7edf209c70bd520a5d5424d23b084",
"transactionHash": "0xf0cd95843453bdac02ad8018ef507479ea62989e56d69ad0ac1aad9d3a8515d2" "transactionHash": "0xf0cd95843453bdac02ad8018ef507479ea62989e56d69ad0ac1aad9d3a8515d2"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0x9be4b89520d1dd6f2d115192587689c6c9bd1a99",
"transactionHash": "0xf0cd95843453bdac02ad8018ef507479ea62989e56d69ad0ac1aad9d3a8515d2"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.135Z" "updatedAt": "2018-06-06T14:51:43.692Z"
} }

View File

@ -5881,8 +5881,14 @@
"links": {}, "links": {},
"address": "0x589fd9eea7cca488a80e17a5105befff9616f11d", "address": "0x589fd9eea7cca488a80e17a5105befff9616f11d",
"transactionHash": "0x0396e1c9da4fa7bd313286e6033446dbb6e491f267956f8cf13202ce534fd0e6" "transactionHash": "0x0396e1c9da4fa7bd313286e6033446dbb6e491f267956f8cf13202ce534fd0e6"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0xe8abcdb37db8e7c563de32c5d207433ce44d1445",
"transactionHash": "0x0396e1c9da4fa7bd313286e6033446dbb6e491f267956f8cf13202ce534fd0e6"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.118Z" "updatedAt": "2018-06-06T14:51:43.684Z"
} }

View File

@ -4354,8 +4354,14 @@
"links": {}, "links": {},
"address": "0xca574a31a4cf1eeabeecaffc555bdc7f91c5caf9", "address": "0xca574a31a4cf1eeabeecaffc555bdc7f91c5caf9",
"transactionHash": "0x463374c2fbc7eaff5b87e65c6a8fdc1177ef82c66084df6e7b88b506f99b193c" "transactionHash": "0x463374c2fbc7eaff5b87e65c6a8fdc1177ef82c66084df6e7b88b506f99b193c"
},
"1528296677763": {
"events": {},
"links": {},
"address": "0x66c535e20f0c90530431ebab626da0ebdd55ec2d",
"transactionHash": "0x463374c2fbc7eaff5b87e65c6a8fdc1177ef82c66084df6e7b88b506f99b193c"
} }
}, },
"schemaVersion": "2.0.0", "schemaVersion": "2.0.0",
"updatedAt": "2018-06-04T10:56:37.132Z" "updatedAt": "2018-06-06T14:51:43.700Z"
} }

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 }) => ( render={({ handleSubmit, ...rest }) => (
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}> <form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
{render(rest)} {render(rest)}
{children(rest.submitting)} {children(rest.submitting, rest.submitSucceeded)}
</form> </form>
)} )}
/> />

View File

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

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, tx: string,
safeAddress: string, safeAddress: string,
safeThreshold: number, safeThreshold: number,
data: string,
) => { ) => {
const notMinedWhenOneOwnerSafe = confirmations.count() === 1 && !tx const notMinedWhenOneOwnerSafe = confirmations.count() === 1 && !tx
if (notMinedWhenOneOwnerSafe) { if (notMinedWhenOneOwnerSafe) {
@ -54,7 +55,7 @@ export const storeTransaction = (
} }
const transaction: Transaction = makeTransaction({ const transaction: Transaction = makeTransaction({
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx, name, nonce, value, confirmations, destination, threshold: safeThreshold, tx, data,
}) })
const safeTransactions = load(TX_KEY) || {} const safeTransactions = load(TX_KEY) || {}
@ -79,37 +80,44 @@ const hasOneOwner = (safe: Safe) => {
return owners.count() === 1 return owners.count() === 1
} }
export const getSafeEthereumInstance = async (safeAddress: string) => {
const web3 = getWeb3()
const GnosisSafe = await getGnosisSafeContract(web3)
return GnosisSafe.at(safeAddress)
}
export const createTransaction = async ( export const createTransaction = async (
safe: Safe, safe: Safe,
txName: string, txName: string,
txDestination: string, txDest: string,
txValue: number, txValue: number,
nonce: number, nonce: number,
user: string, user: string,
data: string = '0x',
) => { ) => {
const web3 = getWeb3() const web3 = getWeb3()
const GnosisSafe = await getGnosisSafeContract(web3)
const safeAddress = safe.get('address') const safeAddress = safe.get('address')
const gnosisSafe = GnosisSafe.at(safeAddress) const gnosisSafe = await getSafeEthereumInstance(safeAddress)
const valueInWei = web3.toWei(txValue, 'ether') const valueInWei = web3.toWei(txValue, 'ether')
const CALL = 0 const CALL = 0
const thresholdIsOne = safe.get('confirmations') === 1 const thresholdIsOne = safe.get('threshold') === 1
if (hasOneOwner(safe) || thresholdIsOne) { if (hasOneOwner(safe) || thresholdIsOne) {
const txConfirmationData = gnosisSafe.contract.execTransactionIfApproved.getData(txDestination, valueInWei, '0x', CALL, nonce) const txConfirmationData =
gnosisSafe.contract.execTransactionIfApproved.getData(txDest, valueInWei, data, CALL, nonce)
const txHash = await executeTransaction(txConfirmationData, user, safeAddress) const txHash = await executeTransaction(txConfirmationData, user, safeAddress)
checkReceiptStatus(txHash) checkReceiptStatus(txHash)
const executedConfirmations: List<Confirmation> = buildExecutedConfirmationFrom(safe.get('owners'), user) const executedConfirmations: List<Confirmation> = buildExecutedConfirmationFrom(safe.get('owners'), user)
return storeTransaction(txName, nonce, txDestination, txValue, user, executedConfirmations, txHash, safeAddress, safe.get('confirmations')) return storeTransaction(txName, nonce, txDest, txValue, user, executedConfirmations, txHash, safeAddress, safe.get('threshold'), data)
} }
const txConfirmationData = gnosisSafe.contract.approveTransactionWithParameters.getData(txDestination, valueInWei, '0x', CALL, nonce) const txConfirmationData =
gnosisSafe.contract.approveTransactionWithParameters.getData(txDest, valueInWei, data, CALL, nonce)
const txConfirmationHash = await executeTransaction(txConfirmationData, user, safeAddress) const txConfirmationHash = await executeTransaction(txConfirmationData, user, safeAddress)
checkReceiptStatus(txConfirmationHash) checkReceiptStatus(txConfirmationHash)
const confirmations: List<Confirmation> = buildConfirmationsFrom(safe.get('owners'), user, txConfirmationHash) const confirmations: List<Confirmation> = buildConfirmationsFrom(safe.get('owners'), user, txConfirmationHash)
return storeTransaction(txName, nonce, txDestination, txValue, user, confirmations, '', safeAddress, safe.get('confirmations')) return storeTransaction(txName, nonce, txDest, txValue, user, confirmations, '', safeAddress, safe.get('threshold'), data)
} }

View File

@ -33,7 +33,7 @@ describe('Transactions Suite', () => {
const txName = 'Buy butteries for project' const txName = 'Buy butteries for project'
const nonce: number = 10 const nonce: number = 10
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, 'foo', 'confirmationHash') const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, 'foo', 'confirmationHash')
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('confirmations')) storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
// WHEN // WHEN
const transactions: Map<string, List<Transaction>> = loadSafeTransactions() const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
@ -45,7 +45,7 @@ describe('Transactions Suite', () => {
if (!safeTransactions) { throw new Error() } if (!safeTransactions) { throw new Error() }
testSizeOfTransactions(safeTransactions, 1) testSizeOfTransactions(safeTransactions, 1)
testTransactionFrom(safeTransactions, 0, txName, nonce, value, 2, destination, 'foo', 'confirmationHash', owners.get(0), owners.get(1)) testTransactionFrom(safeTransactions, 0, txName, nonce, value, 2, destination, '0x', 'foo', 'confirmationHash', owners.get(0), owners.get(1))
}) })
it('adds second confirmation to stored safe with one confirmation', async () => { it('adds second confirmation to stored safe with one confirmation', async () => {
@ -55,12 +55,12 @@ describe('Transactions Suite', () => {
const safeAddress = safe.get('address') const safeAddress = safe.get('address')
const creator = 'foo' const creator = 'foo'
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash') const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(firstTxName, firstNonce, destination, value, creator, confirmations, '', safeAddress, safe.get('confirmations')) storeTransaction(firstTxName, firstNonce, destination, value, creator, confirmations, '', safeAddress, safe.get('threshold'), '0x')
const secondTxName = 'Buy printers for project' const secondTxName = 'Buy printers for project'
const secondNonce: number = firstNonce + 100 const secondNonce: number = firstNonce + 100
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash') const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(secondTxName, secondNonce, destination, value, creator, secondConfirmations, '', safeAddress, safe.get('confirmations')) storeTransaction(secondTxName, secondNonce, destination, value, creator, secondConfirmations, '', safeAddress, safe.get('threshold'), '0x')
// WHEN // WHEN
const transactions: Map<string, List<Transaction>> = loadSafeTransactions() const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
@ -72,8 +72,8 @@ describe('Transactions Suite', () => {
if (!safeTxs) { throw new Error() } if (!safeTxs) { throw new Error() }
testSizeOfTransactions(safeTxs, 2) testSizeOfTransactions(safeTxs, 2)
testTransactionFrom(safeTxs, 0, firstTxName, firstNonce, value, 2, destination, 'foo', 'confirmationHash', owners.get(0), owners.get(1)) testTransactionFrom(safeTxs, 0, firstTxName, firstNonce, value, 2, destination, '0x', 'foo', 'confirmationHash', owners.get(0), owners.get(1))
testTransactionFrom(safeTxs, 1, secondTxName, secondNonce, value, 2, destination, 'foo', 'confirmationHash', owners.get(0), owners.get(1)) testTransactionFrom(safeTxs, 1, secondTxName, secondNonce, value, 2, destination, '0x', 'foo', 'confirmationHash', owners.get(0), owners.get(1))
}) })
it('adds second confirmation to stored safe having two safes with one confirmation each', async () => { it('adds second confirmation to stored safe having two safes with one confirmation each', async () => {
@ -82,7 +82,7 @@ describe('Transactions Suite', () => {
const safeAddress = safe.address const safeAddress = safe.address
const creator = 'foo' const creator = 'foo'
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash') const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safeAddress, safe.get('confirmations')) storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safeAddress, safe.get('threshold'), '0x')
const secondSafe = SafeFactory.dailyLimitSafe(10, 2) const secondSafe = SafeFactory.dailyLimitSafe(10, 2)
const txSecondName = 'Buy batteris for Beta project' const txSecondName = 'Buy batteris for Beta project'
@ -92,7 +92,7 @@ describe('Transactions Suite', () => {
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(secondSafe.get('owners'), secondCreator, 'confirmationHash') const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(secondSafe.get('owners'), secondCreator, 'confirmationHash')
storeTransaction( storeTransaction(
txSecondName, txSecondNonce, destination, value, secondCreator, txSecondName, txSecondNonce, destination, value, secondCreator,
secondConfirmations, '', secondSafeAddress, secondSafe.get('confirmations'), secondConfirmations, '', secondSafeAddress, secondSafe.get('threshold'), '0x',
) )
let transactions: Map<string, List<Transaction>> = loadSafeTransactions() let transactions: Map<string, List<Transaction>> = loadSafeTransactions()
@ -112,7 +112,7 @@ describe('Transactions Suite', () => {
const txConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'secondConfirmationHash') const txConfirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'secondConfirmationHash')
storeTransaction( storeTransaction(
txFirstName, txFirstNonce, destination, value, creator, txFirstName, txFirstNonce, destination, value, creator,
txConfirmations, '', safe.get('address'), safe.get('confirmations'), txConfirmations, '', safe.get('address'), safe.get('threshold'), '0x',
) )
transactions = loadSafeTransactions() transactions = loadSafeTransactions()
@ -125,19 +125,19 @@ describe('Transactions Suite', () => {
// Test 2 transactions of first safe // Test 2 transactions of first safe
testTransactionFrom( testTransactionFrom(
transactions.get(safe.address), 0, transactions.get(safe.address), 0,
txName, nonce, value, 2, destination, txName, nonce, value, 2, destination, '0x',
'foo', 'confirmationHash', owners.get(0), owners.get(1), 'foo', 'confirmationHash', owners.get(0), owners.get(1),
) )
testTransactionFrom( testTransactionFrom(
transactions.get(safe.address), 1, transactions.get(safe.address), 1,
txFirstName, txFirstNonce, value, 2, destination, txFirstName, txFirstNonce, value, 2, destination, '0x',
'foo', 'secondConfirmationHash', owners.get(0), owners.get(1), 'foo', 'secondConfirmationHash', owners.get(0), owners.get(1),
) )
// Test one transaction of second safe // Test one transaction of second safe
testTransactionFrom( testTransactionFrom(
transactions.get(secondSafe.address), 0, transactions.get(secondSafe.address), 0,
txSecondName, txSecondNonce, value, 2, destination, txSecondName, txSecondNonce, value, 2, destination, '0x',
'0x03db1a8b26d08df23337e9276a36b474510f0023', 'confirmationHash', secondSafe.get('owners').get(0), secondSafe.get('owners').get(1), '0x03db1a8b26d08df23337e9276a36b474510f0023', 'confirmationHash', secondSafe.get('owners').get(0), secondSafe.get('owners').get(1),
) )
}) })
@ -148,10 +148,10 @@ describe('Transactions Suite', () => {
const nonce: number = 10 const nonce: number = 10
const creator = 'foo' const creator = 'foo'
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash') const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations')) storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
// WHEN // WHEN
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations')) const createTxFnc = () => storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
expect(createTxFnc).toThrow(/Transaction with same nonce/) expect(createTxFnc).toThrow(/Transaction with same nonce/)
}) })
@ -161,7 +161,7 @@ describe('Transactions Suite', () => {
const nonce: number = 10 const nonce: number = 10
const creator = 'foo' const creator = 'foo'
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash') const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, creator, 'confirmationHash')
storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations')) storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('threshold'), '0x')
// WHEN // WHEN
const transactions: Map<string, List<Transaction>> = loadSafeTransactions() const transactions: Map<string, List<Transaction>> = loadSafeTransactions()
@ -185,7 +185,7 @@ describe('Transactions Suite', () => {
const nonce: number = 10 const nonce: number = 10
const tx = '' const tx = ''
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName) const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('confirmations')) const createTxFnc = () => storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('threshold'), '0x')
expect(createTxFnc).toThrow(/The tx should be mined before storing it in safes with one owner/) expect(createTxFnc).toThrow(/The tx should be mined before storing it in safes with one owner/)
}) })
@ -197,7 +197,7 @@ describe('Transactions Suite', () => {
const nonce: number = 10 const nonce: number = 10
const tx = 'validTxHash' const tx = 'validTxHash'
const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName) const confirmations: List<Confirmation> = buildExecutedConfirmationFrom(oneOwnerSafe.get('owners'), ownerName)
storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('confirmations')) storeTransaction(txName, nonce, destination, value, ownerName, confirmations, tx, oneOwnerSafe.get('address'), oneOwnerSafe.get('threshold'), '0x')
// WHEN // WHEN
const safeTransactions: Map<string, List<Transaction>> = loadSafeTransactions() const safeTransactions: Map<string, List<Transaction>> = loadSafeTransactions()

View File

@ -20,7 +20,7 @@ export const testSizeOfTransactions = (safeTxs: List<Transaction> | typeof undef
export const testTransactionFrom = ( export const testTransactionFrom = (
safeTxs: List<Transaction> | typeof undefined, pos: number, name: string, safeTxs: List<Transaction> | typeof undefined, pos: number, name: string,
nonce: number, value: number, threshold: number, destination: string, nonce: number, value: number, threshold: number, destination: string,
creator: string, txHash: string, data: string, creator: string, txHash: string,
firstOwner: Owner | typeof undefined, secondOwner: Owner | typeof undefined, firstOwner: Owner | typeof undefined, secondOwner: Owner | typeof undefined,
) => { ) => {
if (!safeTxs) { throw new Error() } if (!safeTxs) { throw new Error() }
@ -33,6 +33,7 @@ export const testTransactionFrom = (
expect(tx.get('destination')).toBe(destination) expect(tx.get('destination')).toBe(destination)
expect(tx.get('confirmations').count()).toBe(2) expect(tx.get('confirmations').count()).toBe(2)
expect(tx.get('nonce')).toBe(nonce) expect(tx.get('nonce')).toBe(nonce)
expect(tx.get('data')).toBe(data)
const confirmations: List<Confirmation> = tx.get('confirmations') const confirmations: List<Confirmation> = tx.get('confirmations')
const firstConfirmation: Confirmation | typeof undefined = confirmations.get(0) const firstConfirmation: Confirmation | typeof undefined = confirmations.get(0)

View File

@ -4,12 +4,16 @@ import { ListItem } from 'material-ui/List'
import Avatar from 'material-ui/Avatar' import Avatar from 'material-ui/Avatar'
import DoneAll from 'material-ui-icons/DoneAll' import DoneAll from 'material-ui-icons/DoneAll'
import ListItemText from '~/components/List/ListItemText' import ListItemText from '~/components/List/ListItemText'
import Button from '~/components/layout/Button'
type Props = { type Props = {
confirmations: number, confirmations: number,
onEditThreshold: () => void,
} }
const Confirmations = ({ confirmations }: Props) => ( const EDIT_THRESHOLD_BUTTON_TEXT = 'EDIT'
const Confirmations = ({ confirmations, onEditThreshold }: Props) => (
<ListItem> <ListItem>
<Avatar> <Avatar>
<DoneAll /> <DoneAll />
@ -19,6 +23,13 @@ const Confirmations = ({ confirmations }: Props) => (
secondary={`${confirmations} required confirmations per transaction`} secondary={`${confirmations} required confirmations per transaction`}
cut cut
/> />
<Button
variant="raised"
color="primary"
onClick={onEditThreshold}
>
{EDIT_THRESHOLD_BUTTON_TEXT}
</Button>
</ListItem> </ListItem>
) )

View File

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

View File

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

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 // @flow
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
type FetchTransactions = typeof fetchTransactions
export type Actions = { export type Actions = {
fetchTransactions: typeof fetchTransactions, fetchTransactions: FetchTransactions,
} }
export default { export default {

View File

@ -4,7 +4,6 @@ import { connect } from 'react-redux'
import { type Transaction } from '~/routes/safe/store/model/transaction' import { type Transaction } from '~/routes/safe/store/model/transaction'
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions' import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction' import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
import { sleep } from '~/utils/timer'
import { processTransaction } from './processTransactions' import { processTransaction } from './processTransactions'
import selector, { type SelectorProps } from './selector' import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions' import actions, { type Actions } from './actions'
@ -17,9 +16,11 @@ type Props = SelectorProps & Actions & {
} }
class Transactions extends React.Component<Props, {}> { class Transactions extends React.Component<Props, {}> {
onProcessTx = async (tx: Transaction, alreadyConfirmed: number) => { onProcessTx = async (tx: Transaction, alreadyConfirmed: number) => {
const { fetchTransactions, safeAddress, userAddress } = this.props const {
fetchTransactions, safeAddress, userAddress,
} = this.props
await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress) await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress)
await sleep(1200)
fetchTransactions() fetchTransactions()
} }

View File

@ -20,9 +20,10 @@ export const updateTransaction = (
tx: string, tx: string,
safeAddress: string, safeAddress: string,
safeThreshold: number, safeThreshold: number,
data: string,
) => { ) => {
const transaction: Transaction = makeTransaction({ const transaction: Transaction = makeTransaction({
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx, name, nonce, value, confirmations, destination, threshold: safeThreshold, tx, data,
}) })
const safeTransactions = load(TX_KEY) || {} const safeTransactions = load(TX_KEY) || {}
@ -36,7 +37,6 @@ export const updateTransaction = (
localStorage.setItem(TX_KEY, JSON.stringify(safeTransactions)) localStorage.setItem(TX_KEY, JSON.stringify(safeTransactions))
} }
const getData = () => '0x'
const getOperation = () => 0 const getOperation = () => 0
const execTransaction = async ( const execTransaction = async (
@ -45,8 +45,8 @@ const execTransaction = async (
txValue: number, txValue: number,
nonce: number, nonce: number,
executor: string, executor: string,
data: string,
) => { ) => {
const data = getData()
const CALL = getOperation() const CALL = getOperation()
const web3 = getWeb3() const web3 = getWeb3()
const valueInWei = web3.toWei(txValue, 'ether') const valueInWei = web3.toWei(txValue, 'ether')
@ -61,8 +61,8 @@ const execConfirmation = async (
txValue: number, txValue: number,
nonce: number, nonce: number,
executor: string, executor: string,
data: string,
) => { ) => {
const data = getData()
const CALL = getOperation() const CALL = getOperation()
const web3 = getWeb3() const web3 = getWeb3()
const valueInWei = web3.toWei(txValue, 'ether') const valueInWei = web3.toWei(txValue, 'ether')
@ -110,10 +110,11 @@ export const processTransaction = async (
const txName = tx.get('name') const txName = tx.get('name')
const txValue = tx.get('value') const txValue = tx.get('value')
const txDestination = tx.get('destination') const txDestination = tx.get('destination')
const data = tx.get('data')
const txHash = thresholdReached const txHash = thresholdReached
? await execTransaction(gnosisSafe, txDestination, txValue, nonce, userAddress) ? await execTransaction(gnosisSafe, txDestination, txValue, nonce, userAddress, data)
: await execConfirmation(gnosisSafe, txDestination, txValue, nonce, userAddress) : await execConfirmation(gnosisSafe, txDestination, txValue, nonce, userAddress, data)
checkReceiptStatus(txHash) checkReceiptStatus(txHash)
@ -130,5 +131,6 @@ export const processTransaction = async (
thresholdReached ? txHash : '', thresholdReached ? txHash : '',
safeAddress, safeAddress,
threshold, threshold,
data,
) )
} }

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

View File

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

View File

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

View File

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

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 = { export type SafeProps = {
name: string, name: string,
address: string, address: string,
confirmations: number, threshold: number,
owners: List<Owner>, owners: List<Owner>,
dailyLimit: DailyLimit, dailyLimit: DailyLimit,
} }
@ -15,7 +15,7 @@ export type SafeProps = {
export const makeSafe: RecordFactory<SafeProps> = Record({ export const makeSafe: RecordFactory<SafeProps> = Record({
name: '', name: '',
address: '', address: '',
confirmations: 0, threshold: 0,
owners: List([]), owners: List([]),
dailyLimit: makeDailyLimit(), dailyLimit: makeDailyLimit(),
}) })

View File

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

View File

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

View File

@ -20,7 +20,7 @@ class SafeBuilder {
} }
withConfirmations(confirmations: number) { withConfirmations(confirmations: number) {
this.safe = this.safe.set('confirmations', confirmations) this.safe = this.safe.set('threshold', confirmations)
return this return this
} }

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 // @flow
import { combineReducers, createStore, applyMiddleware, compose } from 'redux' import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import safeReducer, { safeInitialState, SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe' import safeReducer, { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
import addSafe from '~/routes/safe/store/actions/addSafe' import addSafe from '~/routes/safe/store/actions/addSafe'
import * as SafeFields from '~/routes/open/components/fields' import * as SafeFields from '~/routes/open/components/fields'
import { getAccountsFrom, getNamesFrom } from '~/routes/open/utils/safeDataExtractor' import { getAccountsFrom, getNamesFrom } from '~/routes/open/utils/safeDataExtractor'
@ -56,26 +56,6 @@ const providerReducerTests = () => {
// THEN // THEN
expect(safes.get(address)).toEqual(SafeFactory.oneOwnerSafe()) expect(safes.get(address)).toEqual(SafeFactory.oneOwnerSafe())
}) })
it('reducer loads information from localStorage', async () => {
// GIVEN in beforeEach method
// WHEN
store.dispatch(addSafe(
formValues[SafeFields.FIELD_NAME],
formValues.address,
formValues[SafeFields.FIELD_CONFIRMATIONS],
formValues[SafeFields.FIELD_DAILY_LIMIT],
getNamesFrom(formValues),
getAccountsFrom(formValues),
))
const anotherStore = aStore({ [SAFE_REDUCER_ID]: safeInitialState() })
const safes = anotherStore.getState()[SAFE_REDUCER_ID]
// THEN
expect(safeInitialState()).toEqual(safes)
})
}) })
} }

View File

@ -1,7 +1,6 @@
// @flow // @flow
import balanceReducerTests from './balance.reducer' import balanceReducerTests from './balance.reducer'
import safeReducerTests from './safe.reducer' import safeReducerTests from './safe.reducer'
import dailyLimitReducerTests from './dailyLimit.reducer'
import balanceSelectorTests from './balance.selector' import balanceSelectorTests from './balance.selector'
import safeSelectorTests from './safe.selector' import safeSelectorTests from './safe.selector'
import grantedSelectorTests from './granted.selector' import grantedSelectorTests from './granted.selector'
@ -12,7 +11,6 @@ describe('Safe Test suite', () => {
// ACTIONS AND REDUCERS // ACTIONS AND REDUCERS
safeReducerTests() safeReducerTests()
balanceReducerTests() balanceReducerTests()
dailyLimitReducerTests()
// SAFE SELECTOR // SAFE SELECTOR
safeSelectorTests() safeSelectorTests()

View File

@ -45,7 +45,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
// $FlowFixMe // $FlowFixMe
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button) const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
const addTxButton = buttons[1] const addTxButton = buttons[3]
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT) expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
await sleep(1800) // Give time to enable Add button await sleep(1800) // Give time to enable Add button
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(addTxButton, 'button')[0]) TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(addTxButton, 'button')[0])

View File

@ -53,6 +53,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 1 thresh
const confirmed = paragraphs[3].innerHTML const confirmed = paragraphs[3].innerHTML
const tx = getTransactionFromReduxStore(store, address) const tx = getTransactionFromReduxStore(store, address)
if (!tx) throw new Error()
expect(confirmed).toBe(tx.get('tx')) expect(confirmed).toBe(tx.get('tx'))
const ownerTx = paragraphs[6].innerHTML const ownerTx = paragraphs[6].innerHTML

View File

@ -45,6 +45,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 3 thresh
const getAlreadyConfirmed = () => { const getAlreadyConfirmed = () => {
const tx = getTransactionFromReduxStore(store, address) const tx = getTransactionFromReduxStore(store, address)
if (!tx) throw new Error()
const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx }) const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx })
return confirmed return confirmed
@ -53,6 +54,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 3 thresh
const makeConfirmation = async (executor) => { const makeConfirmation = async (executor) => {
const alreadyConfirmed = getAlreadyConfirmed() const alreadyConfirmed = getAlreadyConfirmed()
const tx = getTransactionFromReduxStore(store, address) const tx = getTransactionFromReduxStore(store, address)
if (!tx) throw new Error()
await processTransaction(address, tx, alreadyConfirmed, executor) await processTransaction(address, tx, alreadyConfirmed, executor)
await sleep(800) await sleep(800)
store.dispatch(fetchTransactions()) store.dispatch(fetchTransactions())
@ -96,6 +98,7 @@ describe('React DOM TESTS > Multisig transactions from safe [3 owners & 3 thresh
const confirmedExecuted = paragraphsExecuted[3].innerHTML const confirmedExecuted = paragraphsExecuted[3].innerHTML
const tx = getTransactionFromReduxStore(store, address) const tx = getTransactionFromReduxStore(store, address)
if (!tx) throw new Error()
expect(confirmedExecuted).toBe(tx.get('tx')) expect(confirmedExecuted).toBe(tx.get('tx'))
}) })
}) })

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 // $FlowFixMe
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button) const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
const withdrawnButton = buttons[0] const withdrawnButton = buttons[2]
expect(withdrawnButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT) expect(withdrawnButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(withdrawnButton, 'button')[0]) TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(withdrawnButton, 'button')[0])
await sleep(4000) await sleep(4000)
@ -96,7 +96,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
// $FlowFixMe // $FlowFixMe
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button) const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
const addTxButton = buttons[1] const addTxButton = buttons[3]
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT) expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
expect(addTxButton.props.disabled).toBe(true) expect(addTxButton.props.disabled).toBe(true)
@ -110,7 +110,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
// $FlowFixMe // $FlowFixMe
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button) const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
const addTxButton = buttons[0] const addTxButton = buttons[2]
expect(addTxButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT) expect(addTxButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
expect(addTxButton.props.disabled).toBe(true) expect(addTxButton.props.disabled).toBe(true)

View File

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

View File

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

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

View File

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

View File

@ -5,7 +5,7 @@ import type { ProviderProps } from '~/wallets/store/model/provider'
import { promisify } from '~/utils/promisify' import { promisify } from '~/utils/promisify'
let web3 let web3
export const getWeb3 = () => web3 export const getWeb3 = () => web3 || new Web3(window.web3.currentProvider)
const isMetamask: Function = (web3Provider): boolean => { const isMetamask: Function = (web3Provider): boolean => {
const isMetamaskConstructor = web3Provider.currentProvider.constructor.name === 'MetamaskInpageProvider' const isMetamaskConstructor = web3Provider.currentProvider.constructor.name === 'MetamaskInpageProvider'

View File

@ -136,3 +136,11 @@ export const deploySafeContract = async (
return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, { from: userAccount, gas, gasPrice }) return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, { from: userAccount, gas, gasPrice })
} }
export const getGnosisSafeInstanceAt = async (safeAddress: string) => {
const web3 = getWeb3()
const GnosisSafe = await getGnosisSafeContract(web3)
const gnosisSafe = GnosisSafe.at(safeAddress)
return gnosisSafe
}