Merge pull request #146 from gnosis/125-ens

Feature #125: ENS Integration
This commit is contained in:
Mikhail Mikheev 2019-08-19 13:40:57 +04:00 committed by GitHub
commit f4f4787d23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1489 additions and 684 deletions

View File

@ -14,7 +14,7 @@ module.exports = {
'<rootDir>/config/jest/LocalStorageMock.js', '<rootDir>/config/jest/LocalStorageMock.js',
'<rootDir>/config/jest/Web3Mock.js', '<rootDir>/config/jest/Web3Mock.js',
], ],
setupFilesAfterEnv: ['<rootDir>/config/jest/jest.setup.js', '@testing-library/react/cleanup-after-each'], setupFilesAfterEnv: ['<rootDir>/config/jest/jest.setup.js'],
testEnvironment: 'node', testEnvironment: 'node',
testMatch: ['<rootDir>/src/**/__tests__/**/*.js?(x)', '<rootDir>/src/**/?(*.)(spec|test).js?(x)'], testMatch: ['<rootDir>/src/**/__tests__/**/*.js?(x)', '<rootDir>/src/**/?(*.)(spec|test).js?(x)'],
testURL: 'http://localhost:8000', testURL: 'http://localhost:8000',

View File

@ -31,27 +31,28 @@
"dependencies": { "dependencies": {
"@gnosis.pm/safe-contracts": "^1.0.0", "@gnosis.pm/safe-contracts": "^1.0.0",
"@gnosis.pm/util-contracts": "2.0.1", "@gnosis.pm/util-contracts": "2.0.1",
"@material-ui/core": "4.2.1", "@material-ui/core": "4.3.2",
"@material-ui/icons": "4.2.1", "@material-ui/icons": "4.2.1",
"@testing-library/jest-dom": "^4.0.0", "@testing-library/jest-dom": "^4.0.0",
"@welldone-software/why-did-you-render": "3.2.1", "@welldone-software/why-did-you-render": "3.3.3",
"axios": "0.19.0", "axios": "0.19.0",
"bignumber.js": "9.0.0", "bignumber.js": "9.0.0",
"connected-react-router": "6.5.2", "connected-react-router": "6.5.2",
"date-fns": "1.30.1", "date-fns": "1.30.1",
"final-form": "4.18.2", "ethereum-ens": "^0.7.7",
"final-form": "4.18.5",
"history": "^4.7.2", "history": "^4.7.2",
"immortal-db": "^1.0.2", "immortal-db": "^1.0.2",
"immutable": "^4.0.0-rc.9", "immutable": "^4.0.0-rc.9",
"material-ui-search-bar": "^1.0.0-beta.13", "material-ui-search-bar": "^1.0.0-beta.13",
"optimize-css-assets-webpack-plugin": "5.0.3", "optimize-css-assets-webpack-plugin": "5.0.3",
"qrcode.react": "^0.9.3", "qrcode.react": "^0.9.3",
"react": "^16.8.6", "react": "16.9.0",
"react-dom": "^16.8.6", "react-dom": "16.9.0",
"react-final-form": "6.3.0", "react-final-form": "6.3.0",
"react-final-form-listeners": "^1.0.2", "react-final-form-listeners": "^1.0.2",
"react-hot-loader": "4.12.8", "react-hot-loader": "4.12.11",
"react-infinite-scroll-component": "^4.5.2", "react-infinite-scroll-component": "4.5.3",
"react-qr-reader": "^2.2.1", "react-qr-reader": "^2.2.1",
"react-redux": "7.1.0", "react-redux": "7.1.0",
"react-router-dom": "^5.0.1", "react-router-dom": "^5.0.1",
@ -60,7 +61,7 @@
"redux-actions": "^2.3.0", "redux-actions": "^2.3.0",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"web3": "1.0.0-beta.37" "web3": "1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.5.5", "@babel/cli": "7.5.5",
@ -88,11 +89,11 @@
"@babel/preset-flow": "^7.0.0-beta.40", "@babel/preset-flow": "^7.0.0-beta.40",
"@babel/preset-react": "^7.0.0-beta.40", "@babel/preset-react": "^7.0.0-beta.40",
"@sambego/storybook-state": "^1.0.7", "@sambego/storybook-state": "^1.0.7",
"@storybook/addon-actions": "5.1.9", "@storybook/addon-actions": "5.1.11",
"@storybook/addon-knobs": "5.1.9", "@storybook/addon-knobs": "5.1.11",
"@storybook/addon-links": "5.1.9", "@storybook/addon-links": "5.1.11",
"@storybook/react": "5.1.9", "@storybook/react": "5.1.11",
"@testing-library/react": "8.0.5", "@testing-library/react": "9.1.1",
"autoprefixer": "9.6.1", "autoprefixer": "9.6.1",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-eslint": "10.0.2", "babel-eslint": "10.0.2",
@ -102,19 +103,19 @@
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0", "babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.22.0", "babel-plugin-transform-es3-property-literals": "^6.22.0",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"css-loader": "3.1.0", "css-loader": "3.2.0",
"detect-port": "^1.2.2", "detect-port": "^1.2.2",
"eslint": "5.16.0", "eslint": "5.16.0",
"eslint-config-airbnb": "17.1.1", "eslint-config-airbnb": "18.0.1",
"eslint-plugin-flowtype": "3.12.1", "eslint-plugin-flowtype": "4.2.0",
"eslint-plugin-import": "2.18.1", "eslint-plugin-import": "2.18.2",
"eslint-plugin-jest": "22.11.1", "eslint-plugin-jest": "22.15.1",
"eslint-plugin-jsx-a11y": "6.2.3", "eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.14.2", "eslint-plugin-react": "7.14.3",
"ethereumjs-abi": "^0.6.7", "ethereumjs-abi": "0.6.8",
"extract-text-webpack-plugin": "^4.0.0-beta.0", "extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "4.1.0", "file-loader": "4.2.0",
"flow-bin": "0.103.0", "flow-bin": "0.105.2",
"fs-extra": "8.1.0", "fs-extra": "8.1.0",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.0.4", "html-webpack-plugin": "^3.0.4",
@ -130,15 +131,15 @@
"run-with-testrpc": "0.3.1", "run-with-testrpc": "0.3.1",
"storybook-host": "5.1.0", "storybook-host": "5.1.0",
"storybook-router": "^0.3.3", "storybook-router": "^0.3.3",
"style-loader": "^0.23.1", "style-loader": "1.0.0",
"truffle": "5.0.28", "truffle": "5.0.31",
"truffle-contract": "4.0.25", "truffle-contract": "4.0.28",
"truffle-solidity-loader": "0.1.27", "truffle-solidity-loader": "0.1.30",
"uglifyjs-webpack-plugin": "2.1.3", "uglifyjs-webpack-plugin": "2.2.0",
"webpack": "4.36.1", "webpack": "4.39.2",
"webpack-bundle-analyzer": "3.3.2", "webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.3.6", "webpack-cli": "3.3.6",
"webpack-dev-server": "3.7.2", "webpack-dev-server": "3.8.0",
"webpack-manifest-plugin": "^2.0.0-rc.2" "webpack-manifest-plugin": "^2.0.0-rc.2"
} }
} }

