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:
parent
88bfca0a0d
commit
f4284d513a
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.block {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sm {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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 Nº {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 Nº {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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -49,7 +49,7 @@ const GnoSafe = ({ safe }: SafeProps) => (
|
|||
</Paragraph>
|
||||
</Row>
|
||||
<Row margin="lg">
|
||||
<Table>
|
||||
<Table size={700}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
|
|
|
@ -12,7 +12,7 @@ type Props = {
|
|||
}
|
||||
|
||||
const SafeList = ({ safes }: Props) => (
|
||||
<Page overflow>
|
||||
<Page>
|
||||
<Layout safes={safes} />
|
||||
</Page>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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.");
|
||||
}
|
||||
|
||||
}
|
|
@ -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.");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in New Issue