Tech Debt: Validator Type definitions (#1108)

* type validators

* safeSelector types

* history 5.0.0 breaking changes adaptation

* replace simpleMemoize with memoize from lodash because of typing issues

* add type definitions for history and react-router-dom

* type fixes

* yarn lock update

* fix router state

* more type improvements

* validator tests wip

* add tests for validators, remove duplicated validators

* add error messages to tests

* fix minValue error message for inclusive param

* Replace jsx.element with react.reactelement

* Fix uniqueAddress validator argument type

* remove comment in AddCustomToken validator

* use absolute import for saferecord in safe paage container
This commit is contained in:
Mikhail Mikheev 2020-07-27 14:31:13 +04:00 committed by GitHub
parent 93448b550a
commit f62bbffdd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 377 additions and 154 deletions

View File

@ -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: {

View File

@ -10,6 +10,4 @@ public
scripts
src/assets
src/config
test
*.spec*
*.test*
test

View File

@ -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",

View File

@ -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'))
})
})
})

View File

@ -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<ValidatorReturnType>
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<ValidatorReturnType> => {
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<string>) =>
simpleMemoize((value: string[]) => {
const addressAlreadyExists = addresses.some((address) => sameAddress(value, address))
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined
})
export const uniqueAddress = (addresses: string[] | List<string>): 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<string, unknown>): boolean => errors[name] === undefined

View File

@ -13,7 +13,7 @@ class EtherscanService {
}
_fetch = memoize(
async (url, contractAddress) => {
async (url: string, contractAddress: string) => {
let params: any = {
module: 'contract',
action: 'getAbi',

View File

@ -7,5 +7,5 @@ const proxyCodeV10 =
const oldProxyCode =
'0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600'
export const isProxyCode = (codeToCheck) =>
export const isProxyCode = (codeToCheck: string): boolean =>
codeToCheck === oldProxyCode || codeToCheck === proxyCodeV10

View File

@ -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<any> => {
export const getGnosisSafeInstanceAt = memoize(async (safeAddress: string): Promise<any> => {
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<boolean> => {
// https://solidity.readthedocs.io/en/latest/metadata.html#usage-for-source-code-verification
const web3 = getWeb3()
const code = await web3.eth.getCode(safeAddress)

View File

@ -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> | 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)

View File

@ -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()

View File

@ -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 (
<TableRow

View File

@ -32,7 +32,7 @@ const EthAddressInput = ({
name,
onScannedValue,
text,
}: EthAddressInputProps) => {
}: EthAddressInputProps): React.ReactElement => {
const classes = useStyles()
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
const validate = composeValidators(...validatorsList.filter((_) => _))

View File

@ -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<Props> = ({ initialValues, onClose, onNext, contrac
placeholder="Value*"
text="Value*"
type="text"
validate={composeValidators(mustBeFloat, maxValue(ethBalance), equalOrGreaterThan(0))}
validate={composeValidators(mustBeFloat, maxValue(ethBalance), minValue(0))}
/>
</Col>
</Row>

View File

@ -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),
)}
/>

View File

@ -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) {

View File

@ -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) {

View File

@ -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<string, any>
match: Record<string, any>
history: Record<string, any>
location: Record<string, any>
}
const BalancesLabel = (

View File

@ -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<string, any>
showReceive: boolean
onShow: (value: string) => void
onHide: (value: string) => void
showSendFunds: (value: string) => void
hideSendFunds: () => void
match: Record<string, any>
location: Record<string, any>
history: Record<string, any>
}
const useStyles = makeStyles(styles as any)

View File

@ -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
}

View File

@ -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
}

View File

@ -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<SafeRecord> => 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,

View File

@ -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

View File

@ -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<string>((resolve, reject) => {
let times = 0
const interval = setInterval(() => {
if (times >= MAX_TIMES_EXECUTED) {

View File

@ -44,7 +44,7 @@ const renderOpenSafeForm = async (localStore) => {
)
}
const deploySafe = async (createSafeForm, threshold, numOwners) => {
const deploySafe = async (createSafeForm, threshold, numOwners): Promise<string> => {
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<string> => {
const safe = await renderOpenSafeForm(specificStore)
await sleep(1500)
const safeAddress = await deploySafe(safe, threshold, numOwners)

View File

@ -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"