View File

@ -0,0 +1,100 @@
// @flow
import * as React from 'react'
import { Field } from 'react-final-form'
import { OnChange } from 'react-final-form-listeners'
import TextField from '~/components/forms/TextField'
import {
composeValidators,
required,
mustBeEthereumAddress,
} from '~/components/forms/validator'
import { getAddressFromENS } from '~/logic/wallets/getWeb3'
type Props = {
className?: string,
name?: string,
text?: string,
placeholder?: string,
fieldMutator: Function,
testId?: string,
validators?: Function[],
inputAdornment?: React.Element,
}
const isValidEnsName = (name) => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name)
// an idea for second field was taken from here
// https://github.com/final-form/react-final-form-listeners/blob/master/src/OnBlur.js
const AddressInput = ({
className = '',
name = 'recipientAddress',
text = 'Recipient*',
placeholder = 'Recipient*',
fieldMutator,
testId,
inputAdornment,
validators = [],
}: Props): React.Element<*> => (
<>
<Field
name={name}
component={TextField}
type="text"
validate={composeValidators(
required,
mustBeEthereumAddress,
...validators,
)}
inputAdornment={inputAdornment}
placeholder={placeholder}
text={text}
className={className}
testId={testId}
/>
<OnChange name={name}>
{async (value) => {
if (isValidEnsName(value)) {
try {
const resolverAddr = await getAddressFromENS(value)
fieldMutator(resolverAddr)
} catch (err) {
console.error('Failed to resolve address for ENS name: ', err)
}
}
}}
</OnChange>
{/* onBlur - didn't work because of the complex validation
(if you submit before it gets the address, breaks everything) */}
{/* <Field
name={name}
subscription={{ active: true, value: true }}
render={({ meta, input }) => {
const [prevActive, setPrevActive] = useState<boolean>(!!meta.active)
useEffect(() => {
async function setAddressFromENS() {
if (isValidEnsName(input.value)) {
try {
const resolverAddr = await getAddressFromENS(input.value)
fieldMutator(resolverAddr)
} catch (err) {
console.error('Error when trying to fetch address for ENS name: ', err)
}
}
}
if (prevActive && !meta.active) {
setAddressFromENS()
} else if (prevActive !== meta.active) {
setPrevActive(meta.active)
}
}, [meta.active])
return null
}}
/> */}
</>
)
export default AddressInput

View File

@ -61,7 +61,7 @@ export const ok = () => undefined
export const mustBeEthereumAddress = simpleMemoize((address: Field) => { export const mustBeEthereumAddress = simpleMemoize((address: Field) => {
const isAddress: boolean = getWeb3().utils.isAddress(address) const isAddress: boolean = getWeb3().utils.isAddress(address)
return isAddress ? undefined : 'Address should be a valid Ethereum address' return isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name'
}) })
export const minMaxLength = (minLen: string | number, maxLen: string | number) => (value: string) => (value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`) export const minMaxLength = (minLen: string | number, maxLen: string | number) => (value: string) => (value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`)

