Merge pull request #26 from gnosis/feature/WA-238-daily-limit-logic

WA-238 Daily limit logic (including spentToday information)
This commit is contained in:
Adolfo Panizo 2018-05-16 09:25:33 +02:00 committed by GitHub
commit 5531e0fc19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 423 additions and 62 deletions

View File

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

View File

@ -113,6 +113,7 @@
"collectCoverageFrom": [
"src/**/*.{js,jsx}"
],
"setupTestFrameworkScriptFile": "<rootDir>/config/jest/jest.setup.js",
"setupFiles": [
"<rootDir>/config/webpack.config.test.js",
"<rootDir>/config/polyfills.js",

View File

@ -1208,8 +1208,14 @@
"links": {},
"address": "0x544c20ddcab0459a99c93823d0c02d50f75ced36",
"transactionHash": "0x0e6adf453722b13530f4f8c8f947f6e5105156aa99a10b588447ed57e27d7b85"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0xdeabe313841db5cddcc1b5f01c6497ece16c2347",
"transactionHash": "0x0e6adf453722b13530f4f8c8f947f6e5105156aa99a10b588447ed57e27d7b85"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.706Z"
"updatedAt": "2018-05-14T07:39:37.967Z"
}

View File

@ -7909,8 +7909,14 @@
"links": {},
"address": "0xe52c225329d3fb9f6933bd52e7067a24d20f7983",
"transactionHash": "0xaffd9cdbf1bd14f5f349af2782a1b4dbebd9ac97abedbcfb9aee5fb1707afe96"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0x452dd8d6f81786c3ad3ec3cbcf024687659c682a",
"transactionHash": "0xaffd9cdbf1bd14f5f349af2782a1b4dbebd9ac97abedbcfb9aee5fb1707afe96"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.692Z"
"updatedAt": "2018-05-14T07:39:37.963Z"
}

View File

@ -2542,8 +2542,14 @@
"links": {},
"address": "0x788256524db64c2b23ff2e417a833927550a2d65",
"transactionHash": "0x13942c7ebe4c7c49493ac8d9d8ee3c329a0be8b7a78717117e0c5d43cbf8632c"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0x3e2ade0d97956160691a96fb2adf83844155708d",
"transactionHash": "0x13942c7ebe4c7c49493ac8d9d8ee3c329a0be8b7a78717117e0c5d43cbf8632c"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.705Z"
"updatedAt": "2018-05-14T07:39:37.966Z"
}

View File

@ -8185,8 +8185,14 @@
"links": {},
"address": "0x1f8829f66b8ac7a6893109dd298007f5dd3bcadf",
"transactionHash": "0x9a582bc25c7705ede926f13bef0ba8fa76176d0ec80dc0871e1b28d87382f545"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0xad5d0371132b0959508b44c6221e6bc4de8df987",
"transactionHash": "0x9a582bc25c7705ede926f13bef0ba8fa76176d0ec80dc0871e1b28d87382f545"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.685Z"
"updatedAt": "2018-05-14T07:39:37.952Z"
}

View File

@ -5456,8 +5456,14 @@
"links": {},
"address": "0x9327970e8e29e8dba16b28acb177906a92447a0c",
"transactionHash": "0x8b91e38b0bbafe43309aca9832bcd234b82d7ac9f81abe94891cd406a735549c"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0xf2eb76d41d54cc58f9a8e86e2b42d41ccd22a8fa",
"transactionHash": "0x8b91e38b0bbafe43309aca9832bcd234b82d7ac9f81abe94891cd406a735549c"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.688Z"
"updatedAt": "2018-05-14T07:39:37.959Z"
}

View File

@ -6495,8 +6495,14 @@
"links": {},
"address": "0xd16506c40cb044bf78552d9bea796c9d98fa0a45",
"transactionHash": "0x782ef1b28e30afdcb711e7119c7bc794bbd7f42356ea5a68072b8f59400b05e3"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0xaadc387a6c96744064754aa1f2391cbe1bb55970",
"transactionHash": "0x782ef1b28e30afdcb711e7119c7bc794bbd7f42356ea5a68072b8f59400b05e3"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.681Z"
"updatedAt": "2018-05-14T07:39:37.956Z"
}

View File

