diff --git a/.eslintrc.js b/.eslintrc.js index 764f17bc..a4bfe751 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,7 +22,6 @@ module.exports = { '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], }, settings: { diff --git a/.prettierignore b/.prettierignore index c2ffcb25..0b56bd82 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,6 +10,4 @@ public scripts src/assets src/config -test -*.spec* -*.test* \ No newline at end of file +test \ No newline at end of file diff --git a/package.json b/package.json index f29a3d7a..9a833e47 100644 --- a/package.json +++ b/package.json @@ -220,6 +220,10 @@ "web3": "1.2.11" }, "devDependencies": { + "@types/history": "4.6.2", + "@types/lodash.memoize": "^4.1.6", + "@types/react-router-dom": "^5.1.5", + "@types/react-redux": "^7.1.9", "@testing-library/jest-dom": "5.11.1", "@testing-library/react": "10.4.7", "@testing-library/user-event": "12.0.13", diff --git a/src/components/forms/validator.test.ts b/src/components/forms/validator.test.ts new file mode 100644 index 00000000..0eff1983 --- /dev/null +++ b/src/components/forms/validator.test.ts @@ -0,0 +1,193 @@ +import { + required, + mustBeInteger, + mustBeFloat, + maxValue, + mustBeUrl, + minValue, + mustBeEthereumAddress, + minMaxLength, + uniqueAddress, + differentFrom, + ADDRESS_REPEATED_ERROR, +} from 'src/components/forms/validator' + +describe('Forms > Validators', () => { + describe('Required validator', () => { + const REQUIRED_ERROR_MSG = 'Required' + + it('Returns undefined for a non-empty string', () => { + expect(required('Im not empty')).toBeUndefined() + }) + + it('Returns an error message for an empty string', () => { + expect(required('')).toEqual(REQUIRED_ERROR_MSG) + }) + + it('Returns an error message for a string containing only spaces', () => { + expect(required(' ')).toEqual(REQUIRED_ERROR_MSG) + }) + }) + + describe('mustBeInteger validator', () => { + const MUST_BE_INTEGER_ERROR_MSG = 'Must be an integer' + + it('Returns undefined for an integer number string', () => { + expect(mustBeInteger('10')).toBeUndefined() + }) + + it('Returns an error message for a float number', () => { + expect(mustBeInteger('1.0')).toEqual(MUST_BE_INTEGER_ERROR_MSG) + }) + + it('Returns an error message for a non-number string', () => { + expect(mustBeInteger('iamnotanumber')).toEqual(MUST_BE_INTEGER_ERROR_MSG) + }) + }) + + describe('mustBeFloat validator', () => { + const MUST_BE_FLOAT_ERR_MSG = 'Must be a number' + + it('Returns undefined for a float number string', () => { + expect(mustBeFloat('1.0')).toBeUndefined() + }) + + it('Returns an error message for a non-number string', () => { + expect(mustBeFloat('iamnotanumber')).toEqual(MUST_BE_FLOAT_ERR_MSG) + }) + }) + + describe('minValue validator', () => { + const getMinValueErrMsg = (minValue: number, inclusive = true): string => + `Should be greater than ${inclusive ? 'or equal to ' : ''}${minValue}` + + it('Returns undefined for a number greater than minimum', () => { + const minimum = Math.random() + const number = (minimum + 1).toString() + + expect(minValue(minimum)(number)).toBeUndefined() + }) + + it('Returns an error message for a number less than minimum', () => { + const minimum = Math.random() + const number = (minimum - 1).toString() + + expect(minValue(minimum)(number)).toEqual(getMinValueErrMsg(minimum)) + }) + + it('Returns an error message for a number equal to minimum with false inclusive param', () => { + const minimum = Math.random() + const number = (minimum - 1).toString() + + expect(minValue(minimum, false)(number)).toEqual(getMinValueErrMsg(minimum, false)) + }) + + it('Returns an error message for a non-number string', () => { + expect(minValue(1)('imnotanumber')).toEqual(getMinValueErrMsg(1)) + }) + }) + + describe('mustBeUrl validator', () => { + const MUST_BE_URL_ERR_MSG = 'Please, provide a valid url' + + it('Returns undefined for a valid url', () => { + expect(mustBeUrl('https://gnosis-safe.io')).toBeUndefined() + }) + + it('Returns an error message for an valid url', () => { + expect(mustBeUrl('gnosis-safe')).toEqual(MUST_BE_URL_ERR_MSG) + }) + }) + + describe('maxValue validator', () => { + const getMaxValueErrMsg = (maxValue: number): string => `Maximum value is ${maxValue}` + + it('Returns undefined for a number less than maximum', () => { + const max = Math.random() + const number = (max - 1).toString() + + expect(maxValue(max)(number)).toBeUndefined() + }) + + it('Returns undefined for a number equal to maximum', () => { + const max = Math.random() + + expect(maxValue(max)(max.toString())).toBeUndefined() + }) + + it('Returns an error message for a number greater than maximum', () => { + const max = Math.random() + const number = (max + 1).toString() + + expect(maxValue(max)(number)).toEqual(getMaxValueErrMsg(max)) + }) + + it('Returns an error message for a non-number string', () => { + expect(maxValue(1)('imnotanumber')).toEqual(getMaxValueErrMsg(1)) + }) + }) + + describe('mustBeEthereumAddress validator', () => { + const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Address should be a valid Ethereum address or ENS name' + + it('Returns undefined for a valid ethereum address', async () => { + expect(await mustBeEthereumAddress('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toBeUndefined() + }) + + it('Returns an error message for an address with an invalid checksum', async () => { + expect(await mustBeEthereumAddress('0xde0b295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toEqual( + MUST_BE_ETH_ADDRESS_ERR_MSG, + ) + }) + + it('Returns an error message for non-address string', async () => { + expect(await mustBeEthereumAddress('notanaddress')).toEqual(MUST_BE_ETH_ADDRESS_ERR_MSG) + }) + }) + + describe('minMaxLength validator', () => { + const getMinMaxLenErrMsg = (minLen: number, maxLen: number): string => `Should be ${minLen} to ${maxLen} symbols` + + it('Returns undefined for a string between minimum and maximum length', async () => { + expect(minMaxLength(1, 10)('length7')).toBeUndefined() + }) + + it('Returns an error message for a string with length greater than maximum', async () => { + const minMaxLengths: [number, number] = [1, 5] + + expect(minMaxLength(...minMaxLengths)('length7')).toEqual(getMinMaxLenErrMsg(...minMaxLengths)) + }) + + it('Returns an error message for a string with length less than minimum', async () => { + const minMaxLengths: [number, number] = [7, 10] + + expect(minMaxLength(...minMaxLengths)('string')).toEqual(getMinMaxLenErrMsg(...minMaxLengths)) + }) + }) + + describe('uniqueAddress validator', () => { + it('Returns undefined for an address not contained in the passed array', async () => { + const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'] + + expect(uniqueAddress(addresses)('0xe7e3272a84cf3fe180345b9f7234ba705eB5E2CA')).toBeUndefined() + }) + + it('Returns an error message for an address already contained in the array', async () => { + const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'] + + expect(uniqueAddress(addresses)(addresses[0])).toEqual(ADDRESS_REPEATED_ERROR) + }) + }) + + describe('differentFrom validator', () => { + const getDifferentFromErrMsg = (diffValue: string): string => `Value should be different than ${diffValue}` + + it('Returns undefined for different values', async () => { + expect(differentFrom('a')('b')).toBeUndefined() + }) + + it('Returns an error message for equal values', async () => { + expect(differentFrom('a')('a')).toEqual(getDifferentFromErrMsg('a')) + }) + }) +}) diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 9aa3b87c..a689de83 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -2,20 +2,14 @@ import { List } from 'immutable' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { getWeb3 } from 'src/logic/wallets/getWeb3' +import memoize from 'lodash.memoize' -export const simpleMemoize = (fn) => { - let lastArg - let lastResult - return (arg, ...args) => { - if (arg !== lastArg) { - lastArg = arg - lastResult = fn(arg, ...args) - } - return lastResult - } -} +type ValidatorReturnType = string | undefined +type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType +type AsyncValidator = (...args: unknown[]) => Promise +type Validator = GenericValidatorType | AsyncValidator -export const required = (value?: string) => { +export const required = (value?: string): ValidatorReturnType => { const required = 'Required' if (!value) { @@ -29,30 +23,15 @@ export const required = (value?: string) => { return undefined } -export const mustBeInteger = (value: string) => +export const mustBeInteger = (value: string): ValidatorReturnType => !Number.isInteger(Number(value)) || value.includes('.') ? 'Must be an integer' : undefined -export const mustBeFloat = (value: string) => (value && Number.isNaN(Number(value)) ? 'Must be a number' : undefined) - -export const greaterThan = (min: number | string) => (value: string) => { - if (Number.isNaN(Number(value)) || Number.parseFloat(value) > Number(min)) { - return undefined - } - - return `Should be greater than ${min}` -} - -export const equalOrGreaterThan = (min: number | string) => (value: string): undefined | string => { - if (Number.isNaN(Number(value)) || Number.parseFloat(value) >= Number(min)) { - return undefined - } - - return `Should be equal or greater than ${min}` -} +export const mustBeFloat = (value: string): ValidatorReturnType => + value && Number.isNaN(Number(value)) ? 'Must be a number' : undefined const regexQuery = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i const url = new RegExp(regexQuery) -export const mustBeUrl = (value: string) => { +export const mustBeUrl = (value: string): ValidatorReturnType => { if (url.test(value)) { return undefined } @@ -60,73 +39,68 @@ export const mustBeUrl = (value: string) => { return 'Please, provide a valid url' } -export const minValue = (min: number | string) => (value: string) => { - if (Number.isNaN(Number(value)) || Number.parseFloat(value) >= Number(min)) { +export const minValue = (min: number | string, inclusive = true) => (value: string): ValidatorReturnType => { + if (Number.parseFloat(value) > Number(min) || (inclusive && Number.parseFloat(value) >= Number(min))) { return undefined } - return `Should be at least ${min}` + return `Should be greater than ${inclusive ? 'or equal to ' : ''}${min}` } -export const maxValueCheck = (max: number | string, value: string): string | undefined => { - if (!max || Number.isNaN(Number(value)) || parseFloat(value) <= parseFloat(max.toString())) { +export const maxValue = (max: number | string) => (value: string): ValidatorReturnType => { + if (!max || parseFloat(value) <= parseFloat(max.toString())) { return undefined } return `Maximum value is ${max}` } -export const maxValue = (max: number | string) => (value: string) => { - return maxValueCheck(max, value) -} +export const ok = (): undefined => undefined -export const ok = () => undefined +export const mustBeEthereumAddress = memoize( + (address: string): ValidatorReturnType => { + const startsWith0x = address.startsWith('0x') + const isAddress = getWeb3().utils.isAddress(address) -export const mustBeEthereumAddress = simpleMemoize((address: string) => { - const startsWith0x = address.startsWith('0x') - const isAddress = getWeb3().utils.isAddress(address) + return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name' + }, +) - return startsWith0x && isAddress ? undefined : 'Address should be a valid Ethereum address or ENS name' -}) +export const mustBeEthereumContractAddress = memoize( + async (address: string): Promise => { + const contractCode = await getWeb3().eth.getCode(address) -export const mustBeEthereumContractAddress = simpleMemoize(async (address: string) => { - const contractCode = await getWeb3().eth.getCode(address) + return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' + ? 'Address should be a valid Ethereum contract address or ENS name' + : undefined + }, +) - return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' - ? 'Address should be a valid Ethereum contract address or ENS name' - : undefined -}) - -export const minMaxLength = (minLen, maxLen) => (value) => +export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols` export const ADDRESS_REPEATED_ERROR = 'Address already introduced' -export const uniqueAddress = (addresses: string[] | List) => - simpleMemoize((value: string[]) => { - const addressAlreadyExists = addresses.some((address) => sameAddress(value, address)) - return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined - }) +export const uniqueAddress = (addresses: string[] | List): GenericValidatorType => + memoize( + (value: string): ValidatorReturnType => { + const addressAlreadyExists = addresses.some((address) => sameAddress(value, address)) + return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined + }, + ) -export const composeValidators = (...validators) => (value) => - validators.reduce((error, validator) => error || validator(value), undefined) +export const composeValidators = (...validators: Validator[]) => (value: unknown): ValidatorReturnType => + validators.reduce( + (error: string | undefined, validator: GenericValidatorType): ValidatorReturnType => error || validator(value), + undefined, + ) -export const inLimit = (limit, base, baseText, symbol = 'ETH') => (value) => { - const amount = Number(value) - const max = limit - base - if (amount <= max) { - return undefined - } - - return `Should not exceed ${max} ${symbol} (amount to reach ${baseText})` -} - -export const differentFrom = (diffValue: number | string) => (value: string) => { +export const differentFrom = (diffValue: number | string) => (value: string): ValidatorReturnType => { if (value === diffValue.toString()) { - return `Value should be different than ${value}` + return `Value should be different than ${diffValue}` } return undefined } -export const noErrorsOn = (name, errors) => errors[name] === undefined +export const noErrorsOn = (name: string, errors: Record): boolean => errors[name] === undefined diff --git a/src/logic/contractInteraction/sources/EtherscanService.ts b/src/logic/contractInteraction/sources/EtherscanService.ts index e03c6d46..d9e7bfc5 100644 --- a/src/logic/contractInteraction/sources/EtherscanService.ts +++ b/src/logic/contractInteraction/sources/EtherscanService.ts @@ -13,7 +13,7 @@ class EtherscanService { } _fetch = memoize( - async (url, contractAddress) => { + async (url: string, contractAddress: string) => { let params: any = { module: 'contract', action: 'getAbi', diff --git a/src/logic/contracts/historicProxyCode.ts b/src/logic/contracts/historicProxyCode.ts index 38d0a143..90e08fdc 100644 --- a/src/logic/contracts/historicProxyCode.ts +++ b/src/logic/contracts/historicProxyCode.ts @@ -7,5 +7,5 @@ const proxyCodeV10 = const oldProxyCode = '0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600' -export const isProxyCode = (codeToCheck) => +export const isProxyCode = (codeToCheck: string): boolean => codeToCheck === oldProxyCode || codeToCheck === proxyCodeV10 diff --git a/src/logic/contracts/safeContracts.ts b/src/logic/contracts/safeContracts.ts index ded25a39..2fd08bee 100644 --- a/src/logic/contracts/safeContracts.ts +++ b/src/logic/contracts/safeContracts.ts @@ -3,8 +3,8 @@ import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSaf import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json' import { ensureOnce } from 'src/utils/singleton' -import { simpleMemoize } from 'src/components/forms/validator' -import { getNetworkIdFrom, getWeb3 } from 'src/logic/wallets/getWeb3' +import memoize from 'lodash.memoize' +import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3' import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { isProxyCode } from 'src/logic/contracts/historicProxyCode' @@ -34,8 +34,8 @@ const createProxyFactoryContract = (web3, networkId) => { return proxyFactory } -export const getGnosisSafeContract = simpleMemoize(createGnosisSafeContract) -const getCreateProxyFactoryContract = simpleMemoize(createProxyFactoryContract) +export const getGnosisSafeContract = memoize(createGnosisSafeContract) +const getCreateProxyFactoryContract = memoize(createProxyFactoryContract) const instantiateMasterCopies = async () => { const web3 = getWeb3() @@ -55,7 +55,7 @@ const createMasterCopies = async () => { const accounts = await web3.eth.getAccounts() const userAccount = accounts[0] - const ProxyFactory = getCreateProxyFactoryContract(web3) + const ProxyFactory = getCreateProxyFactoryContract(web3, 4447) proxyFactoryMaster = await ProxyFactory.new({ from: userAccount, gas: '5000000' }) const GnosisSafe = getGnosisSafeContract(web3) @@ -95,18 +95,18 @@ export const estimateGasForDeployingSafe = async ( return gas * parseInt(gasPrice, 10) } -export const getGnosisSafeInstanceAt = simpleMemoize(async (safeAddress): Promise => { +export const getGnosisSafeInstanceAt = memoize(async (safeAddress: string): Promise => { const web3 = getWeb3() const GnosisSafe = await getGnosisSafeContract(web3) return GnosisSafe.at(safeAddress) }) -const cleanByteCodeMetadata = (bytecode) => { +const cleanByteCodeMetadata = (bytecode: string): string => { const metaData = 'a165' return bytecode.substring(0, bytecode.lastIndexOf(metaData)) } -export const validateProxy = async (safeAddress) => { +export const validateProxy = async (safeAddress: string): Promise => { // https://solidity.readthedocs.io/en/latest/metadata.html#usage-for-source-code-verification const web3 = getWeb3() const code = await web3.eth.getCode(safeAddress) diff --git a/src/logic/wallets/ethAddresses.ts b/src/logic/wallets/ethAddresses.ts index b6ad3a75..37cc00fa 100644 --- a/src/logic/wallets/ethAddresses.ts +++ b/src/logic/wallets/ethAddresses.ts @@ -1,6 +1,8 @@ +import { List } from 'immutable' +import { SafeRecord } from 'src/routes/safe/store/models/safe' export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -export const sameAddress = (firstAddress, secondAddress) => { +export const sameAddress = (firstAddress: string, secondAddress: string): boolean => { if (!firstAddress) { return false } @@ -12,7 +14,7 @@ export const sameAddress = (firstAddress, secondAddress) => { return firstAddress.toLowerCase() === secondAddress.toLowerCase() } -export const shortVersionOf = (value, cut) => { +export const shortVersionOf = (value: string, cut: number): string => { if (!value) { return 'Unknown' } @@ -25,7 +27,7 @@ export const shortVersionOf = (value, cut) => { return `${value.substring(0, cut)}...${value.substring(final)}` } -export const isUserOwner = (safe, userAccount) => { +export const isUserAnOwner = (safe: SafeRecord, userAccount: string): boolean => { if (!safe) { return false } @@ -42,6 +44,7 @@ export const isUserOwner = (safe, userAccount) => { return owners.find((owner) => sameAddress(owner.address, userAccount)) !== undefined } -export const isUserOwnerOnAnySafe = (safes, userAccount) => safes.some((safe) => isUserOwner(safe, userAccount)) +export const isUserAnOwnerOfAnySafe = (safes: List | SafeRecord[], userAccount: string): boolean => + safes.some((safe: SafeRecord) => isUserAnOwner(safe, userAccount)) -export const isValidEnsName = (name) => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name) +export const isValidEnsName = (name: string): boolean => /^([\w-]+\.)+(eth|test|xyz|luxe)$/.test(name) diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index 45c5e714..85b545ef 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -3,7 +3,7 @@ import queryString from 'query-string' import React, { useEffect, useState } from 'react' import ReactGA from 'react-ga' import { connect } from 'react-redux' -import { withRouter } from 'react-router-dom' +import { withRouter, RouteComponentProps } from 'react-router-dom' import Opening from '../../opening' import Layout from '../components/Layout' @@ -81,7 +81,14 @@ export const createSafe = (values, userAccount) => { return promiEvent } -const Open = ({ addSafe, network, provider, userAccount }) => { +interface OwnProps extends RouteComponentProps { + userAccount: string + network: string + provider: string + addSafe: any +} + +const Open = ({ addSafe, network, provider, userAccount }: OwnProps): React.ReactElement => { const [loading, setLoading] = useState(false) const [showProgress, setShowProgress] = useState(false) const [creationTxPromise, setCreationTxPromise] = useState() diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 8f3f9bed..80ca792b 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -23,7 +23,7 @@ import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddr import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' import { getAddressBookListSelector } from 'src/logic/addressBook/store/selectors' -import { isUserOwnerOnAnySafe } from 'src/logic/wallets/ethAddresses' +import { isUserAnOwnerOfAnySafe } from 'src/logic/wallets/ethAddresses' import CreateEditEntryModal from 'src/routes/safe/components/AddressBook/CreateEditEntryModal' import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal' import { @@ -136,7 +136,7 @@ const AddressBookTable = ({ classes }) => { > {(sortedData) => sortedData.map((row, index) => { - const userOwner = isUserOwnerOnAnySafe(safesList, row.address) + const userOwner = isUserAnOwnerOfAnySafe(safesList, row.address) const hideBorderBottom = index >= 3 && index === sortedData.size - 1 && classes.noBorderBottom return ( { +}: EthAddressInputProps): React.ReactElement => { const classes = useStyles() const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress] const validate = composeValidators(...validatorsList.filter((_) => _)) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx index a222e961..f1d822ef 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx @@ -19,7 +19,7 @@ import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import TextField from 'src/components/forms/TextField' import TextareaField from 'src/components/forms/TextareaField' -import { composeValidators, maxValue, mustBeFloat, equalOrGreaterThan } from 'src/components/forms/validator' +import { composeValidators, maxValue, mustBeFloat, minValue } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import ButtonLink from 'src/components/layout/ButtonLink' @@ -230,7 +230,7 @@ const SendCustomTx: React.FC = ({ initialValues, onClose, onNext, contrac placeholder="Value*" text="Value*" type="text" - validate={composeValidators(mustBeFloat, maxValue(ethBalance), equalOrGreaterThan(0))} + validate={composeValidators(mustBeFloat, maxValue(ethBalance), minValue(0))} /> diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index 8d3a1888..2812a06b 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -17,14 +17,7 @@ import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import TextField from 'src/components/forms/TextField' -import { - composeValidators, - greaterThan, - maxValue, - maxValueCheck, - mustBeFloat, - required, -} from 'src/components/forms/validator' +import { composeValidators, minValue, maxValue, mustBeFloat, required } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import ButtonLink from 'src/components/layout/ButtonLink' @@ -102,7 +95,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT const selectedTokenRecord = tokens.find((token) => token.address === values?.token) return { - amount: maxValueCheck(selectedTokenRecord?.balance, values.amount), + amount: maxValue(selectedTokenRecord?.balance)(values.amount), } }} > @@ -247,7 +240,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT validate={composeValidators( required, mustBeFloat, - greaterThan(0), + minValue(0), maxValue(selectedTokenRecord?.balance), )} /> diff --git a/src/routes/safe/components/Balances/Tokens/screens/AddCustomAsset/validators.ts b/src/routes/safe/components/Balances/Tokens/screens/AddCustomAsset/validators.ts index b29afadb..4bd84f6a 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/AddCustomAsset/validators.ts +++ b/src/routes/safe/components/Balances/Tokens/screens/AddCustomAsset/validators.ts @@ -1,9 +1,9 @@ -import { simpleMemoize } from 'src/components/forms/validator' +import memoize from 'lodash.memoize' import { isERC721Contract } from 'src/logic/tokens/utils/tokenHelpers' import { sameAddress } from 'src/logic/wallets/ethAddresses' // eslint-disable-next-line -export const addressIsAssetContract = simpleMemoize(async (tokenAddress) => { +export const addressIsAssetContract = memoize(async (tokenAddress) => { const isAsset = await isERC721Contract(tokenAddress) if (!isAsset) { return 'Not a asset address' @@ -12,7 +12,7 @@ export const addressIsAssetContract = simpleMemoize(async (tokenAddress) => { // eslint-disable-next-line export const doesntExistInAssetsList = (assetsList) => - simpleMemoize((tokenAddress) => { + memoize((tokenAddress) => { const tokenIndex = assetsList.findIndex(({ address }) => sameAddress(address, tokenAddress)) if (tokenIndex !== -1) { diff --git a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.ts b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.ts index 232426af..636a1454 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.ts +++ b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.ts @@ -1,11 +1,9 @@ -import { simpleMemoize } from 'src/components/forms/validator' +import memoize from 'lodash.memoize' import { isAddressAToken } from 'src/logic/tokens/utils/tokenHelpers' import { sameAddress } from 'src/logic/wallets/ethAddresses' -// import { getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens' -// eslint-disable-next-line -export const addressIsTokenContract = simpleMemoize(async (tokenAddress) => { +export const addressIsTokenContract = memoize(async (tokenAddress) => { // SECOND APPROACH: // They both seem to work the same // const tokenContract = await getStandardTokenContract() @@ -24,7 +22,7 @@ export const addressIsTokenContract = simpleMemoize(async (tokenAddress) => { // eslint-disable-next-line export const doesntExistInTokenList = (tokenList) => - simpleMemoize((tokenAddress) => { + memoize((tokenAddress: string) => { const tokenIndex = tokenList.findIndex(({ address }) => sameAddress(address, tokenAddress)) if (tokenIndex !== -1) { diff --git a/src/routes/safe/components/Layout/Tabs/index.tsx b/src/routes/safe/components/Layout/Tabs/index.tsx index 1cd25189..1ad32c57 100644 --- a/src/routes/safe/components/Layout/Tabs/index.tsx +++ b/src/routes/safe/components/Layout/Tabs/index.tsx @@ -2,7 +2,7 @@ import Tab from '@material-ui/core/Tab' import Tabs from '@material-ui/core/Tabs' import { withStyles } from '@material-ui/core/styles' import React from 'react' -import { withRouter } from 'react-router-dom' +import { withRouter, RouteComponentProps } from 'react-router-dom' import { styles } from './style' @@ -19,11 +19,8 @@ import { AppsIcon } from 'src/routes/safe/components/assets/AppsIcon' import { BalancesIcon } from 'src/routes/safe/components/assets/BalancesIcon' import { TransactionsIcon } from 'src/routes/safe/components/assets/TransactionsIcon' -interface Props { +interface Props extends RouteComponentProps { classes: Record - match: Record - history: Record - location: Record } const BalancesLabel = ( diff --git a/src/routes/safe/components/Layout/index.tsx b/src/routes/safe/components/Layout/index.tsx index c86ea47c..2473eda0 100644 --- a/src/routes/safe/components/Layout/index.tsx +++ b/src/routes/safe/components/Layout/index.tsx @@ -2,7 +2,7 @@ import { GenericModal } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' import React, { useState } from 'react' import { useSelector } from 'react-redux' -import { Redirect, Route, Switch, withRouter } from 'react-router-dom' +import { Redirect, Route, Switch, withRouter, RouteComponentProps } from 'react-router-dom' import Receive from '../Balances/Receive' @@ -32,16 +32,13 @@ const Balances = React.lazy(() => import('../Balances')) const TxsTable = React.lazy(() => import('src/routes/safe/components/Transactions/TxsTable')) const AddressBookTable = React.lazy(() => import('src/routes/safe/components/AddressBook')) -interface Props { +interface Props extends RouteComponentProps { sendFunds: Record showReceive: boolean onShow: (value: string) => void onHide: (value: string) => void showSendFunds: (value: string) => void hideSendFunds: () => void - match: Record - location: Record - history: Record } const useStyles = makeStyles(styles as any) diff --git a/src/routes/safe/container/selector.ts b/src/routes/safe/container/selector.ts index 1abb8816..30efcd06 100644 --- a/src/routes/safe/container/selector.ts +++ b/src/routes/safe/container/selector.ts @@ -1,19 +1,22 @@ import { List, Map } from 'immutable' import { createSelector } from 'reselect' +import { Token } from 'src/logic/tokens/store/model/token' import { tokensSelector } from 'src/logic/tokens/store/selectors' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' -import { isUserOwner } from 'src/logic/wallets/ethAddresses' +import { isUserAnOwner } from 'src/logic/wallets/ethAddresses' import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { safeActiveTokensSelector, safeBalancesSelector, safeSelector } from 'src/routes/safe/store/selectors' -import { Token } from 'src/logic/tokens/store/model/token' +import { SafeRecord } from 'src/routes/safe/store/models/safe' -export const grantedSelector = createSelector(userAccountSelector, safeSelector, (userAccount, safe) => - isUserOwner(safe, userAccount), +export const grantedSelector = createSelector( + userAccountSelector, + safeSelector, + (userAccount: string, safe: SafeRecord): boolean => isUserAnOwner(safe, userAccount), ) -const safeEthAsTokenSelector = createSelector(safeSelector, (safe): Token | undefined => { +const safeEthAsTokenSelector = createSelector(safeSelector, (safe?: SafeRecord): Token | undefined => { if (!safe) { return undefined } diff --git a/src/routes/safe/store/middleware/notificationsMiddleware.ts b/src/routes/safe/store/middleware/notificationsMiddleware.ts index 081fb19f..fe89c50a 100644 --- a/src/routes/safe/store/middleware/notificationsMiddleware.ts +++ b/src/routes/safe/store/middleware/notificationsMiddleware.ts @@ -5,7 +5,7 @@ import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnac import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import { getAwaitingTransactions } from 'src/logic/safe/transactions/awaitingTransactions' import { getSafeVersionInfo } from 'src/logic/safe/utils/safeVersion' -import { isUserOwner } from 'src/logic/wallets/ethAddresses' +import { isUserAnOwner } from 'src/logic/wallets/ethAddresses' import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns' import { grantedSelector } from 'src/routes/safe/container/selector' @@ -85,7 +85,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => { const safes = safesMapSelector(state) const currentSafe = safes.get(safeAddress) - if (!isUserOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) { + if (!isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.size === 0) { break } diff --git a/src/routes/safe/store/selectors/index.ts b/src/routes/safe/store/selectors/index.ts index 93e5c5fa..6fb83baf 100644 --- a/src/routes/safe/store/selectors/index.ts +++ b/src/routes/safe/store/selectors/index.ts @@ -1,5 +1,5 @@ import { List, Map, Set } from 'immutable' -import { matchPath } from 'react-router-dom' +import { matchPath, RouteComponentProps } from 'react-router-dom' import { createSelector } from 'reselect' import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes' @@ -16,7 +16,7 @@ const safesStateSelector = (state: AppReduxState) => state[SAFE_REDUCER_ID] export const safesMapSelector = (state: AppReduxState): SafesMap => safesStateSelector(state).get('safes') -export const safesListSelector = createSelector(safesMapSelector, (safes) => safes.toList()) +export const safesListSelector = createSelector(safesMapSelector, (safes): List => safes.toList()) export const safesCountSelector = createSelector(safesMapSelector, (safes) => safes.size) @@ -33,7 +33,9 @@ const cancellationTransactionsSelector = (state: AppReduxState) => state[CANCELL const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID] export const safeParamAddressFromStateSelector = (state: AppReduxState): string | null => { - const match = matchPath(state.router.location.pathname, { path: `${SAFELIST_ADDRESS}/:safeAddress` }) + const match = matchPath<{ safeAddress: string }>(state.router.location.pathname, { + path: `${SAFELIST_ADDRESS}/:safeAddress`, + }) if (match) { return checksumAddress(match.params.safeAddress) @@ -42,7 +44,10 @@ export const safeParamAddressFromStateSelector = (state: AppReduxState): string return null } -export const safeParamAddressSelector = (state, props) => { +export const safeParamAddressSelector = ( + state: AppReduxState, + props: RouteComponentProps<{ [SAFE_PARAM_ADDRESS]?: string }>, +): string => { const urlAdd = props.match.params[SAFE_PARAM_ADDRESS] return urlAdd ? checksumAddress(urlAdd) : '' } @@ -105,15 +110,19 @@ export const safeIncomingTransactionsSelector = createSelector( }, ) -export const safeSelector = createSelector(safesMapSelector, safeParamAddressFromStateSelector, (safes, address): - | SafeRecord - | undefined => { - if (!address) { - return undefined - } - const checksumed = checksumAddress(address) - return safes.get(checksumed) -}) +export const safeSelector = createSelector( + safesMapSelector, + safeParamAddressFromStateSelector, + (safes: SafesMap, address: string): SafeRecord | undefined => { + if (!address) { + return undefined + } + const checksumed = checksumAddress(address) + const safe = safes.get(checksumed) + + return safe + }, +) export const safeActiveTokensSelector = createSelector( safeSelector, diff --git a/src/store/index.ts b/src/store/index.ts index 15d93e8f..97fa7ced 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -32,7 +32,7 @@ import safe, { SAFE_REDUCER_ID, SafeReducerMap } from 'src/routes/safe/store/red import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions' import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea' -export const history = createHashHistory({ hashType: 'slash' }) +export const history = createHashHistory() // eslint-disable-next-line const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose diff --git a/src/test/builder/safe.dom.utils.tsx b/src/test/builder/safe.dom.utils.tsx index f90b72f5..28104b71 100644 --- a/src/test/builder/safe.dom.utils.tsx +++ b/src/test/builder/safe.dom.utils.tsx @@ -107,7 +107,7 @@ export const renderSafeView = (store, address) => { const INTERVAL = 500 const MAX_TIMES_EXECUTED = 30 -export const whenSafeDeployed = () => new Promise((resolve, reject) => { +export const whenSafeDeployed = () => new Promise((resolve, reject) => { let times = 0 const interval = setInterval(() => { if (times >= MAX_TIMES_EXECUTED) { diff --git a/src/test/safe.dom.create.tsx b/src/test/safe.dom.create.tsx index 58095ac8..253100ba 100644 --- a/src/test/safe.dom.create.tsx +++ b/src/test/safe.dom.create.tsx @@ -44,7 +44,7 @@ const renderOpenSafeForm = async (localStore) => { ) } -const deploySafe = async (createSafeForm, threshold, numOwners) => { +const deploySafe = async (createSafeForm, threshold, numOwners): Promise => { const web3 = getWeb3() const accounts = await web3.eth.getAccounts() @@ -112,7 +112,7 @@ const deploySafe = async (createSafeForm, threshold, numOwners) => { return whenSafeDeployed() } -const aDeployedSafe = async (specificStore, threshold = 1, numOwners = 1) => { +const aDeployedSafe = async (specificStore, threshold = 1, numOwners = 1): Promise => { const safe = await renderOpenSafeForm(specificStore) await sleep(1500) const safeAddress = await deploySafe(safe, threshold, numOwners) diff --git a/yarn.lock b/yarn.lock index 04432353..73ab2a84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2219,7 +2219,17 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/hoist-non-react-statics@*": +"@types/history@*": + version "4.7.6" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.6.tgz#ed8fc802c45b8e8f54419c2d054e55c9ea344356" + integrity sha512-GRTZLeLJ8ia00ZH8mxMO8t0aC9M1N9bN461Z2eaRurJo6Fpa+utgCwLzI4jQHcrdzuzp5WPN9jRwpsCQ1VhJ5w== + +"@types/history@4.6.2": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0" + integrity sha512-eVAb52MJ4lfPLiO9VvTgv8KaZDEIqCwhv+lXOMLlt4C1YHTShgmMULEg0RrCbnqfYd6QKfHsMp0MiX0vWISpSw== + +"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== @@ -2273,6 +2283,18 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash.memoize@^4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23" + integrity sha512-mYxjKiKzRadRJVClLKxS4wb3Iy9kzwJ1CkbyKiadVxejnswnRByyofmPMscFKscmYpl36BEEhCMPuWhA1R/1ZQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.157" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" + integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -2334,6 +2356,33 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.9": + version "7.1.9" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3" + integrity sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react-router-dom@^5.1.5": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090" + integrity sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.8" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.8.tgz#4614e5ba7559657438e17766bb95ef6ed6acc3fa" + integrity sha512-HzOyJb+wFmyEhyfp4D4NYrumi+LQgQL/68HvJO+q6XtuHSDvw6Aqov7sCAhjbNq3bUPgPqbdvjXC5HeB2oEAPg== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-transition-group@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" @@ -14895,7 +14944,7 @@ redux-thunk@^2.3.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== -redux@4.0.5: +redux@4.0.5, redux@^4.0.0: version "4.0.5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== @@ -18456,7 +18505,6 @@ websocket@^1.0.31: dependencies: debug "^2.2.0" es5-ext "^0.10.50" - gulp "^4.0.2" nan "^2.14.0" typedarray-to-buffer "^3.1.5" yaeti "^0.0.6"