(Fix) Prevent ENS check when not supported (#1570)

This commit is contained in:
Fernando 2020-11-10 16:16:44 -03:00 committed by GitHub
parent 2a01470d2d
commit 325864cffb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 381 additions and 340 deletions

View File

@ -72,7 +72,8 @@ export enum FEATURES {
ERC721 = 'ERC721', ERC721 = 'ERC721',
ERC1155 = 'ERC1155', ERC1155 = 'ERC1155',
SAFE_APPS = 'SAFE_APPS', SAFE_APPS = 'SAFE_APPS',
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION' CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
ENS_LOOKUP = 'ENS_LOOKUP'
} }
``` ```
@ -235,7 +236,7 @@ const rinkeby: NetworkConfig = {
address: '', address: '',
name: '', name: '',
symbol: '', symbol: '',
decimals: ?, decimals: 0,
logoUri: '', logoUri: '',
}, },
}, },

View File

@ -1,8 +1,10 @@
import { List } from 'immutable' import { List } from 'immutable'
import memoize from 'lodash.memoize'
import { isFeatureEnabled } from 'src/config'
import { FEATURES } from 'src/config/networks/network.d'
import { sameAddress } from 'src/logic/wallets/ethAddresses' import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { getWeb3 } from 'src/logic/wallets/getWeb3' import { getWeb3 } from 'src/logic/wallets/getWeb3'
import memoize from 'lodash.memoize'
type ValidatorReturnType = string | undefined type ValidatorReturnType = string | undefined
type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
@ -62,7 +64,11 @@ export const mustBeEthereumAddress = memoize(
const startsWith0x = address?.startsWith('0x') const startsWith0x = address?.startsWith('0x')
const isAddress = getWeb3().utils.isAddress(address) const isAddress = getWeb3().utils.isAddress(address)
return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name' const errorMessage = `Address should be a valid Ethereum address${
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
}`
return startsWith0x && isAddress ? undefined : errorMessage
}, },
) )
@ -70,9 +76,11 @@ export const mustBeEthereumContractAddress = memoize(
async (address: string): Promise<ValidatorReturnType> => { async (address: string): Promise<ValidatorReturnType> => {
const contractCode = await getWeb3().eth.getCode(address) const contractCode = await getWeb3().eth.getCode(address)
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' const errorMessage = `Address should be a valid Ethereum contract address${
? 'Address should be a valid Ethereum contract address or ENS name' isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
: undefined }`
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined
}, },
) )

View File

@ -1,7 +1,15 @@
import memoize from 'lodash.memoize' import memoize from 'lodash.memoize'
import networks from 'src/config/networks' import networks from 'src/config/networks'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkSettings, SafeFeatures, Wallets, GasPriceOracle } from 'src/config/networks/network.d' import {
EnvironmentSettings,
ETHEREUM_NETWORK,
FEATURES,
GasPriceOracle,
NetworkSettings,
SafeFeatures,
Wallets,
} from 'src/config/networks/network.d'
import { APP_ENV, ETHERSCAN_API_KEY, GOOGLE_ANALYTICS_ID, INFURA_TOKEN, NETWORK, NODE_ENV } from 'src/utils/constants' import { APP_ENV, ETHERSCAN_API_KEY, GOOGLE_ANALYTICS_ID, INFURA_TOKEN, NETWORK, NODE_ENV } from 'src/utils/constants'
import { ensureOnce } from 'src/utils/singleton' import { ensureOnce } from 'src/utils/singleton'
@ -90,6 +98,16 @@ export const getNetworkExplorerInfo = (): { name: string; url: string; apiUrl: s
export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig().disabledFeatures || [] export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig().disabledFeatures || []
/**
* Checks if a particular feature is enabled in the current network configuration
* @params {FEATURES} feature
* @returns boolean
*/
export const isFeatureEnabled = memoize((feature: FEATURES): boolean => {
const disabledFeatures = getNetworkConfigDisabledFeatures()
return !disabledFeatures.some((disabledFeature) => disabledFeature === feature)
})
export const getNetworkConfigDisabledWallets = (): Wallets => getConfig()?.disabledWallets || [] export const getNetworkConfigDisabledWallets = (): Wallets => getConfig()?.disabledWallets || []
export const getNetworkInfo = (): NetworkSettings => getConfig().network export const getNetworkInfo = (): NetworkSettings => getConfig().network

View File

@ -1,5 +1,5 @@
import EwcLogo from 'src/config/assets/token_ewc.svg' import EwcLogo from 'src/config/assets/token_ewc.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d' import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
// @todo (agustin) we need to use fixed gasPrice because the oracle is not working right now and it's returning 0 // @todo (agustin) we need to use fixed gasPrice because the oracle is not working right now and it's returning 0
// once the oracle is fixed we need to remove the fixed value // once the oracle is fixed we need to remove the fixed value
@ -44,7 +44,7 @@ const mainnet: NetworkConfig = {
logoUri: EwcLogo, logoUri: EwcLogo,
}, },
}, },
disabledWallets:[ disabledWallets: [
WALLETS.TREZOR, WALLETS.TREZOR,
WALLETS.LEDGER, WALLETS.LEDGER,
WALLETS.COINBASE, WALLETS.COINBASE,
@ -59,8 +59,11 @@ const mainnet: NetworkConfig = {
WALLETS.WALLET_CONNECT, WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK, WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM, WALLETS.AUTHEREUM,
WALLETS.LATTICE WALLETS.LATTICE,
] ],
disabledFeatures: [
FEATURES.ENS_LOOKUP,
],
} }
export default mainnet export default mainnet

View File

@ -23,7 +23,8 @@ export enum FEATURES {
ERC721 = 'ERC721', ERC721 = 'ERC721',
ERC1155 = 'ERC1155', ERC1155 = 'ERC1155',
SAFE_APPS = 'SAFE_APPS', SAFE_APPS = 'SAFE_APPS',
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION' CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
ENS_LOOKUP = 'ENS_LOOKUP',
} }
type Token = { type Token = {

View File

@ -1,5 +1,5 @@
import EwcLogo from 'src/config/assets/token_ewc.svg' import EwcLogo from 'src/config/assets/token_ewc.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, WALLETS, NetworkConfig } from 'src/config/networks/network.d' import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = { const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1', txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1',
@ -41,7 +41,7 @@ const mainnet: NetworkConfig = {
logoUri: EwcLogo, logoUri: EwcLogo,
}, },
}, },
disabledWallets:[ disabledWallets: [
WALLETS.TREZOR, WALLETS.TREZOR,
WALLETS.LEDGER, WALLETS.LEDGER,
WALLETS.COINBASE, WALLETS.COINBASE,
@ -56,8 +56,11 @@ const mainnet: NetworkConfig = {
WALLETS.WALLET_CONNECT, WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK, WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM, WALLETS.AUTHEREUM,
WALLETS.LATTICE WALLETS.LATTICE,
] ],
disabledFeatures: [
FEATURES.ENS_LOOKUP,
],
} }
export default mainnet export default mainnet

View File

@ -1,5 +1,5 @@
import { EnvironmentSettings, ETHEREUM_NETWORK, WALLETS, NetworkConfig } from 'src/config/networks/network.d'
import xDaiLogo from 'src/config/assets/token_xdai.svg' import xDaiLogo from 'src/config/assets/token_xdai.svg'
import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
const baseConfig: EnvironmentSettings = { const baseConfig: EnvironmentSettings = {
txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1', txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1',
@ -36,7 +36,7 @@ const xDai: NetworkConfig = {
logoUri: xDaiLogo, logoUri: xDaiLogo,
}, },
}, },
disabledWallets:[ disabledWallets: [
WALLETS.TREZOR, WALLETS.TREZOR,
WALLETS.LEDGER, WALLETS.LEDGER,
WALLETS.COINBASE, WALLETS.COINBASE,
@ -50,8 +50,11 @@ const xDai: NetworkConfig = {
WALLETS.WALLET_CONNECT, WALLETS.WALLET_CONNECT,
WALLETS.WALLET_LINK, WALLETS.WALLET_LINK,
WALLETS.AUTHEREUM, WALLETS.AUTHEREUM,
WALLETS.LATTICE WALLETS.LATTICE,
] ],
disabledFeatures: [
FEATURES.ENS_LOOKUP,
],
} }
export default xDai export default xDai

View File

@ -1,8 +1,9 @@
import { List } from 'immutable' import { List } from 'immutable'
import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { mustBeEthereumContractAddress } from 'src/components/forms/validator'
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { SafeOwner } from 'src/logic/safe/store/models/safe' import { SafeOwner } from 'src/logic/safe/store/models/safe'
import { sameAddress } from 'src/logic/wallets/ethAddresses' import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY' const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
@ -138,3 +139,39 @@ export const checkIfEntryWasDeletedFromAddressBook = (
const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address)) const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address))
return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook
} }
/**
* Returns a filtered list of AddressBookEntries whose addresses are contracts
* @param {Array<AddressBookEntry>} addressBook
* @returns Array<AddressBookEntry>
*/
export const filterContractAddressBookEntries = async (addressBook: AddressBookState): Promise<AddressBookEntry[]> => {
const abFlags = await Promise.all(
addressBook.map(
async ({ address }: AddressBookEntry): Promise<boolean> => {
return (await mustBeEthereumContractAddress(address)) === undefined
},
),
)
return addressBook.filter((_, index) => abFlags[index])
}
/**
* Filters the AddressBookEntries by `address` or `name` based on the `inputValue`
* @param {Array<AddressBookEntry>} addressBookEntries
* @param {Object} filterParams
* @param {String} filterParams.inputValue
* @return Array<AddressBookEntry>
*/
export const filterAddressEntries = (
addressBookEntries: AddressBookEntry[],
{ inputValue }: { inputValue: string },
): AddressBookEntry[] =>
addressBookEntries.filter(({ address, name }) => {
const inputLowerCase = inputValue.toLowerCase()
const foundName = name.toLowerCase().includes(inputLowerCase)
const foundAddress = address?.toLowerCase().includes(inputLowerCase)
return foundName || foundAddress
})

View File

@ -5,7 +5,7 @@ import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts'
import { LATEST_SAFE_VERSION } from 'src/utils/constants' import { LATEST_SAFE_VERSION } from 'src/utils/constants'
import { getNetworkConfigDisabledFeatures } from 'src/config' import { isFeatureEnabled } from 'src/config'
import { FEATURES } from 'src/config/networks/network.d' import { FEATURES } from 'src/config/networks/network.d'
type FeatureConfigByVersion = { type FeatureConfigByVersion = {
@ -41,9 +41,8 @@ const checkFeatureEnabledByVersion = (featureConfig: FeatureConfigByVersion, ver
} }
export const enabledFeatures = (version?: string): FEATURES[] => { export const enabledFeatures = (version?: string): FEATURES[] => {
const disabledFeatures = getNetworkConfigDisabledFeatures()
return FEATURES_BY_VERSION.reduce((acc: FEATURES[], feature: Feature) => { return FEATURES_BY_VERSION.reduce((acc: FEATURES[], feature: Feature) => {
if (!disabledFeatures.includes(feature.name) && version && checkFeatureEnabledByVersion(feature, version)) { if (isFeatureEnabled(feature.name) && version && checkFeatureEnabledByVersion(feature, version)) {
acc.push(feature.name) acc.push(feature.name)
} }
return acc return acc

View File

@ -1,246 +1,204 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import MuiTextField from '@material-ui/core/TextField' import MuiTextField from '@material-ui/core/TextField'
import makeStyles from '@material-ui/core/styles/makeStyles' import Autocomplete, { AutocompleteProps } from '@material-ui/lab/Autocomplete'
import Autocomplete from '@material-ui/lab/Autocomplete' import React, { Dispatch, ReactElement, SetStateAction, useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { trimSpaces } from 'src/utils/strings'
import { styles } from './style'
import Identicon from 'src/components/Identicon'
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator' import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
import { isFeatureEnabled } from 'src/config'
import { FEATURES } from 'src/config/networks/network.d'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getAddressFromENS } from 'src/logic/wallets/getWeb3' import { filterContractAddressBookEntries, filterAddressEntries } from 'src/logic/addressBook/utils'
import { isValidEnsName } from 'src/logic/wallets/ethAddresses' import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook' import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
import {
useTextFieldInputStyle,
useTextFieldLabelStyle,
} from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style'
import { trimSpaces } from 'src/utils/strings'
export interface AddressBookProps { export interface AddressBookProps {
fieldMutator: (address: string) => void fieldMutator: (address: string) => void
isCustomTx?: boolean pristine?: boolean
pristine: boolean
recipientAddress?: string recipientAddress?: string
setSelectedEntry: (
entry: { address?: string; name?: string } | React.SetStateAction<{ address?: string; name? }> | null,
) => void
setIsValidAddress: (valid: boolean) => void setIsValidAddress: (valid: boolean) => void
setSelectedEntry: Dispatch<SetStateAction<{ address: string; name: string }> | null>
} }
const useStyles = makeStyles(styles) export interface BaseAddressBookInputProps extends AddressBookProps {
addressBookEntries: AddressBookEntry[]
const textFieldLabelStyle = makeStyles(() => ({ setSelectedEntry: (args: { address: string; name: string } | null) => void
root: { setValidationText: Dispatch<SetStateAction<string | undefined>>
overflow: 'hidden', validationText: string | undefined
borderRadius: 4,
fontSize: '15px',
width: '500px',
},
}))
const textFieldInputStyle = makeStyles(() => ({
root: {
fontSize: '14px',
width: '420px',
},
}))
const filterAddressBookWithContractAddresses = async (addressBook: AddressBookState): Promise<AddressBookEntry[]> => {
const abFlags = await Promise.all(
addressBook.map(
async ({ address }: AddressBookEntry): Promise<boolean> => {
return (await mustBeEthereumContractAddress(address)) === undefined
},
),
)
return addressBook.filter((_, index) => abFlags[index])
} }
const AddressBookInput = ({ const BaseAddressBookInput = ({
addressBookEntries,
fieldMutator, fieldMutator,
isCustomTx,
pristine,
recipientAddress,
setIsValidAddress, setIsValidAddress,
setSelectedEntry, setSelectedEntry,
}: AddressBookProps): React.ReactElement => { setValidationText,
const classes = useStyles() validationText,
const addressBook = useSelector(addressBookSelector) }: BaseAddressBookInputProps): ReactElement => {
const [isValidForm, setIsValidForm] = useState(true) const updateAddressInfo = (addressEntry: AddressBookEntry): void => {
const [validationText, setValidationText] = useState<string>('') setSelectedEntry(addressEntry)
const [inputTouched, setInputTouched] = useState(false) fieldMutator(addressEntry.address)
const [blurred, setBlurred] = useState(pristine) }
const [adbkList, setADBKList] = useState<AddressBookEntry[]>([])
const [inputAddValue, setInputAddValue] = useState(recipientAddress) const validateAddress = (address: string): AddressBookEntry | string | undefined => {
const addressErrorMessage = mustBeEthereumAddress(address)
setIsValidAddress(!addressErrorMessage)
const onAddressInputChanged = async (value: string): Promise<void> => { if (addressErrorMessage) {
const normalizedAddress = trimSpaces(value) setValidationText(addressErrorMessage)
const isENSDomain = isValidEnsName(normalizedAddress)
setInputAddValue(normalizedAddress)
let resolvedAddress = normalizedAddress
let addressErrorMessage
if (inputTouched && !normalizedAddress) {
setIsValidForm(false)
setValidationText('Required')
setIsValidAddress(false)
return return
} }
if (normalizedAddress) {
if (isENSDomain) { const filteredEntries = filterAddressEntries(addressBookEntries, { inputValue: address })
resolvedAddress = await getAddressFromENS(normalizedAddress) return filteredEntries.length === 1 ? filteredEntries[0] : address
setInputAddValue(resolvedAddress) }
const onChange: AutocompleteProps<AddressBookEntry, false, false, true>['onChange'] = (_, value, reason) => {
switch (reason) {
case 'select-option': {
const { address, name } = value as AddressBookEntry
updateAddressInfo({ address, name })
break
} }
}
}
addressErrorMessage = mustBeEthereumAddress(resolvedAddress) const onInputChange: AutocompleteProps<AddressBookEntry, false, false, true>['onInputChange'] = async (
if (isCustomTx && addressErrorMessage === undefined) { _,
addressErrorMessage = await mustBeEthereumContractAddress(resolvedAddress) value,
} reason,
) => {
switch (reason) {
case 'input': {
const normalizedValue = trimSpaces(value)
// First removes the entries that are not contracts if the operation is custom tx if (!normalizedValue) {
const adbkToFilter = isCustomTx ? await filterAddressBookWithContractAddresses(addressBook) : addressBook break
// Then Filters the entries based on the input of the user
const filteredADBK = adbkToFilter.filter((adbkEntry) => {
const { address, name } = adbkEntry
return (
name.toLowerCase().includes(normalizedAddress.toLowerCase()) ||
address.toLowerCase().includes(resolvedAddress.toLowerCase())
)
})
setADBKList(filteredADBK)
if (!addressErrorMessage) {
// base case if isENSDomain we set the domain as the name
// if address does not exist in address book we use blank name
let addressName = isENSDomain ? normalizedAddress : ''
// if address is valid, and is in the address book, then we use the stored values
if (filteredADBK.length === 1) {
const addressBookContact = filteredADBK[0]
addressName = addressBookContact.name ?? addressName
} }
setSelectedEntry({ // ENS-enabled resolve/validation
name: addressName, if (isFeatureEnabled(FEATURES.ENS_LOOKUP) && isValidEnsName(normalizedValue)) {
address: resolvedAddress, const address = await getAddressFromENS(normalizedValue).catch(() => normalizedValue)
})
const validatedAddress = validateAddress(address)
if (!validatedAddress) {
fieldMutator('')
break
}
const newEntry = typeof validatedAddress === 'string' ? { address, name: normalizedValue } : validatedAddress
updateAddressInfo(newEntry)
break
}
// ETH address validation
const validatedAddress = validateAddress(normalizedValue)
if (!validatedAddress) {
fieldMutator('')
break
}
const newEntry =
typeof validatedAddress === 'string' ? { address: validatedAddress, name: '' } : validatedAddress
updateAddressInfo(newEntry)
break
} }
} }
setIsValidForm(addressErrorMessage === undefined)
setValidationText(addressErrorMessage)
fieldMutator(resolvedAddress)
setIsValidAddress(addressErrorMessage === undefined)
} }
const labelStyles = useTextFieldLabelStyle()
const inputStyles = useTextFieldInputStyle()
return (
<Autocomplete<AddressBookEntry, false, false, true>
closeIcon={null}
openOnFocus={false}
filterOptions={filterAddressEntries}
freeSolo
onChange={onChange}
onInputChange={onInputChange}
options={addressBookEntries}
renderInput={(params) => (
<MuiTextField
{...params}
autoFocus={true}
error={!!validationText}
fullWidth
id="filled-error-helper-text"
variant="filled"
label={validationText ? validationText : 'Recipient'}
InputLabelProps={{ shrink: true, required: true, classes: labelStyles }}
InputProps={{ ...params.InputProps, classes: inputStyles }}
/>
)}
getOptionLabel={({ address }) => address}
renderOption={({ address, name }) => <EthHashInfo hash={address} name={name} showIdenticon />}
role="listbox"
style={{ display: 'flex', flexGrow: 1 }}
/>
)
}
export const AddressBookInput = (props: AddressBookProps): ReactElement => {
const addressBookEntries = useSelector(addressBookSelector)
const [validationText, setValidationText] = useState<string>('')
return (
<BaseAddressBookInput
addressBookEntries={addressBookEntries}
setValidationText={setValidationText}
validationText={validationText}
{...props}
/>
)
}
export const ContractsAddressBookInput = ({
setIsValidAddress,
setSelectedEntry,
...props
}: AddressBookProps): ReactElement => {
const addressBookEntries = useSelector(addressBookSelector)
const [filteredEntries, setFilteredEntries] = useState<AddressBookEntry[]>([])
const [validationText, setValidationText] = useState<string>('')
useEffect(() => { useEffect(() => {
const filterAdbkContractAddresses = async (): Promise<void> => { const filterContractAddresses = async (): Promise<void> => {
if (!isCustomTx) { const filteredADBK = await filterContractAddressBookEntries(addressBookEntries)
setADBKList(addressBook) setFilteredEntries(filteredADBK)
return
}
const filteredADBK = await filterAddressBookWithContractAddresses(addressBook)
setADBKList(filteredADBK)
} }
filterAdbkContractAddresses() filterContractAddresses()
}, [addressBook, isCustomTx]) }, [addressBookEntries])
const labelStyling = textFieldLabelStyle() const onSetSelectedEntry = async (selectedEntry) => {
const txInputStyling = textFieldInputStyle() if (selectedEntry?.address) {
// verify if `address` is a contract
let statusClasses = '' const contractAddressErrorMessage = await mustBeEthereumContractAddress(selectedEntry.address)
if (!isValidForm) { setIsValidAddress(!contractAddressErrorMessage)
statusClasses = 'isInvalid' setValidationText(contractAddressErrorMessage ?? '')
} setSelectedEntry(selectedEntry)
if (isValidForm && inputTouched) { }
statusClasses = 'isValid'
} }
return ( return (
<> <BaseAddressBookInput
<Autocomplete addressBookEntries={filteredEntries}
closeIcon={null} setIsValidAddress={setIsValidAddress}
openOnFocus={false} setSelectedEntry={onSetSelectedEntry}
filterOptions={(optionsArray, { inputValue }) => setValidationText={setValidationText}
optionsArray.filter((item) => { validationText={validationText}
const inputLowerCase = inputValue.toLowerCase() {...props}
const foundName = item.name.toLowerCase().includes(inputLowerCase) />
const foundAddress = item.address?.toLowerCase().includes(inputLowerCase)
return foundName || foundAddress
})
}
freeSolo
getOptionLabel={(adbkEntry) => adbkEntry.address || ''}
id="free-solo-demo"
onChange={(_, value: AddressBookEntry) => {
let address = ''
let name = ''
if (value) {
address = value.address
name = value.name
}
setSelectedEntry({ address, name })
fieldMutator(address)
}}
onClose={() => setBlurred(true)}
onOpen={() => {
setSelectedEntry(null)
setBlurred(false)
}}
open={!blurred}
options={adbkList}
renderInput={(params) => (
<MuiTextField
{...params}
// eslint-disable-next-line
autoFocus={!blurred || pristine}
error={!isValidForm}
fullWidth
id="filled-error-helper-text"
InputLabelProps={{
shrink: true,
required: true,
classes: labelStyling,
}}
InputProps={{
...params.InputProps,
classes: {
...txInputStyling,
},
className: statusClasses,
}}
label={!isValidForm ? validationText : 'Recipient'}
onChange={(event) => {
setInputTouched(true)
onAddressInputChanged(event.target.value)
}}
value={{ address: inputAddValue }}
variant="filled"
/>
)}
renderOption={(adbkEntry) => {
const { address, name } = adbkEntry
if (!address) {
return
}
return (
<div className={classes.itemOptionList}>
<div className={classes.identicon}>
<Identicon address={address} diameter={32} />
</div>
<div className={classes.adbkEntryName}>
<span>{name}</span>
<span>{address}</span>
</div>
</div>
)
}}
role="listbox"
style={{ display: 'flex', flexGrow: 1 }}
value={{ address: inputAddValue, name: '' }}
/>
</>
) )
} }
export default AddressBookInput

View File

@ -1,24 +1,21 @@
import { createStyles } from '@material-ui/core' import { createStyles, makeStyles } from '@material-ui/core'
export const styles = createStyles({ export const useTextFieldLabelStyle = makeStyles(
itemOptionList: { createStyles({
display: 'flex', root: {
}, overflow: 'hidden',
borderRadius: 4,
fontSize: '15px',
width: '500px',
},
}),
)
adbkEntryName: { export const useTextFieldInputStyle = makeStyles(
display: 'flex', createStyles({
flexDirection: 'column', root: {
fontSize: '14px', fontSize: '14px',
}, width: '420px',
identicon: { },
display: 'flex', }),
padding: '5px', )
flexDirection: 'column',
justifyContent: 'center',
},
root: {
fontSize: '14px',
backgroundColor: 'red',
},
})

View File

@ -3,7 +3,7 @@ import React, { useState } from 'react'
import { useFormState, useField } from 'react-final-form' import { useFormState, useField } from 'react-final-form'
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import { ContractsAddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import Field from 'src/components/forms/Field' import Field from 'src/components/forms/Field'
import TextField from 'src/components/forms/TextField' import TextField from 'src/components/forms/TextField'
import { import {
@ -82,11 +82,10 @@ const EthAddressInput = ({
validate={validate} validate={validate}
/> />
) : ( ) : (
<AddressBookInput <ContractsAddressBookInput
setSelectedEntry={setSelectedEntry} setSelectedEntry={setSelectedEntry}
setIsValidAddress={() => {}} setIsValidAddress={() => {}}
fieldMutator={onScannedValue} fieldMutator={onScannedValue}
isCustomTx
pristine={pristine} pristine={pristine}
/> />
)} )}

View File

@ -25,7 +25,7 @@ import Row from 'src/components/layout/Row'
import ScanQRModal from 'src/components/ScanQRModal' import ScanQRModal from 'src/components/ScanQRModal'
import { safeSelector } from 'src/logic/safe/store/selectors' import { safeSelector } from 'src/logic/safe/store/selectors'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import { ContractsAddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import { sm } from 'src/theme/variables' import { sm } from 'src/theme/variables'
import ArrowDown from '../../assets/arrow-down.svg' import ArrowDown from '../../assets/arrow-down.svg'
@ -147,9 +147,13 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
{selectedEntry && selectedEntry.address ? ( {selectedEntry && selectedEntry.address ? (
<div <div
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.keyCode !== 9) { if (e.key === 'Tab') {
setSelectedEntry(null) return
} }
setSelectedEntry(null)
}}
onClick={() => {
setSelectedEntry(null)
}} }}
role="listbox" role="listbox"
tabIndex={0} tabIndex={0}
@ -193,9 +197,8 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
<> <>
<Row margin="md"> <Row margin="md">
<Col xs={11}> <Col xs={11}>
<AddressBookInput <ContractsAddressBookInput
fieldMutator={mutators.setRecipient} fieldMutator={mutators.setRecipient}
isCustomTx
pristine={pristine} pristine={pristine}
setIsValidAddress={setIsValidAddress} setIsValidAddress={setIsValidAddress}
setSelectedEntry={setSelectedEntry} setSelectedEntry={setSelectedEntry}

View File

@ -1,3 +1,4 @@
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
@ -19,17 +20,17 @@ import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils' import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors' import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import { CollectibleSelectField } from 'src/routes/safe/components/Balances/SendModal/screens/SendCollectible/CollectibleSelectField' import { NFTToken } from 'src/logic/collectibles/sources/collectibles'
import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendCollectible/TokenSelectField' import { getExplorerInfo } from 'src/config'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { sm } from 'src/theme/variables' import { sm } from 'src/theme/variables'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from 'src/routes/safe/components/Balances/SendModal/screens/assets/arrow-down.svg'
import { CollectibleSelectField } from './CollectibleSelectField'
import { styles } from './style' import { styles } from './style'
import { NFTToken } from 'src/logic/collectibles/sources/collectibles' import TokenSelectField from './TokenSelectField'
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
import { getExplorerInfo } from 'src/config'
const formMutators = { const formMutators = {
setMax: (args, state, utils) => { setMax: (args, state, utils) => {
@ -71,9 +72,27 @@ const SendCollectible = ({
const nftAssets = useSelector(safeActiveSelectorMap) const nftAssets = useSelector(safeActiveSelectorMap)
const nftTokens = useSelector(nftTokensSelector) const nftTokens = useSelector(nftTokensSelector)
const addressBook = useSelector(addressBookSelector) const addressBook = useSelector(addressBookSelector)
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => {
address: recipientAddress || initialValues.recipientAddress, const defaultEntry = { address: '', name: '' }
name: '',
// if there's nothing to lookup for, we return the default entry
if (!initialValues?.recipientAddress && !recipientAddress) {
return defaultEntry
}
// if there's something to lookup for, `initialValues` has precedence over `recipientAddress`
const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress
const addressBookEntry = addressBook.find(({ address }) => {
return sameAddress(predefinedAddress, address)
})
// if found in the Address Book, then we return the entry
if (addressBookEntry) {
return addressBookEntry
}
// otherwise we return the default entry
return defaultEntry
}) })
const [pristine, setPristine] = useState(true) const [pristine, setPristine] = useState(true)
const [isValidAddress, setIsValidAddress] = useState(false) const [isValidAddress, setIsValidAddress] = useState(false)
@ -123,7 +142,7 @@ const SendCollectible = ({
const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : '' const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress) mutators.setRecipient(scannedAddress)
setSelectedEntry({ setSelectedEntry({
name: scannedName, name: scannedName ?? '',
address: scannedAddress, address: scannedAddress,
}) })
closeQrModal() closeQrModal()
@ -151,9 +170,13 @@ const SendCollectible = ({
{selectedEntry && selectedEntry.address ? ( {selectedEntry && selectedEntry.address ? (
<div <div
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.keyCode !== 9) { if (e.key === 'Tab') {
setSelectedEntry({ address: '', name: 'string' }) return
} }
setSelectedEntry({ address: '', name: '' })
}}
onClick={() => {
setSelectedEntry({ address: '', name: '' })
}} }}
role="listbox" role="listbox"
tabIndex={0} tabIndex={0}
@ -200,7 +223,6 @@ const SendCollectible = ({
<AddressBookInput <AddressBookInput
fieldMutator={mutators.setRecipient} fieldMutator={mutators.setRecipient}
pristine={pristine} pristine={pristine}
recipientAddress={recipientAddress}
setIsValidAddress={setIsValidAddress} setIsValidAddress={setIsValidAddress}
setSelectedEntry={setSelectedEntry} setSelectedEntry={setSelectedEntry}
/> />

View File

@ -3,16 +3,14 @@ import InputAdornment from '@material-ui/core/InputAdornment'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import { getExplorerInfo, getNetworkInfo } from 'src/config' import { getExplorerInfo, getNetworkInfo } from 'src/config'
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { OnChange } from 'react-final-form-listeners' import { OnChange } from 'react-final-form-listeners'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import CopyBtn from 'src/components/CopyBtn'
import Field from 'src/components/forms/Field' import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm' import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField' import TextField from 'src/components/forms/TextField'
import { composeValidators, maxValue, minValue, mustBeFloat, required } from 'src/components/forms/validator' import { composeValidators, maxValue, minValue, mustBeFloat, required } from 'src/components/forms/validator'
import Identicon from 'src/components/Identicon'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button' import Button from 'src/components/layout/Button'
import ButtonLink from 'src/components/layout/ButtonLink' import ButtonLink from 'src/components/layout/ButtonLink'
@ -23,9 +21,10 @@ import Row from 'src/components/layout/Row'
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils' import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField'
import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
import { sm } from 'src/theme/variables' import { sm } from 'src/theme/variables'
@ -33,7 +32,7 @@ import { sm } from 'src/theme/variables'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style' import { styles } from './style'
import { ExplorerButton } from '@gnosis.pm/safe-react-components' import { EthHashInfo } from '@gnosis.pm/safe-react-components'
const formMutators = { const formMutators = {
setMax: (args, state, utils) => { setMax: (args, state, utils) => {
@ -75,15 +74,32 @@ const SendFunds = ({
const classes = useStyles() const classes = useStyles()
const tokens = useSelector(extendedSafeTokensSelector) const tokens = useSelector(extendedSafeTokensSelector)
const addressBook = useSelector(addressBookSelector) const addressBook = useSelector(addressBookSelector)
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => {
address: recipientAddress || initialValues.recipientAddress, const defaultEntry = { address: '', name: '' }
name: '',
})
// if there's nothing to lookup for, we return the default entry
if (!initialValues?.recipientAddress && !recipientAddress) {
return defaultEntry
}
// if there's something to lookup for, `initialValues` has precedence over `recipientAddress`
const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress
const addressBookEntry = addressBook.find(({ address }) => {
return sameAddress(predefinedAddress, address)
})
// if found in the Address Book, then we return the entry
if (addressBookEntry) {
return addressBookEntry
}
// otherwise we return the default entry
return defaultEntry
})
const [pristine, setPristine] = useState(true) const [pristine, setPristine] = useState(true)
const [isValidAddress, setIsValidAddress] = useState(false) const [isValidAddress, setIsValidAddress] = useState(false)
React.useMemo(() => { useEffect(() => {
if (selectedEntry === null && pristine) { if (selectedEntry === null && pristine) {
setPristine(false) setPristine(false)
} }
@ -152,9 +168,13 @@ const SendFunds = ({
{selectedEntry && selectedEntry.address ? ( {selectedEntry && selectedEntry.address ? (
<div <div
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.keyCode !== 9) { if (e.key === 'Tab') {
setSelectedEntry({ address: '', name: 'string' }) return
} }
setSelectedEntry({ address: '', name: '' })
}}
onClick={() => {
setSelectedEntry({ address: '', name: '' })
}} }}
role="listbox" role="listbox"
tabIndex={0} tabIndex={0}
@ -165,52 +185,29 @@ const SendFunds = ({
</Paragraph> </Paragraph>
</Row> </Row>
<Row align="center" margin="md"> <Row align="center" margin="md">
<Col xs={1}> <EthHashInfo
<Identicon address={selectedEntry.address} diameter={32} /> hash={selectedEntry.address}
</Col> name={selectedEntry.name}
<Col layout="column" xs={11}> showIdenticon
<Block justify="left"> showCopyBtn
<Block> explorerUrl={getExplorerInfo(selectedEntry.address)}
<Paragraph />
className={classes.selectAddress}
noMargin
onClick={() => setSelectedEntry({ address: '', name: 'string' })}
weight="bolder"
>
{selectedEntry.name}
</Paragraph>
<Paragraph
className={classes.selectAddress}
noMargin
onClick={() => setSelectedEntry({ address: '', name: 'string' })}
weight="bolder"
>
{selectedEntry.address}
</Paragraph>
</Block>
<CopyBtn content={selectedEntry.address} />
<ExplorerButton explorerUrl={getExplorerInfo(selectedEntry.address)} />
</Block>
</Col>
</Row> </Row>
</div> </div>
) : ( ) : (
<> <Row margin="md">
<Row margin="md"> <Col xs={11}>
<Col xs={11}> <AddressBookInput
<AddressBookInput fieldMutator={mutators.setRecipient}
fieldMutator={mutators.setRecipient} pristine={pristine}
pristine={pristine} setIsValidAddress={setIsValidAddress}
recipientAddress={recipientAddress} setSelectedEntry={setSelectedEntry}
setIsValidAddress={setIsValidAddress} />
setSelectedEntry={setSelectedEntry} </Col>
/> <Col center="xs" className={classes} middle="xs" xs={1}>
</Col> <ScanQRWrapper handleScan={handleScan} />
<Col center="xs" className={classes} middle="xs" xs={1}> </Col>
<ScanQRWrapper handleScan={handleScan} /> </Row>
</Col>
</Row>
</>
)} )}
<Row margin="sm"> <Row margin="sm">
<Col> <Col>
@ -256,15 +253,7 @@ const SendFunds = ({
maxValue(selectedTokenRecord?.balance || 0), maxValue(selectedTokenRecord?.balance || 0),
)} )}
/> />
<OnChange name="token"> <OnChange name="token">{() => mutators.onTokenChange()}</OnChange>
{() => {
setSelectedEntry({
name: selectedEntry?.name,
address: selectedEntry?.address,
})
mutators.onTokenChange()
}}
</OnChange>
</Col> </Col>
</Row> </Row>
</Block> </Block>