@ -1392,8 +1392,14 @@
"links": {},
"address": "0xb97a46b50b9e38d540e9701d3d4afe71a65c09df",
"transactionHash": "0x137111f15934455430bea53bd8a6721561285af6a431f174f090257877635ab6"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0x3b293b12ee278dba3d4350cd5b4434c228bad69b",
"transactionHash": "0x137111f15934455430bea53bd8a6721561285af6a431f174f090257877635ab6"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.707Z"
"updatedAt": "2018-05-14T07:39:37.978Z"
}

View File

@ -348,8 +348,14 @@
"links": {},
"address": "0xa4604b882b2c10ce381c4e61ad9ac72ab32f350f",
"transactionHash": "0xf4586ae05ae02801de1759128e43658bb0439e622a5ba84ad6bb4b652d641f4f"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0xf5cfa4069271285402ba2585c521c6c627810963",
"transactionHash": "0xf4586ae05ae02801de1759128e43658bb0439e622a5ba84ad6bb4b652d641f4f"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.706Z"
"updatedAt": "2018-05-14T07:39:37.970Z"
}

View File

@ -999,8 +999,14 @@
"links": {},
"address": "0x7686eac12d94e3c0bdfe0a00ec759f89fbd115f7",
"transactionHash": "0x5b47c779cfd719a97f218a56d99b64b2c5b382549f3375822d5afed010cdb9c5"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0x321151783f8dfb4699370d1bd5cee4e82bc3b52a",
"transactionHash": "0x5b47c779cfd719a97f218a56d99b64b2c5b382549f3375822d5afed010cdb9c5"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.678Z"
"updatedAt": "2018-05-14T07:39:37.950Z"
}

View File

@ -6958,8 +6958,14 @@
"links": {},
"address": "0xfe2114e016fa8d92959754f25d4f63f155ad1a6a",
"transactionHash": "0xa514f0c5c6fcab99a16bba503b6ed893935cedfafe2e5c8c825dfc117e1e266d"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0x12fb2fe4f6d4c08b14c694e163b9bee65697e708",
"transactionHash": "0xa514f0c5c6fcab99a16bba503b6ed893935cedfafe2e5c8c825dfc117e1e266d"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.698Z"
"updatedAt": "2018-05-14T07:39:37.975Z"
}

View File

@ -3956,8 +3956,14 @@
"links": {},
"address": "0x15fd83fcf27f1726e692389be1b6e03fe7d56bb6",
"transactionHash": "0xc7f84311daf6a72740fe5822cd6007cec3ce1ff6aeaf454559f3e5f36c81cfd8"
},
"1526283540628": {
"events": {},
"links": {},
"address": "0x536f677993e3eada3e17f2f42888ee777441fc3e",
"transactionHash": "0xc7f84311daf6a72740fe5822cd6007cec3ce1ff6aeaf454559f3e5f36c81cfd8"
}
},
"schemaVersion": "2.0.0",
"updatedAt": "2018-05-10T11:07:04.695Z"
"updatedAt": "2018-05-14T07:39:37.968Z"
}

View File

