(Fix) Prevent ENS check when not supported (#1570)
This commit is contained in:
parent
2a01470d2d
commit
325864cffb
|
@ -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: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 onAddressInputChanged = async (value: string): Promise<void> => {
|
|
||||||
const normalizedAddress = trimSpaces(value)
|
|
||||||
const isENSDomain = isValidEnsName(normalizedAddress)
|
|
||||||
setInputAddValue(normalizedAddress)
|
|
||||||
let resolvedAddress = normalizedAddress
|
|
||||||
let addressErrorMessage
|
|
||||||
if (inputTouched && !normalizedAddress) {
|
|
||||||
setIsValidForm(false)
|
|
||||||
setValidationText('Required')
|
|
||||||
setIsValidAddress(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (normalizedAddress) {
|
|
||||||
if (isENSDomain) {
|
|
||||||
resolvedAddress = await getAddressFromENS(normalizedAddress)
|
|
||||||
setInputAddValue(resolvedAddress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addressErrorMessage = mustBeEthereumAddress(resolvedAddress)
|
const validateAddress = (address: string): AddressBookEntry | string | undefined => {
|
||||||
if (isCustomTx && addressErrorMessage === undefined) {
|
const addressErrorMessage = mustBeEthereumAddress(address)
|
||||||
addressErrorMessage = await mustBeEthereumContractAddress(resolvedAddress)
|
setIsValidAddress(!addressErrorMessage)
|
||||||
}
|
|
||||||
|
|
||||||
// First removes the entries that are not contracts if the operation is custom tx
|
if (addressErrorMessage) {
|
||||||
const adbkToFilter = isCustomTx ? await filterAddressBookWithContractAddresses(addressBook) : addressBook
|
|
||||||
// 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({
|
|
||||||
name: addressName,
|
|
||||||
address: resolvedAddress,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsValidForm(addressErrorMessage === undefined)
|
|
||||||
setValidationText(addressErrorMessage)
|
setValidationText(addressErrorMessage)
|
||||||
fieldMutator(resolvedAddress)
|
|
||||||
setIsValidAddress(addressErrorMessage === undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const filterAdbkContractAddresses = async (): Promise<void> => {
|
|
||||||
if (!isCustomTx) {
|
|
||||||
setADBKList(addressBook)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredADBK = await filterAddressBookWithContractAddresses(addressBook)
|
const filteredEntries = filterAddressEntries(addressBookEntries, { inputValue: address })
|
||||||
setADBKList(filteredADBK)
|
return filteredEntries.length === 1 ? filteredEntries[0] : address
|
||||||
}
|
}
|
||||||
filterAdbkContractAddresses()
|
|
||||||
}, [addressBook, isCustomTx])
|
|
||||||
|
|
||||||
const labelStyling = textFieldLabelStyle()
|
const onChange: AutocompleteProps<AddressBookEntry, false, false, true>['onChange'] = (_, value, reason) => {
|
||||||
const txInputStyling = textFieldInputStyle()
|
switch (reason) {
|
||||||
|
case 'select-option': {
|
||||||
|
const { address, name } = value as AddressBookEntry
|
||||||
|
updateAddressInfo({ address, name })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let statusClasses = ''
|
const onInputChange: AutocompleteProps<AddressBookEntry, false, false, true>['onInputChange'] = async (
|
||||||
if (!isValidForm) {
|
_,
|
||||||
statusClasses = 'isInvalid'
|
value,
|
||||||
|
reason,
|
||||||
|
) => {
|
||||||
|
switch (reason) {
|
||||||
|
case 'input': {
|
||||||
|
const normalizedValue = trimSpaces(value)
|
||||||
|
|
||||||
|
if (!normalizedValue) {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if (isValidForm && inputTouched) {
|
|
||||||
statusClasses = 'isValid'
|
// ENS-enabled resolve/validation
|
||||||
|
if (isFeatureEnabled(FEATURES.ENS_LOOKUP) && isValidEnsName(normalizedValue)) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelStyles = useTextFieldLabelStyle()
|
||||||
|
const inputStyles = useTextFieldInputStyle()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Autocomplete<AddressBookEntry, false, false, true>
|
||||||
<Autocomplete
|
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
openOnFocus={false}
|
openOnFocus={false}
|
||||||
filterOptions={(optionsArray, { inputValue }) =>
|
filterOptions={filterAddressEntries}
|
||||||
optionsArray.filter((item) => {
|
|
||||||
const inputLowerCase = inputValue.toLowerCase()
|
|
||||||
const foundName = item.name.toLowerCase().includes(inputLowerCase)
|
|
||||||
const foundAddress = item.address?.toLowerCase().includes(inputLowerCase)
|
|
||||||
return foundName || foundAddress
|
|
||||||
})
|
|
||||||
}
|
|
||||||
freeSolo
|
freeSolo
|
||||||
getOptionLabel={(adbkEntry) => adbkEntry.address || ''}
|
onChange={onChange}
|
||||||
id="free-solo-demo"
|
onInputChange={onInputChange}
|
||||||
onChange={(_, value: AddressBookEntry) => {
|
options={addressBookEntries}
|
||||||
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) => (
|
renderInput={(params) => (
|
||||||
<MuiTextField
|
<MuiTextField
|
||||||
{...params}
|
{...params}
|
||||||
// eslint-disable-next-line
|
autoFocus={true}
|
||||||
autoFocus={!blurred || pristine}
|
error={!!validationText}
|
||||||
error={!isValidForm}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
id="filled-error-helper-text"
|
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"
|
variant="filled"
|
||||||
|
label={validationText ? validationText : 'Recipient'}
|
||||||
|
InputLabelProps={{ shrink: true, required: true, classes: labelStyles }}
|
||||||
|
InputProps={{ ...params.InputProps, classes: inputStyles }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderOption={(adbkEntry) => {
|
getOptionLabel={({ address }) => address}
|
||||||
const { address, name } = adbkEntry
|
renderOption={({ address, name }) => <EthHashInfo hash={address} name={name} showIdenticon />}
|
||||||
|
role="listbox"
|
||||||
|
style={{ display: 'flex', flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!address) {
|
export const AddressBookInput = (props: AddressBookProps): ReactElement => {
|
||||||
return
|
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(() => {
|
||||||
|
const filterContractAddresses = async (): Promise<void> => {
|
||||||
|
const filteredADBK = await filterContractAddressBookEntries(addressBookEntries)
|
||||||
|
setFilteredEntries(filteredADBK)
|
||||||
|
}
|
||||||
|
filterContractAddresses()
|
||||||
|
}, [addressBookEntries])
|
||||||
|
|
||||||
|
const onSetSelectedEntry = async (selectedEntry) => {
|
||||||
|
if (selectedEntry?.address) {
|
||||||
|
// verify if `address` is a contract
|
||||||
|
const contractAddressErrorMessage = await mustBeEthereumContractAddress(selectedEntry.address)
|
||||||
|
setIsValidAddress(!contractAddressErrorMessage)
|
||||||
|
setValidationText(contractAddressErrorMessage ?? '')
|
||||||
|
setSelectedEntry(selectedEntry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.itemOptionList}>
|
<BaseAddressBookInput
|
||||||
<div className={classes.identicon}>
|
addressBookEntries={filteredEntries}
|
||||||
<Identicon address={address} diameter={32} />
|
setIsValidAddress={setIsValidAddress}
|
||||||
</div>
|
setSelectedEntry={onSetSelectedEntry}
|
||||||
<div className={classes.adbkEntryName}>
|
setValidationText={setValidationText}
|
||||||
<span>{name}</span>
|
validationText={validationText}
|
||||||
<span>{address}</span>
|
{...props}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
role="listbox"
|
|
||||||
style={{ display: 'flex', flexGrow: 1 }}
|
|
||||||
value={{ address: inputAddValue, name: '' }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddressBookInput
|
|
||||||
|
|
|
@ -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,
|
||||||
adbkEntryName: {
|
fontSize: '15px',
|
||||||
display: 'flex',
|
width: '500px',
|
||||||
flexDirection: 'column',
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
identicon: {
|
|
||||||
display: 'flex',
|
|
||||||
padding: '5px',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const useTextFieldInputStyle = makeStyles(
|
||||||
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
backgroundColor: 'red',
|
width: '420px',
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,43 +185,21 @@ 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}
|
||||||
recipientAddress={recipientAddress}
|
|
||||||
setIsValidAddress={setIsValidAddress}
|
setIsValidAddress={setIsValidAddress}
|
||||||
setSelectedEntry={setSelectedEntry}
|
setSelectedEntry={setSelectedEntry}
|
||||||
/>
|
/>
|
||||||
|
@ -210,7 +208,6 @@ const SendFunds = ({
|
||||||
<ScanQRWrapper handleScan={handleScan} />
|
<ScanQRWrapper handleScan={handleScan} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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>
|
||||||
|
|
Loading…
Reference in New Issue