View File

@ -1,5 +1,6 @@
// @flow // @flow
import Web3 from 'web3' import Web3 from 'web3'
import ENS from 'ethereum-ens'
import type { ProviderProps } from '~/logic/wallets/store/model/provider' import type { ProviderProps } from '~/logic/wallets/store/model/provider'
export const ETHEREUM_NETWORK = { export const ETHEREUM_NETWORK = {
@ -105,6 +106,13 @@ export const getProviderInfo: Function = async (): Promise<ProviderProps> => {
} }
} }
export const getAddressFromENS = async (name: string) => {
const ens = new ENS(web3)
const address = await ens.resolver(name).addr()
return address
}
export const getBalanceInEtherOf = async (safeAddress: string) => { export const getBalanceInEtherOf = async (safeAddress: string) => {
const funds: String = await web3.eth.getBalance(safeAddress) const funds: String = await web3.eth.getBalance(safeAddress)

View File

@ -1,24 +1,26 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/Proxy.json'
import InputAdornment from '@material-ui/core/InputAdornment'
import CheckCircle from '@material-ui/icons/CheckCircle'
import Field from '~/components/forms/Field' import Field from '~/components/forms/Field'
import AddressInput from '~/components/forms/AddressInput'
import { import {
composeValidators, required, noErrorsOn, mustBeEthereumAddress, composeValidators, required, noErrorsOn, mustBeEthereumAddress,
} from '~/components/forms/validator' } from '~/components/forms/validator'
import TextField from '~/components/forms/TextField' import TextField from '~/components/forms/TextField'
import InputAdornment from '@material-ui/core/InputAdornment'
import CheckCircle from '@material-ui/icons/CheckCircle'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import OpenPaper from '~/components/Stepper/OpenPaper' import OpenPaper from '~/components/Stepper/OpenPaper'
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields' import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/Proxy.json'
import { getSafeMasterContract } from '~/logic/contracts/safeContracts' import { getSafeMasterContract } from '~/logic/contracts/safeContracts'
type Props = { type Props = {
classes: Object, classes: Object,
errors: Object, errors: Object,
form: Object,
} }
const styles = () => ({ const styles = () => ({
@ -80,8 +82,8 @@ export const safeFieldsValidation = async (values: Object) => {
return errors return errors
} }
const Details = ({ classes, errors }: Props) => ( const Details = ({ classes, errors, form }: Props) => (
<React.Fragment> <>
<Block margin="sm"> <Block margin="sm">
<Paragraph noMargin size="md" color="primary"> <Paragraph noMargin size="md" color="primary">
Adding an existing Safe only requires the Safe address. Optionally you can give it a name. In case your Adding an existing Safe only requires the Safe address. Optionally you can give it a name. In case your
@ -99,9 +101,12 @@ const Details = ({ classes, errors }: Props) => (
/> />
</Block> </Block>
<Block margin="lg" className={classes.root}> <Block margin="lg" className={classes.root}>
<Field <AddressInput
name={FIELD_LOAD_ADDRESS} name={FIELD_LOAD_ADDRESS}
component={TextField} component={TextField}
fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}}
inputAdornment={ inputAdornment={
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && { noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
endAdornment: ( endAdornment: (
@ -117,17 +122,17 @@ const Details = ({ classes, errors }: Props) => (
text="Safe Address" text="Safe Address"
/> />
</Block> </Block>
</React.Fragment> </>
) )
const DetailsForm = withStyles(styles)(Details) const DetailsForm = withStyles(styles)(Details)
const DetailsPage = () => (controls: React.Node, { errors }: Object) => ( const DetailsPage = () => (controls: React.Node, { errors, form }: Object) => (
<React.Fragment> <>
<OpenPaper controls={controls} container={605}> <OpenPaper controls={controls} container={605}>
<DetailsForm errors={errors} /> <DetailsForm errors={errors} form={form} />
</OpenPaper> </OpenPaper>
</React.Fragment> </>
) )
export default DetailsPage export default DetailsPage

View File

@ -29,6 +29,12 @@ const back = () => {
history.goBack() history.goBack()
} }
const formMutators = {
setValue: ([field, value], state, { changeValue }) => {
changeValue(state, field, () => value)
},
}
const Layout = ({ const Layout = ({
provider, onLoadSafeSubmit, network, userAddress, provider, onLoadSafeSubmit, network, userAddress,
}: Props) => { }: Props) => {
@ -36,7 +42,7 @@ const Layout = ({
const initialValues = {} const initialValues = {}
return ( return (
<React.Fragment> <>
{provider ? ( {provider ? (
<Block> <Block>
<Row align="center"> <Row align="center">
@ -45,7 +51,13 @@ const Layout = ({
</IconButton> </IconButton>
<Heading tag="h2">Load existing Safe</Heading> <Heading tag="h2">Load existing Safe</Heading>
</Row> </Row>
<Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form"> <Stepper
onSubmit={onLoadSafeSubmit}
steps={steps}
initialValues={initialValues}
mutators={formMutators}
testId="load-safe-form"
>
<StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage> <StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage>
<StepperPage network={network}>{OwnerList}</StepperPage> <StepperPage network={network}>{OwnerList}</StepperPage>
<StepperPage network={network} userAddress={userAddress}> <StepperPage network={network} userAddress={userAddress}>
@ -56,7 +68,7 @@ const Layout = ({
) : ( ) : (
<div>No account detected</div> <div>No account detected</div>
)} )}
</React.Fragment> </>
) )
} }

View File

@ -118,7 +118,7 @@ const OwnerListComponent = (props: Props) => {
}, []) }, [])
return ( return (
<React.Fragment> <>
<Block className={classes.title}> <Block className={classes.title}>
<Paragraph noMargin size="md" color="primary"> <Paragraph noMargin size="md" color="primary">
{`This Safe has ${owners.length} owners. Optional: Provide a name for each owner.`} {`This Safe has ${owners.length} owners. Optional: Provide a name for each owner.`}
@ -159,18 +159,18 @@ const OwnerListComponent = (props: Props) => {
</Row> </Row>
))} ))}
</Block> </Block>
</React.Fragment> </>
) )
} }
const OwnerListPage = withStyles(styles)(OwnerListComponent) const OwnerListPage = withStyles(styles)(OwnerListComponent)
const OwnerList = ({ updateInitialProps }: Object, network: string) => (controls: React$Node, { values }: Object) => ( const OwnerList = ({ updateInitialProps }: Object, network: string) => (controls: React$Node, { values }: Object) => (
<React.Fragment> <>
<OpenPaper controls={controls} padding={false}> <OpenPaper controls={controls} padding={false}>
<OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} /> <OwnerListPage network={network} updateInitialProps={updateInitialProps} values={values} />
</OpenPaper> </OpenPaper>
</React.Fragment> </>
) )
export default OwnerList export default OwnerList

View File

@ -51,7 +51,7 @@ const Layout = ({
const initialValues = initialValuesFrom(userAccount) const initialValues = initialValuesFrom(userAccount)
return ( return (
<React.Fragment> <>
{provider ? ( {provider ? (
<Block> <Block>
<Row align="center"> <Row align="center">
@ -75,7 +75,7 @@ const Layout = ({
) : ( ) : (
<div>No web3 provider detected</div> <div>No web3 provider detected</div>
)} )}
</React.Fragment> </>
) )
} }

View File

@ -7,6 +7,7 @@ import MenuItem from '@material-ui/core/MenuItem'
import Field from '~/components/forms/Field' import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField' import TextField from '~/components/forms/TextField'
import SelectField from '~/components/forms/SelectField' import SelectField from '~/components/forms/SelectField'
import AddressInput from '~/components/forms/AddressInput'
import { import {
required, composeValidators, noErrorsOn, mustBeInteger, minValue, required, composeValidators, noErrorsOn, mustBeInteger, minValue,
} from '~/components/forms/validator' } from '~/components/forms/validator'
@ -28,7 +29,7 @@ import Hairline from '~/components/layout/Hairline'
import trash from '~/assets/icons/trash.svg' import trash from '~/assets/icons/trash.svg'
import QRIcon from '~/assets/icons/qrcode.svg' import QRIcon from '~/assets/icons/qrcode.svg'
import ScanQRModal from './ScanQRModal' import ScanQRModal from './ScanQRModal'
import { getAddressValidators } from './validators' import { getAddressValidator } from './validators'
import { styles } from './style' import { styles } from './style'
type Props = { type Props = {
@ -105,7 +106,7 @@ const SafeOwners = (props: Props) => {
} }
return ( return (
<React.Fragment> <>
<Block className={classes.title}> <Block className={classes.title}>
<Paragraph noMargin size="md" color="primary"> <Paragraph noMargin size="md" color="primary">
Specify the owners of the Safe. Specify the owners of the Safe.
@ -135,7 +136,7 @@ const SafeOwners = (props: Props) => {
/> />
</Col> </Col>
<Col xs={6}> <Col xs={6}>
<Field <AddressInput
name={addressName} name={addressName}
component={TextField} component={TextField}
inputAdornment={ inputAdornment={
@ -147,8 +148,11 @@ const SafeOwners = (props: Props) => {
), ),
} }
} }
fieldMutator={(val) => {
form.mutators.setValue(addressName, val)
}}
type="text" type="text"
validate={getAddressValidators(otherAccounts, index)} validators={[getAddressValidator(otherAccounts, index)]}
placeholder="Owner Address*" placeholder="Owner Address*"
text="Owner Address" text="Owner Address"
/> />
@ -208,14 +212,14 @@ owner(s)
</Row> </Row>
</Block> </Block>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onScan={handleScan} onClose={closeQrModal} />} {qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onScan={handleScan} onClose={closeQrModal} />}
</React.Fragment> </>
) )
} }
const SafeOwnersForm = withStyles(styles)(SafeOwners) const SafeOwnersForm = withStyles(styles)(SafeOwners)
const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors, form }: Object) => ( const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node, { values, errors, form }: Object) => (
<React.Fragment> <>
<OpenPaper controls={controls} padding={false}> <OpenPaper controls={controls} padding={false}>
<SafeOwnersForm <SafeOwnersForm
otherAccounts={getAccountsFrom(values)} otherAccounts={getAccountsFrom(values)}
@ -225,7 +229,7 @@ const SafeOwnersPage = ({ updateInitialProps }: Object) => (controls: React.Node
values={values} values={values}
/> />
</OpenPaper> </OpenPaper>
</React.Fragment> </>
) )
export default SafeOwnersPage export default SafeOwnersPage

View File

@ -1,17 +1,14 @@
// @flow // @flow
import { import {
required,
composeValidators,
uniqueAddress, uniqueAddress,
mustBeEthereumAddress,
} from '~/components/forms/validator' } from '~/components/forms/validator'
export const getAddressValidators = (addresses: string[], position: number) => { export const getAddressValidator = (addresses: string[], position: number) => {
// thanks Rich Harris // thanks Rich Harris
// https://twitter.com/Rich_Harris/status/1125850391155965952 // https://twitter.com/Rich_Harris/status/1125850391155965952
const copy = addresses.slice() const copy = addresses.slice()
copy[position] = copy[copy.length - 1] copy[position] = copy[copy.length - 1]
copy.pop() copy.pop()
return composeValidators(required, mustBeEthereumAddress, uniqueAddress(copy)) return uniqueAddress(copy)
} }

View File

@ -9,6 +9,7 @@ import IconButton from '@material-ui/core/IconButton'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import GnoForm from '~/components/forms/GnoForm' import GnoForm from '~/components/forms/GnoForm'
import AddressInput from '~/components/forms/AddressInput'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
@ -18,12 +19,7 @@ import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField' import TextField from '~/components/forms/TextField'
import { type Token } from '~/logic/tokens/store/model/token' import { type Token } from '~/logic/tokens/store/model/token'
import { import {
composeValidators, composeValidators, required, mustBeFloat, maxValue, greaterThan,
required,
mustBeEthereumAddress,
mustBeFloat,
maxValue,
greaterThan,
} from '~/components/forms/validator' } from '~/components/forms/validator'
import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
@ -68,10 +64,13 @@ const SendFunds = ({
onTokenChange: (args, state, utils) => { onTokenChange: (args, state, utils) => {
utils.changeValue(state, 'amount', () => '') utils.changeValue(state, 'amount', () => '')
}, },
setRecipient: (args, state, utils) => {
utils.changeValue(state, 'recipientAddress', () => args[0])
},
} }
return ( return (
<React.Fragment> <>
<Row align="center" grow className={classes.heading}> <Row align="center" grow className={classes.heading}>
<Paragraph weight="bolder" className={classes.manage} noMargin> <Paragraph weight="bolder" className={classes.manage} noMargin>
Send Funds Send Funds
@ -99,17 +98,16 @@ const SendFunds = ({
const { token } = formState.values const { token } = formState.values
return ( return (
<React.Fragment> <>
<Row margin="md"> <Row margin="md">
<Col xs={12}> <Col xs={12}>
<Field <AddressInput
name="recipientAddress" name="recipientAddress"
component={TextField} component={TextField}
type="text"
validate={composeValidators(required, mustBeEthereumAddress)}
placeholder="Recipient*" placeholder="Recipient*"
text="Recipient*" text="Recipient*"
className={classes.addressInput} className={classes.addressInput}
fieldMutator={mutators.setRecipient}
/> />
</Col> </Col>
</Row> </Row>
@ -172,12 +170,12 @@ const SendFunds = ({
Review Review
</Button> </Button>
</Row> </Row>
</React.Fragment> </>
) )
}} }}
</GnoForm> </GnoForm>
</Block> </Block>
</React.Fragment> </>
) )
} }

