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:
commit
5531e0fc19
|
@ -0,0 +1,2 @@
|
|||
// @flow
|
||||
jest.setTimeout(30000)
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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="" />
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// @flow
|
||||
import fetchDailyLimit from '~/routes/safe/store/actions/fetchDailyLimit'
|
||||
|
||||
export type Actions = {
|
||||
fetchDailyLimit: typeof fetchDailyLimit,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchDailyLimit,
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue