Merge pull request #146 from gnosis/125-ens
Feature #125: ENS Integration
This commit is contained in:
commit
f4f4787d23
|
@ -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',
|
||||||
|
|
61
package.json
61
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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`)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
Loading…
Reference in New Issue