View File

@ -6,6 +6,7 @@ import Close from '@material-ui/icons/Close'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import AddressInput from '~/components/forms/AddressInput'
import GnoForm from '~/components/forms/GnoForm' import GnoForm from '~/components/forms/GnoForm'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
@ -17,7 +18,6 @@ import { type Owner } from '~/routes/safe/store/models/owner'
import { import {
composeValidators, composeValidators,
required, required,
mustBeEthereumAddress,
minMaxLength, minMaxLength,
uniqueAddress, uniqueAddress,
} from '~/components/forms/validator' } from '~/components/forms/validator'
@ -27,6 +27,12 @@ export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input'
export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid' export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid'
export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn' export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn'
const formMutators = {
setOwnerAddress: (args, state, utils) => {
utils.changeValue(state, 'ownerAddress', () => args[0])
},
}
type Props = { type Props = {
onClose: () => void, onClose: () => void,
classes: Object, classes: Object,
@ -54,60 +60,63 @@ const OwnerForm = ({
</IconButton> </IconButton>
</Row> </Row>
<Hairline /> <Hairline />
<GnoForm onSubmit={handleSubmit}> <GnoForm onSubmit={handleSubmit} formMutators={formMutators}>
{() => ( {(...args) => {
<React.Fragment> const mutators = args[3]
<Block className={classes.formContainer}>
<Row margin="md"> return (
<Paragraph>Add a new owner to the active Safe</Paragraph> <React.Fragment>
<Block className={classes.formContainer}>
<Row margin="md">
<Paragraph>Add a new owner to the active Safe</Paragraph>
</Row>
<Row margin="md">
<Col xs={8}>
<Field
name="ownerName"
component={TextField}
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
placeholder="Owner name*"
text="Owner name*"
className={classes.addressInput}
testId={ADD_OWNER_NAME_INPUT_TEST_ID}
/>
</Col>
</Row>
<Row margin="md">
<Col xs={8}>
<AddressInput
name="ownerAddress"
validators={[ownerDoesntExist]}
placeholder="Owner address*"
text="Owner address*"
className={classes.addressInput}
fieldMutator={mutators.setOwnerAddress}
testId={ADD_OWNER_ADDRESS_INPUT_TEST_ID}
/>
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
className={classes.button}
variant="contained"
minWidth={140}
color="primary"
testId={ADD_OWNER_NEXT_BTN_TEST_ID}
>
Next
</Button>
</Row> </Row>
<Row margin="md"> </React.Fragment>
<Col xs={8}> )
<Field }}
name="ownerName"
component={TextField}
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
placeholder="Owner name*"
text="Owner name*"
className={classes.addressInput}
testId={ADD_OWNER_NAME_INPUT_TEST_ID}
/>
</Col>
</Row>
<Row margin="md">
<Col xs={8}>
<Field
name="ownerAddress"
component={TextField}
type="text"
validate={composeValidators(required, mustBeEthereumAddress, ownerDoesntExist)}
placeholder="Owner address*"
text="Owner address*"
className={classes.addressInput}
testId={ADD_OWNER_ADDRESS_INPUT_TEST_ID}
/>
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
className={classes.button}
variant="contained"
minWidth={140}
color="primary"
testId={ADD_OWNER_NEXT_BTN_TEST_ID}
>
Next
</Button>
</Row>
</React.Fragment>
)}
</GnoForm> </GnoForm>
</React.Fragment> </React.Fragment>
) )

View File

@ -9,6 +9,7 @@ import OpenInNew from '@material-ui/icons/OpenInNew'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import GnoForm from '~/components/forms/GnoForm' import GnoForm from '~/components/forms/GnoForm'
import AddressInput from '~/components/forms/AddressInput'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
@ -22,7 +23,6 @@ import { type Owner } from '~/routes/safe/store/models/owner'
import { import {
composeValidators, composeValidators,
required, required,
mustBeEthereumAddress,
minMaxLength, minMaxLength,
uniqueAddress, uniqueAddress,
} from '~/components/forms/validator' } from '~/components/forms/validator'
@ -38,6 +38,12 @@ const openIconStyle = {
color: secondary, color: secondary,
} }
const formMutators = {
setOwnerAddress: (args, state, utils) => {
utils.changeValue(state, 'ownerAddress', () => args[0])
},
}
type Props = { type Props = {
onClose: () => void, onClose: () => void,
classes: Object, classes: Object,
@ -68,89 +74,97 @@ const OwnerForm = ({
</IconButton> </IconButton>
</Row> </Row>
<Hairline /> <Hairline />
<GnoForm onSubmit={handleSubmit}> <GnoForm onSubmit={handleSubmit} formMutators={formMutators}>
{() => ( {(...args) => {
<React.Fragment> const mutators = args[3]
<Block className={classes.formContainer}>
<Row> return (
<Paragraph> <React.Fragment>
Review the owner you want to replace from the active Safe. Then specify the new owner you want to <Block className={classes.formContainer}>
replace it with: <Row>
</Paragraph> <Paragraph>
</Row> Review the owner you want to replace from the active Safe. Then specify the new owner you want to
<Row> replace it with:
<Paragraph>Current owner</Paragraph> </Paragraph>
</Row> </Row>
<Row className={classes.owner}> <Row>
<Col xs={1} align="center"> <Paragraph>Current owner</Paragraph>
<Identicon address={ownerAddress} diameter={32} /> </Row>
</Col> <Row className={classes.owner}>
<Col xs={7}> <Col xs={1} align="center">
<Block className={classNames(classes.name, classes.userName)}> <Identicon address={ownerAddress} diameter={32} />
<Paragraph size="lg" noMargin weight="bolder"> </Col>
{ownerName} <Col xs={7}>
</Paragraph> <Block className={classNames(classes.name, classes.userName)}>
<Block align="center" className={classes.user}> <Paragraph size="lg" noMargin weight="bolder">
<Paragraph size="md" color="disabled" noMargin> {ownerName}
{ownerAddress}
</Paragraph> </Paragraph>
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank"> <Block align="center" className={classes.user}>
<OpenInNew style={openIconStyle} /> <Paragraph size="md" color="disabled" noMargin>
</Link> {ownerAddress}
</Paragraph>
<Link
className={classes.open}
to={getEtherScanLink('address', ownerAddress, network)}
target="_blank"
>
<OpenInNew style={openIconStyle} />
</Link>
</Block>
</Block> </Block>
</Block> </Col>
</Col> </Row>
<Row>
<Paragraph>New owner</Paragraph>
</Row>
<Row margin="md">
<Col xs={8}>
<Field
name="ownerName"
component={TextField}
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
placeholder="Owner name*"
text="Owner name*"
className={classes.addressInput}
testId={REPLACE_OWNER_NAME_INPUT_TEST_ID}
/>
</Col>
</Row>
<Row margin="md">
<Col xs={8}>
<AddressInput
name="ownerAddress"
component={TextField}
validators={[ownerDoesntExist]}
placeholder="Owner address*"
text="Owner address*"
className={classes.addressInput}
fieldMutator={mutators.setOwnerAddress}
testId={REPLACE_OWNER_ADDRESS_INPUT_TEST_ID}
/>
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
className={classes.button}
variant="contained"
minWidth={140}
color="primary"
testId={REPLACE_OWNER_NEXT_BTN_TEST_ID}
>
Next
</Button>
</Row> </Row>
<Row> </React.Fragment>
<Paragraph>New owner</Paragraph> )
</Row> }}
<Row margin="md">
<Col xs={8}>
<Field
name="ownerName"
component={TextField}
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
placeholder="Owner name*"
text="Owner name*"
className={classes.addressInput}
testId={REPLACE_OWNER_NAME_INPUT_TEST_ID}
/>
</Col>
</Row>
<Row margin="md">
<Col xs={8}>
<Field
name="ownerAddress"
component={TextField}
type="text"
validate={composeValidators(required, mustBeEthereumAddress, ownerDoesntExist)}
placeholder="Owner address*"
text="Owner address*"
className={classes.addressInput}
testId={REPLACE_OWNER_ADDRESS_INPUT_TEST_ID}
/>
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button className={classes.button} minWidth={140} onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
className={classes.button}
variant="contained"
minWidth={140}
color="primary"
testId={REPLACE_OWNER_NEXT_BTN_TEST_ID}
>
Next
</Button>
</Row>
</React.Fragment>
)}
</GnoForm> </GnoForm>
</React.Fragment> </React.Fragment>
) )

View File

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import { type Store } from 'redux' import { type Store } from 'redux'
import { render, fireEvent, cleanup } from '@testing-library/react' import { render, fireEvent } from '@testing-library/react'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router' import { ConnectedRouter } from 'connected-react-router'
import { ADD_OWNER_BUTTON } from '~/routes/open/components/SafeOwnersConfirmationsForm' import { ADD_OWNER_BUTTON } from '~/routes/open/components/SafeOwnersConfirmationsForm'
@ -14,8 +14,6 @@ import { makeProvider } from '~/logic/wallets/store/model/provider'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { whenSafeDeployed } from './builder/safe.dom.utils' import { whenSafeDeployed } from './builder/safe.dom.utils'
afterEach(cleanup)
// https://github.com/testing-library/@testing-library/react/issues/281 // https://github.com/testing-library/@testing-library/react/issues/281
const originalError = console.error const originalError = console.error
beforeAll(() => { beforeAll(() => {

View File

@ -1,5 +1,5 @@
// @flow // @flow
import { fireEvent, cleanup } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { List } from 'immutable' import { List } from 'immutable'
import { aNewStore } from '~/store' import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { aMinedSafe } from '~/test/builder/safe.redux.builder'
@ -16,8 +16,6 @@ import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { checkRegisteredTxSend, fillAndSubmitSendFundsForm } from './utils/transactions' import { checkRegisteredTxSend, fillAndSubmitSendFundsForm } from './utils/transactions'
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances' import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
afterEach(cleanup)
describe('DOM > Feature > Sending Funds', () => { describe('DOM > Feature > Sending Funds', () => {
let store let store
let safeAddress: string let safeAddress: string

View File

@ -1,5 +1,5 @@
// @flow // @flow
import { fireEvent, cleanup } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { aNewStore } from '~/store' import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { sendEtherTo } from '~/test/utils/tokenMovements' import { sendEtherTo } from '~/test/utils/tokenMovements'
@ -15,7 +15,6 @@ import { useTestAccountAt, resetTestAccount } from './utils/accounts'
import { CONFIRM_TX_BTN_TEST_ID, EXECUTE_TX_BTN_TEST_ID } from '~/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/OwnersColumn/ButtonRow' import { CONFIRM_TX_BTN_TEST_ID, EXECUTE_TX_BTN_TEST_ID } from '~/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/OwnersColumn/ButtonRow'
import { APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/ApproveTxModal' import { APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/ApproveTxModal'
afterEach(cleanup)
afterEach(resetTestAccount) afterEach(resetTestAccount)
describe('DOM > Feature > Sending Funds', () => { describe('DOM > Feature > Sending Funds', () => {

View File

@ -2,7 +2,7 @@
import * as React from 'react' import * as React from 'react'
import { type Store } from 'redux' import { type Store } from 'redux'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { render, fireEvent, cleanup } from '@testing-library/react' import { render, fireEvent } from '@testing-library/react'
import { ConnectedRouter } from 'connected-react-router' import { ConnectedRouter } from 'connected-react-router'
import Load from '~/routes/load/container/Load' import Load from '~/routes/load/container/Load'
import { aNewStore, history, type GlobalState } from '~/store' import { aNewStore, history, type GlobalState } from '~/store'
@ -13,8 +13,6 @@ import { makeProvider } from '~/logic/wallets/store/model/provider'
import { aMinedSafe } from './builder/safe.redux.builder' import { aMinedSafe } from './builder/safe.redux.builder'
import { whenSafeDeployed } from './builder/safe.dom.utils' import { whenSafeDeployed } from './builder/safe.dom.utils'
afterEach(cleanup)
// https://github.com/testing-library/@testing-library/react/issues/281 // https://github.com/testing-library/@testing-library/react/issues/281
const originalError = console.error const originalError = console.error
beforeAll(() => { beforeAll(() => {

View File

@ -1,5 +1,5 @@
// @flow // @flow
import { fireEvent, cleanup } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { aNewStore } from '~/store' import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { renderSafeView } from '~/test/builder/safe.dom.utils' import { renderSafeView } from '~/test/builder/safe.dom.utils'
@ -8,8 +8,6 @@ import '@testing-library/jest-dom/extend-expect'
import { SETTINGS_TAB_BTN_TEST_ID, SAFE_VIEW_NAME_HEADING_TEST_ID } from '~/routes/safe/components/Layout' import { SETTINGS_TAB_BTN_TEST_ID, SAFE_VIEW_NAME_HEADING_TEST_ID } from '~/routes/safe/components/Layout'
import { SAFE_NAME_INPUT_TEST_ID, SAFE_NAME_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Settings/ChangeSafeName' import { SAFE_NAME_INPUT_TEST_ID, SAFE_NAME_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Settings/ChangeSafeName'
afterEach(cleanup)
describe('DOM > Feature > Settings - Name', () => { describe('DOM > Feature > Settings - Name', () => {
let store let store
let safeAddress let safeAddress

View File

@ -1,5 +1,5 @@
// @flow // @flow
import { fireEvent, cleanup } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { aNewStore } from '~/store' import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { renderSafeView } from '~/test/builder/safe.dom.utils' import { renderSafeView } from '~/test/builder/safe.dom.utils'
@ -40,8 +40,6 @@ import {
} from '~/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm' } from '~/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm'
import { REPLACE_OWNER_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review' import { REPLACE_OWNER_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
afterEach(cleanup)
describe('DOM > Feature > Settings - Manage owners', () => { describe('DOM > Feature > Settings - Manage owners', () => {
let store let store
let safeAddress let safeAddress

View File

@ -1,5 +1,4 @@
// @flow // @flow
export * from './moveFunds.helper' export * from './moveFunds.helper'
export * from './moveTokens.helper' export * from './moveTokens.helper'
export * from './threshold.helper'
export * from './transactionList.helper' export * from './transactionList.helper'

View File

@ -1,40 +0,0 @@
// @flow
import TestUtils from 'react-dom/test-utils'
import { sleep } from '~/utils/timer'
import { checkMinedTx } from '~/test/builder/safe.dom.utils'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import Threshold from '~/routes/safe/components/Threshold'
import { whenExecuted } from '~/test/utils/logTransactions'
export const sendChangeThresholdForm = async (
SafeDom: React.Component<any, any>,
changeThreshold: React.Component<any, any>,
threshold: string,
) => {
// Load the Threshold Form
TestUtils.Simulate.click(changeThreshold)
await sleep(400)
// fill the form
const inputs = TestUtils.scryRenderedDOMComponentsWithTag(SafeDom, 'input')
const thresholdInput = inputs[0]
TestUtils.Simulate.change(thresholdInput, { target: { value: threshold } })
// $FlowFixMe
const form = TestUtils.findRenderedDOMComponentWithTag(SafeDom, 'form')
// submit it
TestUtils.Simulate.submit(form)
TestUtils.Simulate.submit(form)
return whenExecuted(SafeDom, Threshold)
}
export const checkMinedThresholdTx = (Transaction: React.Component<any, any>, name: string) => {
checkMinedTx(Transaction, name)
}
export const checkThresholdOf = async (address: string, threshold: number) => {
const gnosisSafe = await getGnosisSafeInstanceAt(address)
const safeThreshold = await gnosisSafe.getThreshold()
expect(Number(safeThreshold)).toEqual(threshold)
}

1537
yarn.lock

File diff suppressed because it is too large Load Diff