@ -59,7 +59,7 @@ describe('React DOM TESTS > Create Safe form', () => {
// giving some time to the component for updating its state with safe
// before destroying its context
await sleep(1500)
await sleep(6000)
// THEN
const deployed = TestUtils.findRenderedDOMComponentWithClass(open, DEPLOYED_COMPONENT_ID)

View File

@ -3,4 +3,3 @@ export const SAFE_PARAM_ADDRESS = 'address'
export const SAFELIST_ADDRESS = '/safes'
export const OPEN_ADDRESS = '/open'
export const WELCOME_ADDRESS = '/welcome'
export const TXS_ADDRESS = '/txs'

View File

@ -30,8 +30,20 @@ storiesOf('Routes /safe:address', module)
fetchBalance={() => {}}
/>
))
.add('Safe with 2 owners', () => {
const safe = SafeFactory.twoOwnersSafe
.add('Safe with 2 owners and 10ETH as dailyLimit', () => {
const safe = SafeFactory.dailyLimitSafe(10, 1.345)
return (
<Component
safe={safe}
provider="METAMASK"
balance="2"
fetchBalance={() => {}}
/>
)
})
.add('Safe with dailyLimit reached', () => {
const safe = SafeFactory.dailyLimitSafe(10, 10)
return (
<Component

View File

@ -6,7 +6,7 @@ import { ConnectedRouter } from 'react-router-redux'
import Button from '~/components/layout/Button'
import { aNewStore, history } from '~/store'
import { addEtherTo } from '~/test/addEtherTo'
import { aDeployedSafe } from '~/routes/safe/store/test/builder/deployedSafe.builder'
import { aDeployedSafe, executeWithdrawnOn } from '~/routes/safe/store/test/builder/deployedSafe.builder'
import { SAFELIST_ADDRESS } from '~/routes/routes'
import SafeView from '~/routes/safe/component/Safe'
import AppRoutes from '~/routes'
@ -14,6 +14,7 @@ import { WITHDRAWN_BUTTON_TEXT } from '~/routes/safe/component/Safe/DailyLimit'
import WithdrawnComponent, { SEE_TXS_BUTTON_TEXT } from '~/routes/safe/component/Withdrawn'
import { getBalanceInEtherOf } from '~/wallets/getWeb3'
import { sleep } from '~/utils/timer'
import { getDailyLimitFrom } from '~/routes/safe/component/Withdrawn/withdrawn'
describe('React DOM TESTS > Withdrawn funds from safe', () => {
let SafeDom
@ -60,7 +61,7 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
TestUtils.Simulate.submit(form) // fill the form
TestUtils.Simulate.submit(form) // confirming data
await sleep(1200)
await sleep(4000)
const safeBalance = await getBalanceInEtherOf(address)
expect(safeBalance).toBe('0.09')
@ -70,4 +71,18 @@ describe('React DOM TESTS > Withdrawn funds from safe', () => {
const visitTxsButton = withdrawnButtons[0]
expect(visitTxsButton.props.children).toEqual(SEE_TXS_BUTTON_TEXT)
})
it('spentToday dailyLimitModule property is updated correctly', async () => {
// GIVEN in beforeEach
// WHEN
await executeWithdrawnOn(address, 0.01)
await executeWithdrawnOn(address, 0.01)
const ethAddress = 0
const dailyLimit: DailyLimitProps = await getDailyLimitFrom(address, ethAddress)
// THEN
expect(dailyLimit.value).toBe(0.5)
expect(dailyLimit.spentToday).toBe(0.02)
})
})

View File

@ -7,26 +7,34 @@ import Button from '~/components/layout/Button'
import ListItemText from '~/components/List/ListItemText'
type Props = {
limit: number,
dailyLimit: DailyLimit,
onWithdrawn: () => void,
}
export const WITHDRAWN_BUTTON_TEXT = 'Withdrawn'
const DailyLimit = ({ limit, onWithdrawn }: Props) => (
<ListItem>
<Avatar>
<NotificationsPaused />
</Avatar>
<ListItemText primary="Daily Limit" secondary={`${limit} ETH`} />
<Button
variant="raised"
color="primary"
onClick={onWithdrawn}
>
{WITHDRAWN_BUTTON_TEXT}
</Button>
</ListItem>
)
const DailyLimitComponent = ({ dailyLimit, onWithdrawn }: Props) => {
const limit = dailyLimit.get('value')
const spentToday = dailyLimit.get('spentToday')
const disabled = spentToday >= limit
const text = `${limit} ETH (spent today: ${spentToday} ETH)`
export default DailyLimit
return (
<ListItem>
<Avatar>
<NotificationsPaused />
</Avatar>
<ListItemText primary="Daily Limit" secondary={text} />
<Button
variant="raised"
color="primary"
onClick={onWithdrawn}
disabled={disabled}
>
{WITHDRAWN_BUTTON_TEXT}
</Button>
</ListItem>
)
}
export default DailyLimitComponent

View File

@ -40,7 +40,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
onWithdrawn = () => {
const { safe } = this.props
this.setState({ component: <Withdrawn safeAddress={safe.get('address')} /> })
this.setState({ component: <Withdrawn safeAddress={safe.get('address')} dailyLimit={safe.get('dailyLimit')} /> })
}
render() {
@ -55,7 +55,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
<Owners owners={safe.owners} />
<Confirmations confirmations={safe.get('confirmations')} />
<Address address={safe.get('address')} />
<DailyLimit limit={safe.get('dailyLimit')} onWithdrawn={this.onWithdrawn} />
<DailyLimit dailyLimit={safe.get('dailyLimit')} onWithdrawn={this.onWithdrawn} />
</List>
</Col>
<Col sm={12} center="xs" md={7} margin="xl" layout="column">

View File

@ -1,16 +1,22 @@
// @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 { DESTINATION_PARAM, VALUE_PARAM } from './withdrawn'
import { DESTINATION_PARAM, VALUE_PARAM } from '~/routes/safe/component/Withdrawn/withdrawn'
type FormProps = {
values: Object,
submitting: boolean,
}
const Review = () => ({ values }: FormProps) => (
const spinnerStyle = {
minHeight: '50px',
}
const Review = () => ({ values, submitting }: FormProps) => (
<Block>
<Heading tag="h2">Review the Withdrawn Operation</Heading>
<Paragraph align="left">
@ -19,6 +25,9 @@ const Review = () => ({ values }: FormProps) => (
<Paragraph align="left">
<Bold>Value in ETH: </Bold> {values[VALUE_PARAM]}
</Paragraph>
<Block style={spinnerStyle}>
{ submitting && <CircularProgress size={50} /> }
</Block>
</Block>
)

View File

@ -0,0 +1,23 @@
// @flow
import { storiesOf } from '@storybook/react'
import * as React from 'react'
import styles from '~/components/layout/PageFrame/index.scss'
import { makeDailyLimit, type DailyLimit } from '~/routes/safe/store/model/dailyLimit'
import Component from './index'
const FrameDecorator = story => (
<div className={styles.frame}>
{ story() }
</div>
)
storiesOf('Components', module)
.addDecorator(FrameDecorator)
.add('WitdrawnForm', () => {
const dailyLimit: DailyLimit = makeDailyLimit({ value: 10, spentToday: 6 })
return (
<Component dailyLimit={dailyLimit} safeAddress="" />
)
})

View File

@ -5,7 +5,7 @@ import TextField from '~/components/forms/TextField'
import { composeValidators, mustBeNumber, required, greaterThan, mustBeEthereumAddress } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading'
import { DESTINATION_PARAM, VALUE_PARAM } from './withdrawn'
import { DESTINATION_PARAM, VALUE_PARAM } from '~/routes/safe/component/Withdrawn/withdrawn'
export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners'
@ -19,15 +19,35 @@ export const safeFieldsValidation = (values: Object) => {
return errors
}
export default () => () => (
type Props = {
limit: number,
spentToday: number,
}
export const inLimit = (limit: number, spentToday: number) => (value: string) => {
const amount = Number(value)
const max = limit - spentToday
if (amount <= max) {
return undefined
}
return `Should not exceed ${max} ETH (amount to reach daily limit)`
}
export default ({ limit, spentToday }: Props) => () => (
<Block margin="md">
<Heading tag="h2" margin="lg">Withdrawn Funds</Heading>
<Heading tag="h2" margin="lg">
Withdrawn Funds
</Heading>
<Heading tag="h4" margin="lg">
{`Daily limit ${limit} ETH (spent today: ${spentToday} ETH)`}
</Heading>
<Block margin="md">
<Field
name={VALUE_PARAM}
component={TextField}
type="text"
validate={composeValidators(required, mustBeNumber, greaterThan(0))}
validate={composeValidators(required, mustBeNumber, greaterThan(0), inLimit(limit, spentToday))}
placeholder="Amount in ETH*"
text="Amount in ETH"
/>
@ -44,3 +64,4 @@ export default () => () => (
</Block>
</Block>
)

View File

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

View File

@ -2,7 +2,9 @@
import * as React from 'react'
import { connect } from 'react-redux'
import Stepper from '~/components/Stepper'
import { TXS_ADDRESS } from '~/routes/routes'
import { SAFELIST_ADDRESS } from '~/routes/routes'
import { sleep } from '~/utils/timer'
import actions, { type Actions } from './actions'
import selector, { type SelectorProps } from './selector'
import withdrawn from './withdrawn'
import WithdrawnForm from './WithdrawnForm'
@ -12,15 +14,16 @@ const getSteps = () => [
'Fill Withdrawn Form', 'Review Withdrawn',
]
type Props = SelectorProps & {
type Props = SelectorProps & Actions & {
safeAddress: string,
dailyLimit: DailyLimit,
}
type State = {
done: boolean,
}
export const SEE_TXS_BUTTON_TEXT = 'SEE TXS'
export const SEE_TXS_BUTTON_TEXT = 'DONE'
class Withdrawn extends React.Component<Props, State> {
state = {
@ -31,6 +34,8 @@ class Withdrawn extends React.Component<Props, State> {
try {
const { safeAddress, userAddress } = this.props
await withdrawn(values, safeAddress, userAddress)
await sleep(3500)
this.props.fetchDailyLimit(safeAddress)
this.setState({ done: true })
} catch (error) {
this.setState({ done: false })
@ -40,19 +45,20 @@ class Withdrawn extends React.Component<Props, State> {
}
render() {
const { dailyLimit, safeAddress } = this.props
const { done } = this.state
const steps = getSteps()
return (
<React.Fragment>
<Stepper
goPath={TXS_ADDRESS}
goPath={`${SAFELIST_ADDRESS}/${safeAddress}`}
goTitle={SEE_TXS_BUTTON_TEXT}
onSubmit={this.onWithdrawn}
finishedTransaction={done}
steps={steps}
>
<Stepper.Page>
<Stepper.Page limit={dailyLimit.get('value')} spentToday={dailyLimit.get('spentToday')}>
{ WithdrawnForm }
</Stepper.Page>
<Stepper.Page>
@ -64,5 +70,5 @@ class Withdrawn extends React.Component<Props, State> {
}
}
export default connect(selector)(Withdrawn)
export default connect(selector, actions)(Withdrawn)

View File

@ -1,11 +1,14 @@
// @flow
import { getWeb3 } from '~/wallets/getWeb3'
import { getGnosisSafeContract, getCreateDailyLimitExtensionContract } from '~/wallets/safeContracts'
import { type DailyLimitProps } from '~/routes/safe/store/model/safe'
export const LIMIT_POSITION = 0
export const SPENT_TODAY_POS = 1
export const DESTINATION_PARAM = 'destination'
export const VALUE_PARAM = 'ether'
const withdrawn = async (values: Object, safeAddress: string, userAccount: string): Promise<void> => {
const getDailyLimitModuleFrom = async (safeAddress) => {
const web3 = getWeb3()
const gnosisSafe = getGnosisSafeContract(web3).at(safeAddress)
@ -17,10 +20,29 @@ const withdrawn = async (values: Object, safeAddress: string, userAccount: strin
throw new Error('Using an extension of different safe')
}
return dailyLimitModule
}
export const getDailyLimitFrom = async (safeAddress, tokenAddress): DailyLimitProps => {
const web3 = getWeb3()
const dailyLimitModule = await getDailyLimitModuleFrom(safeAddress)
const dailyLimitEth = await dailyLimitModule.dailyLimits(tokenAddress)
const limit = web3.fromWei(dailyLimitEth[LIMIT_POSITION].valueOf(), 'ether').toString()
const spentToday = web3.fromWei(dailyLimitEth[SPENT_TODAY_POS].valueOf(), 'ether').toString()
return { value: Number(limit), spentToday: Number(spentToday) }
}
const withdrawn = async (values: Object, safeAddress: string, userAccount: string): Promise<void> => {
const web3 = getWeb3()
const dailyLimitModule = await getDailyLimitModuleFrom(safeAddress)
const destination = values[DESTINATION_PARAM]
const value = web3.toWei(values[VALUE_PARAM], 'ether')
await dailyLimitModule.executeDailyLimit(
return dailyLimitModule.executeDailyLimit(
destination,
value,
0,

View File

@ -0,0 +1,26 @@
// @flow
import { aNewStore } from '~/store'
import { addEtherTo } from '~/test/addEtherTo'
import { aDeployedSafe, executeWithdrawnOn } from '~/routes/safe/store/test/builder/deployedSafe.builder'
describe('Safe Blockchain Test', () => {
let store
beforeEach(async () => {
store = aNewStore()
})
it('wihdrawn should return revert error if exceeded dailyLimit', async () => {
// GIVEN
const dailyLimitValue = 0.30
const safeAddress = await aDeployedSafe(store, dailyLimitValue)
await addEtherTo(safeAddress, '0.7')
const value = 0.15
// WHEN
await executeWithdrawnOn(safeAddress, value)
await executeWithdrawnOn(safeAddress, value)
// THEN
expect(executeWithdrawnOn(safeAddress, value)).rejects.toThrow('VM Exception while processing transaction: revert')
})
})

View File

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

View File

@ -17,6 +17,8 @@ class SafeView extends React.PureComponent<Props> {
const safeAddress: string = safe.get('address')
fetchBalance(safeAddress)
}, 1500)
this.props.fetchDailyLimit(this.props.safe.get('address'))
}
componentWillUnmount() {

View File

@ -1,6 +1,7 @@
// @flow
import { List } from 'immutable'
import { createAction } from 'redux-actions'
import { makeDailyLimit, type DailyLimit } from '~/routes/safe/store/model/dailyLimit'
import { type SafeProps } from '~/routes/safe/store/model/safe'
import { makeOwner, type Owner } from '~/routes/safe/store/model/owner'
@ -12,14 +13,18 @@ export const buildOwnersFrom = (names: string[], addresses: string[]) => {
return List(owners)
}
export const buildDailyLimitFrom = (dailyLimit: number, spentToday: number = 0): DailyLimit =>
makeDailyLimit({ value: dailyLimit, spentToday })
const addSafe = createAction(
ADD_SAFE,
(
name: string, address: string,
confirmations: number, dailyLimit: number,
confirmations: number, limit: number,
ownersName: string[], ownersAddress: string[],
): SafeProps => {
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
const dailyLimit: DailyLimit = buildDailyLimitFrom(limit)
return ({
address, name, confirmations, owners, dailyLimit,

View File

@ -0,0 +1,12 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { type GlobalState } from '~/store/index'
import { getDailyLimitFrom } from '~/routes/safe/component/Withdrawn/withdrawn'
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,19 @@
// @flow
import { createAction } from 'redux-actions'
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,15 @@
// @flow
import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
export type DailyLimitProps = {
value: number,
spentToday: number,
}
export const makeDailyLimit: RecordFactory<DailyLimitProps> = Record({
value: 0,
spentToday: 0,
})
export type DailyLimit = RecordOf<DailyLimitProps>

View File

@ -1,6 +1,7 @@
// @flow
import { List, Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
import { type DailyLimit, makeDailyLimit } from '~/routes/safe/store/model/dailyLimit'
import type { Owner } from '~/routes/safe/store/model/owner'
export type SafeProps = {
@ -8,7 +9,7 @@ export type SafeProps = {
address: string,
confirmations: number,
owners: List<Owner>,
dailyLimit: number,
dailyLimit: DailyLimit,
}
export const makeSafe: RecordFactory<SafeProps> = Record({
@ -16,7 +17,7 @@ export const makeSafe: RecordFactory<SafeProps> = Record({
address: '',
confirmations: 0,
owners: List([]),
dailyLimit: 0,
dailyLimit: makeDailyLimit(),
})
export type Safe = RecordOf<SafeProps>

View File

@ -2,9 +2,11 @@
import { Map, List } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions'
import addSafe, { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
import updateDailyLimit, { UPDATE_DAILY_LIMIT } from '~/routes/safe/store/actions/updateDailyLimit'
import { makeOwner } from '~/routes/safe/store/model/owner'
import { type Safe, makeSafe } from '~/routes/safe/store/model/safe'
import { loadSafes, saveSafes } from '~/utils/localStorage'
import { makeDailyLimit } from '~/routes/safe/store/model/dailyLimit'
export const SAFE_REDUCER_ID = 'safes'
@ -17,6 +19,7 @@ const buildSafesFrom = (loadedSafes: Object): 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))
})
})
@ -45,4 +48,6 @@ export default handleActions({
saveSafes(safes.toJSON())
return safes
},
[UPDATE_DAILY_LIMIT]: (state: State, action: ActionType<typeof updateDailyLimit>): State =>
state.updateIn([action.payload.safeAddress, 'dailyLimit'], () => makeDailyLimit(action.payload.dailyLimit)),
}, Map())

View File

@ -11,6 +11,7 @@ import { sleep } from '~/utils/timer'
import { getProviderInfo } from '~/wallets/getWeb3'
import addProvider from '~/wallets/store/actions/addProvider'
import { makeProvider } from '~/wallets/store/model/provider'
import withdrawn, { DESTINATION_PARAM, VALUE_PARAM } from '~/routes/safe/component/Withdrawn/withdrawn'
export const renderSafe = async (localStore: Store<GlobalState>) => {
const provider = await getProviderInfo()
@ -28,7 +29,7 @@ export const renderSafe = async (localStore: Store<GlobalState>) => {
)
}
const deploySafe = async (safe: React$Component<{}>) => {
const deploySafe = async (safe: React$Component<{}>, dailyLimit: string) => {
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(safe, 'input')
const fieldName = inputs[0]
const fieldOwners = inputs[1]
@ -42,7 +43,7 @@ const deploySafe = async (safe: React$Component<{}>) => {
TestUtils.Simulate.change(fieldName, { target: { value: 'Adolfo Safe' } })
TestUtils.Simulate.change(fieldConfirmations, { target: { value: '1' } })
TestUtils.Simulate.change(ownerName, { target: { value: 'Adolfo Eth Account' } })
TestUtils.Simulate.change(fieldDailyLimit, { target: { value: '0.5' } })
TestUtils.Simulate.change(fieldDailyLimit, { target: { value: dailyLimit } })
const form = TestUtils.findRenderedDOMComponentWithTag(safe, 'form')
@ -52,7 +53,7 @@ const deploySafe = async (safe: React$Component<{}>) => {
// giving some time to the component for updating its state with safe
// before destroying its context
await sleep(1500)
await sleep(3500)
// THEN
const deployed = TestUtils.findRenderedDOMComponentWithClass(safe, DEPLOYED_COMPONENT_ID)
@ -66,9 +67,21 @@ const deploySafe = async (safe: React$Component<{}>) => {
return transactionHash
}
export const aDeployedSafe = async (specificStore: Store<GlobalState>) => {
export const aDeployedSafe = async (specificStore: Store<GlobalState>, dailyLimit?: number = 0.5) => {
const safe: React$Component<{}> = await renderSafe(specificStore)
const deployedSafe = await deploySafe(safe)
const deployedSafe = await deploySafe(safe, `${dailyLimit}`)
return deployedSafe.logs[1].args.proxy
}
export const executeWithdrawnOn = async (safeAddress: string, value: number) => {
const providerInfo = await getProviderInfo()
const userAddress = providerInfo.account
const values = {
[DESTINATION_PARAM]: userAddress,
[VALUE_PARAM]: `${value}`,
}
return withdrawn(values, safeAddress, userAddress)
}

View File

@ -1,6 +1,6 @@
// @flow
import { makeSafe, type Safe } from '~/routes/safe/store/model/safe'
import { buildOwnersFrom } from '~/routes/safe/store/actions'
import { buildOwnersFrom, buildDailyLimitFrom } from '~/routes/safe/store/actions'
class SafeBuilder {
safe: Safe
@ -24,8 +24,9 @@ class SafeBuilder {
return this
}
withDailyLimit(limit: number) {
this.safe = this.safe.set('dailyLimit', limit)
withDailyLimit(limit: number, spentToday: number = 0) {
const dailyLimit = buildDailyLimitFrom(limit, spentToday)
this.safe = this.safe.set('dailyLimit', dailyLimit)
return this
}
@ -59,7 +60,19 @@ export class SafeFactory {
['Adol Metamask', 'Tobias Metamask'],
['0x03db1a8b26d08df23337e9276a36b474510f0023', '0x03db1a8b26d08df23337e9276a36b474510f0024'],
)
.withDailyLimit(10, 1.34)
.get()
static dailyLimitSafe = (dailyLimit: number, spentToday: number) => aSafe()
.withAddress('0x03db1a8b26d08df23337e9276a36b474510f0026')
.withName('Adol & Tobias Safe')
.withConfirmations(2)
.withOwner(
['Adol Metamask', 'Tobias Metamask'],
['0x03db1a8b26d08df23337e9276a36b474510f0023', '0x03db1a8b26d08df23337e9276a36b474510f0024'],
)
.withDailyLimit(dailyLimit, spentToday)
.get()
}
export default aSafe

View File

@ -0,0 +1,51 @@
// @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,6 +1,7 @@
// @flow
import balanceReducerTests from './balance.reducer'
import safeReducerTests from './safe.reducer'
import dailyLimitReducerTests from './dailyLimit.reducer'
import balanceSelectorTests from './balance.selector'
import safeSelectorTests from './safe.selector'
@ -8,6 +9,7 @@ describe('Safe Test suite', () => {
// ACTIONS AND REDUCERS
safeReducerTests()
balanceReducerTests()
dailyLimitReducerTests()
// SAFE SELECTOR
safeSelectorTests()

View File

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