WA-280 - Feature: improve safe creation flow (#13)

* WA-280 limit the number of owners to 50

* WA-280 Creating a text for assuring error is raised when confirmations > owners

* WA-280 Allowing to finish create safe transaction even if the user leaves the page
This commit is contained in:
Adolfo Panizo 2018-04-13 09:37:47 +02:00 committed by GitHub
parent 88bfca0a0d
commit f4284d513a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 198 additions and 130 deletions

View File

@ -3,6 +3,11 @@
echo "Deployment script for gnosis-safe-team"
RANGE=500
number=$RANDOM
let "number %= $RANGE"
# Split on "/", ref: http://stackoverflow.com/a/5257398/689223
REPO_SLUG_ARRAY=(${TRAVIS_REPO_SLUG//\// })
REPO_OWNER=${REPO_SLUG_ARRAY[0]}
@ -17,9 +22,9 @@ if [ "$TRAVIS_PULL_REQUEST" != "false" ]
then
if [ "$NODE_ENV" == "production" ]
then
DEPLOY_SUBDOMAIN_UNFORMATTED_LIST+=(release-${TRAVIS_PULL_REQUEST}-pr)
DEPLOY_SUBDOMAIN_UNFORMATTED_LIST+=(release-${TRAVIS_PULL_REQUEST}-pr-${number})
else
DEPLOY_SUBDOMAIN_UNFORMATTED_LIST+=(staging-${TRAVIS_PULL_REQUEST}-pr)
DEPLOY_SUBDOMAIN_UNFORMATTED_LIST+=(staging-${TRAVIS_PULL_REQUEST}-pr-${number})
fi
elif [ -n "${TRAVIS_TAG// }" ] #TAG is not empty
then

View File

@ -2,30 +2,39 @@
import React from 'react'
import MuiTextField, { TextFieldProps } from 'material-ui/TextField'
const TextField = ({
input: {
name, onChange, value, ...restInput
},
meta,
render,
text,
...rest
}: TextFieldProps) => {
const helperText = value ? text : undefined
const showError = meta.touched && !meta.valid
// Neded for solving a fix in Windows browsers
const overflowStyle = {
overflow: 'hidden',
}
return (
<MuiTextField
{...rest}
name={name}
helperText={showError ? meta.error : helperText}
error={meta.error && meta.touched}
inputProps={restInput}
onChange={onChange}
value={value}
fullWidth
/>
)
class TextField extends React.PureComponent<TextFieldProps> {
render() {
const {
input: {
name, onChange, value, ...restInput
},
meta,
render,
text,
...rest
} = this.props
const helperText = value ? text : undefined
const showError = (meta.touched || !meta.pristine) && !meta.valid
return (
<MuiTextField
style={overflowStyle}
{...rest}
name={name}
helperText={showError ? meta.error : helperText}
error={meta.error && (meta.touched || !meta.pristine)}
inputProps={restInput}
onChange={onChange}
value={value}
fullWidth
/>
)
}
}
export default TextField

View File

@ -1,7 +1,7 @@
.block {
display: inline-block;
width: 100%;
overflow-x: hidden;
overflow: hidden;
}
.sm {

View File

@ -13,14 +13,20 @@ const buildWidthFrom = (size: number) => ({
minWidth: `${size}px`,
})
const overflowStyle = {
overflowX: 'scroll',
}
// see: https://css-tricks.com/responsive-data-tables/
const GnoTable = ({ size, children }: Props) => {
const style = size ? buildWidthFrom(size) : undefined
return (
<Table style={style}>
{children}
</Table>
<div style={overflowStyle}>
<Table style={style}>
{children}
</Table>
</div>
)
}

View File

@ -66,16 +66,13 @@ describe('React DOM TESTS > Create Safe form', () => {
await sleep(1500)
// THEN
const Deployed = TestUtils.findRenderedDOMComponentWithClass(open, DEPLOYED_COMPONENT_ID)
const deployed = TestUtils.findRenderedDOMComponentWithClass(open, DEPLOYED_COMPONENT_ID)
const addressHtml = Deployed.getElementsByTagName('p')[0].innerHTML
const contractAddress = addressHtml.slice(addressHtml.lastIndexOf('>') + 1)
const transactionHash = JSON.parse(Deployed.getElementsByTagName('pre')[0].innerHTML)
delete transactionHash.logsBloom
// eslint-disable-next-line
console.log('Deployed safe address is: ' + contractAddress)
// eslint-disable-next-line
console.log(transactionHash)
if (deployed) {
const transactionHash = JSON.parse(deployed.getElementsByTagName('pre')[0].innerHTML)
delete transactionHash.logsBloom
// eslint-disable-next-line
console.log(transactionHash)
}
})
})

View File

@ -0,0 +1,51 @@
// @flow
import TextField from '~/components/forms/TextField'
import * as React from 'react'
import * as TestUtils from 'react-dom/test-utils'
import Layout from '~/routes/open/components/Layout'
import { FIELD_CONFIRMATIONS, FIELD_OWNERS } from '~/routes/open/components/fields'
import { getProviderInfo } from '~/wallets/getWeb3'
import Wrapper from '~/test/Wrapper'
import { CONFIRMATIONS_ERROR } from '~/routes/open/components/SafeForm'
describe('React DOM TESTS > Create Safe form', () => {
beforeEach(async () => {
// init app web3 instance
await getProviderInfo()
})
it('should not allow to continue if confirmations are higher than owners', async () => {
// GIVEN
const open = TestUtils.renderIntoDocument((
<Wrapper>
<Layout
provider="METAMASK"
userAccount="foo"
safeAddress=""
safeTx=""
onCallSafeContractSubmit={() => { }}
/>
</Wrapper>
))
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(open, 'input')
const fieldOwners = inputs[1]
expect(fieldOwners.name).toEqual(FIELD_OWNERS)
TestUtils.Simulate.change(fieldOwners, { target: { value: '1' } })
const fieldConfirmations = inputs[2]
expect(fieldConfirmations.name).toEqual(FIELD_CONFIRMATIONS)
// WHEN
TestUtils.Simulate.change(fieldConfirmations, { target: { value: '2' } })
// THEN
const muiFields = TestUtils.scryRenderedComponentsWithType(open, TextField)
expect(5).toEqual(muiFields.length)
const confirmationsField = muiFields[4]
expect(confirmationsField.props.meta.valid).toBe(false)
expect(confirmationsField.props.meta.error).toBe(CONFIRMATIONS_ERROR)
})
})

View File

@ -2,7 +2,7 @@
import * as React from 'react'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { composeValidators, minValue, mustBeNumber, mustBeEthereumAddress, required } from '~/components/forms/validator'
import { composeValidators, minValue, maxValue, mustBeNumber, mustBeEthereumAddress, required } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col'
import Heading from '~/components/layout/Heading'
@ -14,49 +14,57 @@ type Props = {
numOwners: number,
}
const Owners = ({ numOwners }: Props) => (
<Block margin="md">
<Heading tag="h3">Owners</Heading>
<Block margin="sm">
<Field
name={FIELD_OWNERS}
component={TextField}
type="text"
validate={composeValidators(required, mustBeNumber, minValue(1))}
placeholder="Number of owners*"
text="Number of owners"
/>
const MAX_NUMBER_OWNERS = 50
const Owners = (props: Props) => {
const { numOwners } = props
const validNumber = numOwners && Number.isInteger(Number(numOwners))
const renderOwners = validNumber && Number(numOwners) <= MAX_NUMBER_OWNERS
return (
<Block margin="md">
<Heading tag="h3">Owners</Heading>
<Block margin="sm">
<Field
name={FIELD_OWNERS}
component={TextField}
type="text"
validate={composeValidators(required, mustBeNumber, maxValue(MAX_NUMBER_OWNERS), minValue(1))}
placeholder="Number of owners*"
text="Number of owners"
/>
</Block>
{ renderOwners && [...Array(Number(numOwners))].map((x, index) => (
<Row key={`owner${(index)}`}>
<Col xs={11} xsOffset={1}>
<Block margin="sm">
<Paragraph bold>Owner {index + 1}</Paragraph>
<Block margin="sm">
<Field
name={getOwnerNameBy(index)}
component={TextField}
type="text"
validate={required}
placeholder="Owner Name*"
text="Owner Name"
/>
</Block>
<Block margin="sm">
<Field
name={getOwnerAddressBy(index)}
component={TextField}
type="text"
validate={composeValidators(required, mustBeEthereumAddress)}
placeholder="Owner Address*"
text="Owner Address"
/>
</Block>
</Block>
</Col>
</Row>
)) }
</Block>
{ numOwners && Number.isInteger(Number(numOwners)) && [...Array(Number(numOwners))].map((x, index) => (
<Row key={`owner${(index)}`}>
<Col xs={11} xsOffset={1}>
<Block margin="sm">
<Paragraph bold>Owner {index + 1}</Paragraph>
<Block margin="sm">
<Field
name={getOwnerNameBy(index)}
component={TextField}
type="text"
validate={required}
placeholder="Owner Name*"
text="Owner Name"
/>
</Block>
<Block margin="sm">
<Field
name={getOwnerAddressBy(index)}
component={TextField}
type="text"
validate={composeValidators(required, mustBeEthereumAddress)}
placeholder="Owner Address*"
text="Owner Address"
/>
</Block>
</Block>
</Col>
</Row>
)) }
</Block>
)
)
}
export default Owners

View File

@ -6,11 +6,13 @@ import Name from './Name'
import Owners from './Owners'
import Confirmations from './Confirmations'
export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners'
export const safeFieldsValidation = (values: Object) => {
const errors = {}
if (values.owners < values.confirmations) {
errors.confirmations = 'Number of confirmations can not be higher than the number of owners'
errors.confirmations = CONFIRMATIONS_ERROR
}
return errors

View File

@ -21,6 +21,20 @@ type State = {
safeTx: string,
}
const createSafe = async (safeContract, values, userAccount, addSafe) => {
const accounts = getAccountsFrom(values)
const numConfirmations = getThresholdFrom(values)
const name = getSafeNameFrom(values)
const owners = getNamesFrom(values)
const web3 = getWeb3()
safeContract.setProvider(web3.currentProvider)
const safe = await safeContract.new(accounts, numConfirmations, 0, 0, { from: userAccount, gas: '5000000' })
addSafe(name, safe.address, numConfirmations, owners, accounts)
return safe
}
class Open extends React.Component<Props, State> {
constructor() {
super()
@ -36,19 +50,13 @@ class Open extends React.Component<Props, State> {
onCallSafeContractSubmit = async (values) => {
try {
const { userAccount, addSafe } = this.props
const accounts = getAccountsFrom(values)
const numConfirmations = getThresholdFrom(values)
const name = getSafeNameFrom(values)
const owners = getNamesFrom(values)
const web3 = getWeb3()
this.safe.setProvider(web3.currentProvider)
const safeInstance = await this.safe.new(accounts, numConfirmations, 0, 0, { from: userAccount, gas: '5000000' })
const safeInstance = await createSafe(this.safe, values, userAccount, addSafe)
const { address, transactionHash } = safeInstance
const transactionReceipt = await promisify(cb => web3.eth.getTransactionReceipt(transactionHash, cb))
addSafe(name, address, numConfirmations, owners, accounts)
this.setState({ safeAddress: address, safeTx: transactionReceipt })
} catch (error) {
// eslint-disable-next-line

View File

@ -49,7 +49,7 @@ const GnoSafe = ({ safe }: SafeProps) => (
</Paragraph>
</Row>
<Row margin="lg">
<Table>
<Table size={700}>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>

View File

@ -12,7 +12,7 @@ type Props = {
}
const SafeList = ({ safes }: Props) => (
<Page overflow>
<Page>
<Layout safes={safes} />
</Page>
)

18
src/test/Wrapper.jsx Normal file
View File

@ -0,0 +1,18 @@
// @flow
import * as React from 'react'
type WrapperProps = {
children: React$Node
}
class Wrapper extends React.PureComponent<WrapperProps> {
render() {
return (
<React.Fragment>
{ this.props.children }
</React.Fragment>
)
}
}
export default Wrapper

View File

@ -1,19 +0,0 @@
pragma solidity ^0.4.2;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/SimpleStorage.sol";
contract TestSimpleStorage {
function testItStoresAValue() public {
SimpleStorage simpleStorage = SimpleStorage(DeployedAddresses.SimpleStorage());
simpleStorage.set(89);
uint expected = 89;
Assert.equal(simpleStorage.get(), expected, "It should store the value 89.");
}
}

View File

@ -1,17 +0,0 @@
var SimpleStorage = artifacts.require("./SimpleStorage.sol");
contract('SimpleStorage', function(accounts) {
it("...should store the value 89.", function() {
return SimpleStorage.deployed().then(function(instance) {
simpleStorageInstance = instance;
return simpleStorageInstance.set(89, {from: accounts[0]});
}).then(function() {
return simpleStorageInstance.get.call();
}).then(function(storedData) {
assert.equal(storedData, 89, "The value 89 was not stored.");
});
});
});