Merge pull request #36 from gnosis/feature/WA-235-modify-confirmation-threshold
WA-235 Modify confirmation threshold
This commit is contained in:
commit
e18ec4660e
|
@ -1,2 +1,2 @@
|
|||
// @flow
|
||||
jest.setTimeout(30000)
|
||||
jest.setTimeout(45000)
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -47,6 +47,7 @@ export const storeTransaction = (
|
|||
tx: string,
|
||||
safeAddress: string,
|
||||
safeThreshold: number,
|
||||
data: string,
|
||||
) => {
|
||||
const notMinedWhenOneOwnerSafe = confirmations.count() === 1 && !tx
|
||||
if (notMinedWhenOneOwnerSafe) {
|
||||
|
@ -54,7 +55,7 @@ export const storeTransaction = (
|
|||
}
|
||||
|
||||
const transaction: Transaction = makeTransaction({
|
||||
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx,
|
||||
name, nonce, value, confirmations, destination, threshold: safeThreshold, tx, data,
|
||||
})
|
||||
|
||||
const safeTransactions = load(TX_KEY) || {}
|
||||
|
@ -79,37 +80,44 @@ const hasOneOwner = (safe: Safe) => {
|
|||
return owners.count() === 1
|
||||
}
|
||||
|
||||
export const getSafeEthereumInstance = async (safeAddress) => {
|
||||
const web3 = getWeb3()
|
||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||
return GnosisSafe.at(safeAddress)
|
||||
}
|
||||
|
||||
export const createTransaction = async (
|
||||
safe: Safe,
|
||||
txName: string,
|
||||
txDestination: string,
|
||||
txDest: string,
|
||||
txValue: number,
|
||||
nonce: number,
|
||||
user: string,
|
||||
data: string = '0x',
|
||||
) => {
|
||||
const web3 = getWeb3()
|
||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||
const safeAddress = safe.get('address')
|
||||
const gnosisSafe = GnosisSafe.at(safeAddress)
|
||||
|
||||
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||
const valueInWei = web3.toWei(txValue, 'ether')
|
||||
const CALL = 0
|
||||
|
||||
const thresholdIsOne = safe.get('confirmations') === 1
|
||||
if (hasOneOwner(safe) || thresholdIsOne) {
|
||||
const txConfirmationData = gnosisSafe.contract.execTransactionIfApproved.getData(txDestination, valueInWei, '0x', CALL, nonce)
|
||||
const txConfirmationData =
|
||||
gnosisSafe.contract.execTransactionIfApproved.getData(txDest, valueInWei, data, CALL, nonce)
|
||||
const txHash = await executeTransaction(txConfirmationData, user, safeAddress)
|
||||
checkReceiptStatus(txHash)
|
||||
|
||||
const executedConfirmations: List<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('confirmations'), data)
|
||||
}
|
||||
|
||||
const txConfirmationData = gnosisSafe.contract.approveTransactionWithParameters.getData(txDestination, valueInWei, '0x', CALL, nonce)
|
||||
const txConfirmationData =
|
||||
gnosisSafe.contract.approveTransactionWithParameters.getData(txDest, valueInWei, data, CALL, nonce)
|
||||
const txConfirmationHash = await executeTransaction(txConfirmationData, user, safeAddress)
|
||||
checkReceiptStatus(txConfirmationHash)
|
||||
|
||||
const confirmations: List<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('confirmations'), data)
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('Transactions Suite', () => {
|
|||
const txName = 'Buy butteries for project'
|
||||
const nonce: number = 10
|
||||
const confirmations: List<Confirmation> = buildConfirmationsFrom(owners, 'foo', 'confirmationHash')
|
||||
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('confirmations'))
|
||||
storeTransaction(txName, nonce, destination, value, 'foo', confirmations, '', safe.get('address'), safe.get('confirmations'), '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('confirmations'), '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('confirmations'), '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('confirmations'), '0x')
|
||||
|
||||
const secondSafe = SafeFactory.dailyLimitSafe(10, 2)
|
||||
const txSecondName = 'Buy batteris for Beta project'
|
||||
|
@ -92,7 +92,7 @@ describe('Transactions Suite', () => {
|
|||
const secondConfirmations: List<Confirmation> = buildConfirmationsFrom(secondSafe.get('owners'), secondCreator, 'confirmationHash')
|
||||
storeTransaction(
|
||||
txSecondName, txSecondNonce, destination, value, secondCreator,
|
||||
secondConfirmations, '', secondSafeAddress, secondSafe.get('confirmations'),
|
||||
secondConfirmations, '', secondSafeAddress, secondSafe.get('confirmations'), '0x',
|
||||
)
|
||||
|
||||
let transactions: Map<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('confirmations'), '0x',
|
||||
)
|
||||
|
||||
transactions = loadSafeTransactions()
|
||||
|
@ -125,19 +125,19 @@ describe('Transactions Suite', () => {
|
|||
// Test 2 transactions of first safe
|
||||
testTransactionFrom(
|
||||
transactions.get(safe.address), 0,
|
||||
txName, nonce, value, 2, destination,
|
||||
txName, nonce, value, 2, destination, '0x',
|
||||
'foo', 'confirmationHash', owners.get(0), owners.get(1),
|
||||
)
|
||||
testTransactionFrom(
|
||||
transactions.get(safe.address), 1,
|
||||
txFirstName, txFirstNonce, value, 2, destination,
|
||||
txFirstName, txFirstNonce, value, 2, destination, '0x',
|
||||
'foo', 'secondConfirmationHash', owners.get(0), owners.get(1),
|
||||
)
|
||||
|
||||
// Test one transaction of second safe
|
||||
testTransactionFrom(
|
||||
transactions.get(secondSafe.address), 0,
|
||||
txSecondName, txSecondNonce, value, 2, destination,
|
||||
txSecondName, txSecondNonce, value, 2, destination, '0x',
|
||||
'0x03db1a8b26d08df23337e9276a36b474510f0023', 'confirmationHash', secondSafe.get('owners').get(0), secondSafe.get('owners').get(1),
|
||||
)
|
||||
})
|
||||
|
@ -148,10 +148,10 @@ describe('Transactions Suite', () => {
|
|||
const nonce: number = 10
|
||||
const creator = 'foo'
|
||||
const confirmations: List<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('confirmations'), '0x')
|
||||
|
||||
// WHEN
|
||||
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'))
|
||||
const createTxFnc = () => storeTransaction(txName, nonce, destination, value, creator, confirmations, '', safe.get('address'), safe.get('confirmations'), '0x')
|
||||
expect(createTxFnc).toThrow(/Transaction with same nonce/)
|
||||
})
|
||||
|
||||
|
@ -161,7 +161,7 @@ describe('Transactions Suite', () => {
|
|||
const nonce: number = 10
|
||||
const creator = 'foo'
|
||||
const confirmations: List<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('confirmations'), '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('confirmations'), '0x')
|
||||
|
||||
expect(createTxFnc).toThrow(/The tx should be mined before storing it in safes with one owner/)
|
||||
})
|
||||
|
@ -197,7 +197,7 @@ describe('Transactions Suite', () => {
|
|||
const nonce: number = 10
|
||||
const tx = 'validTxHash'
|
||||
const confirmations: List<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('confirmations'), '0x')
|
||||
|
||||
// WHEN
|
||||
const safeTransactions: Map<string, List<Transaction>> = loadSafeTransactions()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import List from 'material-ui/List'
|
|||
import Withdrawn from '~/routes/safe/component/Withdrawn'
|
||||
import Transactions from '~/routes/safe/component/Transactions'
|
||||
import AddTransaction from '~/routes/safe/component/AddTransaction'
|
||||
import Threshold from '~/routes/safe/component/Threshold'
|
||||
|
||||
import Address from './Address'
|
||||
import Balance from './Balance'
|
||||
|
@ -59,6 +60,12 @@ class GnoSafe extends React.PureComponent<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} onReset={this.onListTransactions} /> })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { safe, balance } = this.props
|
||||
const { component } = this.state
|
||||
|
@ -69,7 +76,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
|
|||
<List style={listStyle}>
|
||||
<Balance balance={balance} />
|
||||
<Owners owners={safe.owners} />
|
||||
<Confirmations confirmations={safe.get('confirmations')} />
|
||||
<Confirmations confirmations={safe.get('confirmations')} onEditThreshold={this.onEditThreshold} />
|
||||
<Address address={safe.get('address')} />
|
||||
<DailyLimit balance={balance} dailyLimit={safe.get('dailyLimit')} onWithdrawn={this.onWithdrawn} />
|
||||
<MultisigTx balance={balance} onAddTx={this.onAddTx} onSeeTxs={this.onListTransactions} />
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// @flow
|
||||
import fetchThreshold from '~/routes/safe/store/actions/fetchThreshold'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
||||
type FetchThreshold = typeof fetchThreshold
|
||||
type FetchTransactions = typeof fetchTransactions
|
||||
|
||||
export type Actions = {
|
||||
fetchThreshold: FetchThreshold,
|
||||
fetchTransactions: FetchTransactions,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchThreshold,
|
||||
fetchTransactions,
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Heading from '~/components/layout/Heading'
|
||||
import Field from '~/components/forms/Field'
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import GnoForm from '~/components/forms/GnoForm'
|
||||
import { connect } from 'react-redux'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { composeValidators, minValue, maxValue, mustBeInteger, required } from '~/components/forms/validator'
|
||||
import { getSafeEthereumInstance, createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||
import { sleep } from '~/utils/timer'
|
||||
import selector, { type SelectorProps } from './selector'
|
||||
import actions, { type Actions } from './actions'
|
||||
|
||||
type Props = SelectorProps & Actions & {
|
||||
numOwners: number,
|
||||
safe: Safe,
|
||||
onReset: () => void,
|
||||
}
|
||||
|
||||
const THRESHOLD_PARAM = 'threshold'
|
||||
|
||||
const ThresholdComponent = ({ numOwners, safe }: Props) => () => (
|
||||
<Block margin="md">
|
||||
<Heading tag="h2" margin="lg">
|
||||
{'Change safe\'s threshold'}
|
||||
</Heading>
|
||||
<Heading tag="h4" margin="lg">
|
||||
{`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('confirmations')}`}
|
||||
</Heading>
|
||||
<Block margin="md">
|
||||
<Field
|
||||
name={THRESHOLD_PARAM}
|
||||
component={TextField}
|
||||
type="text"
|
||||
validate={composeValidators(
|
||||
required,
|
||||
mustBeInteger,
|
||||
minValue(1),
|
||||
maxValue(numOwners),
|
||||
)}
|
||||
placeholder="New threshold"
|
||||
text="Safe's threshold"
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
)
|
||||
|
||||
type State = {
|
||||
initialValues: Object,
|
||||
}
|
||||
|
||||
class Threshold extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
initialValues: {},
|
||||
}
|
||||
|
||||
onThreshold = async (values: Object) => {
|
||||
const { safe, userAddress } = this.props // , fetchThreshold } = this.props
|
||||
const newThreshold = values[THRESHOLD_PARAM]
|
||||
const gnosisSafe = await getSafeEthereumInstance(safe.get('address'))
|
||||
const nonce = Date.now()
|
||||
const data = gnosisSafe.contract.changeThreshold.getData(newThreshold)
|
||||
await createTransaction(safe, `Change Safe's threshold [${nonce}]`, safe.get('address'), 0, nonce, userAddress, data)
|
||||
await sleep(1500)
|
||||
this.props.fetchTransactions()
|
||||
this.props.fetchThreshold(safe.get('address'))
|
||||
}
|
||||
|
||||
render() {
|
||||
const { numOwners, onReset, safe } = this.props
|
||||
|
||||
return (
|
||||
<GnoForm
|
||||
onSubmit={this.onThreshold}
|
||||
render={ThresholdComponent({ numOwners, safe })}
|
||||
padding={15}
|
||||
initialValues={this.state.initialValues}
|
||||
>
|
||||
{(submitting: boolean, submitSucceeded: boolean) => (
|
||||
<Row align="end" margin="lg" grow>
|
||||
<Col xs={12} center="xs">
|
||||
<Button
|
||||
variant="raised"
|
||||
color="primary"
|
||||
onClick={submitSucceeded ? onReset : undefined}
|
||||
type={submitSucceeded ? 'button' : 'submit'}
|
||||
disabled={submitting}
|
||||
>
|
||||
{ submitSucceeded ? 'VISIT TXs' : 'FINISH' }
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</GnoForm>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(selector, actions)(Threshold)
|
|
@ -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,
|
||||
})
|
|
@ -1,10 +1,16 @@
|
|||
// @flow
|
||||
import fetchThreshold from '~/routes/safe/store/actions/fetchThreshold'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
||||
type FetchThreshold = typeof fetchThreshold
|
||||
type FetchTransactions = typeof fetchTransactions
|
||||
|
||||
export type Actions = {
|
||||
fetchTransactions: typeof fetchTransactions,
|
||||
fetchThreshold: FetchThreshold,
|
||||
fetchTransactions: FetchTransactions,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchThreshold,
|
||||
fetchTransactions,
|
||||
}
|
||||
|
|
|
@ -17,10 +17,14 @@ 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, fetchThreshold,
|
||||
} = this.props
|
||||
|
||||
await processTransaction(safeAddress, tx, alreadyConfirmed, userAddress)
|
||||
await sleep(1200)
|
||||
fetchTransactions()
|
||||
fetchThreshold(safeAddress)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
// @flow
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { getSafeEthereumInstance } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||
import updateThreshold from './updateThreshold'
|
||||
|
||||
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||
const actualThreshold = await gnosisSafe.getThreshold()
|
||||
|
||||
return dispatch(updateThreshold(safeAddress, actualThreshold))
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const UPDATE_THRESHOLD = 'UPDATE_THRESHOLD'
|
||||
|
||||
type ThresholdProps = {
|
||||
safeAddress: string,
|
||||
threshold: number,
|
||||
}
|
||||
|
||||
const updateDailyLimit = createAction(
|
||||
UPDATE_THRESHOLD,
|
||||
(safeAddress: string, threshold: number): ThresholdProps => ({
|
||||
safeAddress,
|
||||
threshold: Number(threshold),
|
||||
}),
|
||||
)
|
||||
|
||||
export default updateDailyLimit
|
|
@ -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>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { makeOwner } from '~/routes/safe/store/model/owner'
|
|||
import { type Safe, makeSafe } from '~/routes/safe/store/model/safe'
|
||||
import { load, saveSafes, SAFES_KEY } from '~/utils/localStorage'
|
||||
import { makeDailyLimit } from '~/routes/safe/store/model/dailyLimit'
|
||||
import updateThreshold, { UPDATE_THRESHOLD } from '~/routes/safe/store/actions/updateThreshold'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
|
||||
|
@ -50,4 +51,6 @@ export default handleActions({
|
|||
},
|
||||
[UPDATE_DAILY_LIMIT]: (state: State, action: ActionType<typeof updateDailyLimit>): State =>
|
||||
state.updateIn([action.payload.safeAddress, 'dailyLimit'], () => makeDailyLimit(action.payload.dailyLimit)),
|
||||
[UPDATE_THRESHOLD]: (state: State, action: ActionType<typeof updateThreshold>): State =>
|
||||
state.updateIn([action.payload.safeAddress, 'confirmations'], () => action.payload.threshold),
|
||||
}, Map())
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import balanceReducerTests from './balance.reducer'
|
||||
import safeReducerTests from './safe.reducer'
|
||||
import dailyLimitReducerTests from './dailyLimit.reducer'
|
||||
import thresholdReducerTests from './threshold.reducer'
|
||||
import balanceSelectorTests from './balance.selector'
|
||||
import safeSelectorTests from './safe.selector'
|
||||
import grantedSelectorTests from './granted.selector'
|
||||
|
@ -13,6 +14,7 @@ describe('Safe Test suite', () => {
|
|||
safeReducerTests()
|
||||
balanceReducerTests()
|
||||
dailyLimitReducerTests()
|
||||
thresholdReducerTests()
|
||||
|
||||
// SAFE SELECTOR
|
||||
safeSelectorTests()
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
// @flow
|
||||
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
||||
import { aNewStore } from '~/store'
|
||||
import updateThreshold from '~/routes/safe/store/actions/updateThreshold'
|
||||
import { aDeployedSafe } from './builder/deployedSafe.builder'
|
||||
|
||||
const thresholdReducerTests = () => {
|
||||
describe('Safe Actions[updateThreshold]', () => {
|
||||
let store
|
||||
beforeEach(async () => {
|
||||
store = aNewStore()
|
||||
})
|
||||
|
||||
it('reducer should return 3 when a safe of 3 threshold has just been created', async () => {
|
||||
// GIVEN
|
||||
const safeThreshold = 3
|
||||
const numOwners = 3
|
||||
|
||||
// WHEN
|
||||
const safeAddress = await aDeployedSafe(store, 0.5, safeThreshold, numOwners)
|
||||
|
||||
// THEN
|
||||
const safes = store.getState()[SAFE_REDUCER_ID]
|
||||
const threshold = safes.get(safeAddress).get('confirmations')
|
||||
expect(threshold).not.toBe(undefined)
|
||||
expect(threshold).toBe(safeThreshold)
|
||||
})
|
||||
|
||||
it('reducer should change correctly', async () => {
|
||||
// GIVEN
|
||||
const safeThreshold = 3
|
||||
const numOwners = 3
|
||||
const safeAddress = await aDeployedSafe(store, 0.5, safeThreshold, numOwners)
|
||||
|
||||
// WHEN
|
||||
const newThreshold = 1
|
||||
await store.dispatch(updateThreshold(safeAddress, newThreshold))
|
||||
|
||||
// THEN
|
||||
const safes = store.getState()[SAFE_REDUCER_ID]
|
||||
const threshold = safes.get(safeAddress).get('confirmations')
|
||||
expect(threshold).not.toBe(undefined)
|
||||
expect(threshold).toBe(newThreshold)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default thresholdReducerTests
|
|
@ -45,7 +45,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
|||
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const addTxButton = buttons[1]
|
||||
const addTxButton = buttons[2]
|
||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||
await sleep(1800) // Give time to enable Add button
|
||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(addTxButton, 'button')[0])
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
// @flow
|
||||
import { aNewStore } from '~/store'
|
||||
import { aDeployedSafe } from '~/routes/safe/store/test/builder/deployedSafe.builder'
|
||||
import { getWeb3 } from '~/wallets/getWeb3'
|
||||
import { sleep } from '~/utils/timer'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import { processTransaction } from '~/routes/safe/component/Transactions/processTransactions'
|
||||
import { confirmationsTransactionSelector, safeSelector, safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||
import { getTransactionFromReduxStore } from '~/routes/safe/test/testMultisig'
|
||||
import { buildMathPropsFrom } from '~/test/buildReactRouterProps'
|
||||
import { createTransaction } from '~/routes/safe/component/AddTransaction/createTransactions'
|
||||
import { getGnosisSafeContract } from '~/wallets/safeContracts'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
||||
describe('React DOM TESTS > Change threshold', () => {
|
||||
it('should update the threshold directly if safe has 1 threshold', async () => {
|
||||
// GIVEN
|
||||
const numOwners = 2
|
||||
const threshold = 1
|
||||
const store = aNewStore()
|
||||
const address = await aDeployedSafe(store, 10, threshold, numOwners)
|
||||
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||
const match: Match = buildMathPropsFrom(address)
|
||||
const safe = safeSelector(store.getState(), { match })
|
||||
const web3 = getWeb3()
|
||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||
const gnosisSafe = GnosisSafe.at(address)
|
||||
|
||||
// WHEN
|
||||
const nonce = Date.now()
|
||||
const data = gnosisSafe.contract.changeThreshold.getData(2)
|
||||
await createTransaction(safe, "Change Safe's threshold", address, 0, nonce, accounts[0], data)
|
||||
await sleep(1500)
|
||||
await store.dispatch(fetchTransactions())
|
||||
|
||||
// THEN
|
||||
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||
expect(transactions.count()).toBe(1)
|
||||
|
||||
const thresholdTx: Transaction = transactions.get(0)
|
||||
expect(thresholdTx.get('tx')).not.toBe(null)
|
||||
expect(thresholdTx.get('tx')).not.toBe(undefined)
|
||||
expect(thresholdTx.get('tx')).not.toBe('')
|
||||
|
||||
const safeThreshold = await gnosisSafe.getThreshold()
|
||||
expect(Number(safeThreshold)).toEqual(2)
|
||||
})
|
||||
|
||||
const changeThreshold = async (store, safeAddress, executor) => {
|
||||
const tx = getTransactionFromReduxStore(store, safeAddress)
|
||||
const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx })
|
||||
const data = tx.get('data')
|
||||
expect(data).not.toBe(null)
|
||||
expect(data).not.toBe(undefined)
|
||||
expect(data).not.toBe('')
|
||||
await processTransaction(safeAddress, tx, confirmed, executor)
|
||||
await sleep(1800)
|
||||
}
|
||||
|
||||
it('should wait for confirmation to update threshold when safe has 1+ threshold', async () => {
|
||||
// GIVEN
|
||||
const numOwners = 3
|
||||
const threshold = 2
|
||||
const store = aNewStore()
|
||||
const address = await aDeployedSafe(store, 10, threshold, numOwners)
|
||||
const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb))
|
||||
const match: Match = buildMathPropsFrom(address)
|
||||
const safe = safeSelector(store.getState(), { match })
|
||||
const web3 = getWeb3()
|
||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||
const gnosisSafe = GnosisSafe.at(address)
|
||||
|
||||
// WHEN
|
||||
const nonce = Date.now()
|
||||
const data = gnosisSafe.contract.changeThreshold.getData(3)
|
||||
await createTransaction(safe, "Change Safe's threshold", address, 0, nonce, accounts[0], data)
|
||||
await sleep(1500)
|
||||
await store.dispatch(fetchTransactions())
|
||||
|
||||
let transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||
expect(transactions.count()).toBe(1)
|
||||
|
||||
let thresholdTx: Transaction = transactions.get(0)
|
||||
expect(thresholdTx.get('tx')).toBe('')
|
||||
let firstOwnerConfirmation = thresholdTx.get('confirmations').get(0)
|
||||
if (!firstOwnerConfirmation) throw new Error()
|
||||
expect(firstOwnerConfirmation.get('status')).toBe(true)
|
||||
let secondOwnerConfirmation = thresholdTx.get('confirmations').get(1)
|
||||
if (!secondOwnerConfirmation) throw new Error()
|
||||
expect(secondOwnerConfirmation.get('status')).toBe(false)
|
||||
|
||||
let safeThreshold = await gnosisSafe.getThreshold()
|
||||
expect(Number(safeThreshold)).toEqual(2)
|
||||
|
||||
// THEN
|
||||
await changeThreshold(store, address, accounts[1])
|
||||
safeThreshold = await gnosisSafe.getThreshold()
|
||||
expect(Number(safeThreshold)).toEqual(3)
|
||||
|
||||
await store.dispatch(fetchTransactions())
|
||||
sleep(1200)
|
||||
transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||
expect(transactions.count()).toBe(1)
|
||||
|
||||
thresholdTx = transactions.get(0)
|
||||
expect(thresholdTx.get('tx')).not.toBe(undefined)
|
||||
expect(thresholdTx.get('tx')).not.toBe(null)
|
||||
expect(thresholdTx.get('tx')).not.toBe('')
|
||||
|
||||
firstOwnerConfirmation = thresholdTx.get('confirmations').get(0)
|
||||
if (!firstOwnerConfirmation) throw new Error()
|
||||
expect(firstOwnerConfirmation.get('status')).toBe(true)
|
||||
secondOwnerConfirmation = thresholdTx.get('confirmations').get(1)
|
||||
if (!secondOwnerConfirmation) throw new Error()
|
||||
expect(secondOwnerConfirmation.get('status')).toBe(true)
|
||||
})
|
||||
})
|
|
@ -46,7 +46,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
|||
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const withdrawnButton = buttons[0]
|
||||
const withdrawnButton = buttons[1]
|
||||
expect(withdrawnButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
|
||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(withdrawnButton, 'button')[0])
|
||||
await sleep(4000)
|
||||
|
@ -96,7 +96,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
|||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const addTxButton = buttons[1]
|
||||
const addTxButton = buttons[2]
|
||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||
expect(addTxButton.props.disabled).toBe(true)
|
||||
|
||||
|
@ -110,7 +110,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
|
|||
const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView)
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const addTxButton = buttons[0]
|
||||
const addTxButton = buttons[1]
|
||||
expect(addTxButton.props.children).toEqual(WITHDRAWN_BUTTON_TEXT)
|
||||
expect(addTxButton.props.disabled).toBe(true)
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export const addFundsTo = async (SafeDom, destination: string) => {
|
|||
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const addTxButton = buttons[1]
|
||||
const addTxButton = buttons[2]
|
||||
expect(addTxButton.props.children).toEqual(ADD_MULTISIG_BUTTON_TEXT)
|
||||
await sleep(1800) // Give time to enable Add button
|
||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(addTxButton, 'button')[0])
|
||||
|
@ -54,7 +54,7 @@ export const listTxsOf = (SafeDom) => {
|
|||
|
||||
// $FlowFixMe
|
||||
const buttons = TestUtils.scryRenderedComponentsWithType(Safe, Button)
|
||||
const seeTx = buttons[2]
|
||||
const seeTx = buttons[3]
|
||||
expect(seeTx.props.children).toEqual(SEE_MULTISIG_BUTTON_TEXT)
|
||||
TestUtils.Simulate.click(TestUtils.scryRenderedDOMComponentsWithTag(seeTx, 'button')[0])
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue