diff --git a/.gitignore b/.gitignore index 607b05dd..ba76c55d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules/ -build/ +./build .DS_Store yarn-error.log .env* diff --git a/package.json b/package.json index 4760fb1b..70d8c515 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "2.5.0", + "version": "2.5.2", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "bugs": { @@ -162,15 +162,15 @@ "@gnosis.pm/safe-contracts": "1.1.1-dev.2", "@gnosis.pm/safe-react-components": "^0.1.3", "@gnosis.pm/util-contracts": "2.0.6", - "@ledgerhq/hw-transport-node-hid": "5.16.0", - "@material-ui/core": "4.10.1", + "@ledgerhq/hw-transport-node-hid": "5.19.0", + "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@material-ui/lab": "4.0.0-alpha.39", - "@openzeppelin/contracts": "3.0.2", + "@openzeppelin/contracts": "3.1.0", "async-sema": "^3.1.0", "axios": "0.19.2", "bignumber.js": "9.0.0", - "bnc-onboard": "1.10.0", + "bnc-onboard": "1.10.2", "classnames": "^2.2.6", "concurrently": "^5.2.0", "connected-react-router": "6.8.0", @@ -178,13 +178,14 @@ "currency-flags": "2.1.2", "date-fns": "2.14.0", "electron-is-dev": "^1.1.0", - "electron-log": "4.2.1", - "electron-settings": "^4.0.0", + "electron-log": "4.2.2", + "electron-settings": "4.0.2", "electron-updater": "4.3.1", "eth-sig-util": "^2.5.3", "ethereum-blockies-base64": "^1.0.2", + "exponential-backoff": "^3.0.1", "express": "^4.17.1", - "final-form": "^4.20.0", + "final-form": "4.20.1", "final-form-calculate": "^1.3.1", "history": "4.10.1", "immortal-db": "^1.0.2", @@ -194,9 +195,9 @@ "material-ui-search-bar": "^1.0.0-beta.13", "notistack": "https://github.com/gnosis/notistack.git#v0.9.4", "open": "^7.0.3", - "polished": "3.6.4", + "polished": "3.6.5", "qrcode.react": "1.0.0", - "query-string": "6.13.0", + "query-string": "6.13.1", "react": "16.13.1", "react-dom": "16.13.1", "react-final-form": "^6.5.0", @@ -216,41 +217,41 @@ "semver": "7.3.2", "styled-components": "^5.0.1", "truffle-contract": "4.0.31", - "web3": "1.2.8" + "web3": "1.2.9" }, "devDependencies": { - "@testing-library/jest-dom": "5.9.0", - "@testing-library/react": "10.2.1", + "@testing-library/jest-dom": "5.11.0", + "@testing-library/react": "10.4.3", "@testing-library/user-event": "11.3.1", "@types/jest": "^25.2.1", - "@types/node": "14.0.12", + "@types/node": "14.0.14", "@types/react": "^16.9.32", "@types/react-dom": "^16.9.6", "@types/styled-components": "^5.1.0", - "@typescript-eslint/eslint-plugin": "3.2.0", - "@typescript-eslint/parser": "3.2.0", - "autoprefixer": "9.8.0", + "@typescript-eslint/eslint-plugin": "3.5.0", + "@typescript-eslint/parser": "3.5.0", + "autoprefixer": "9.8.4", "cross-env": "^7.0.2", "dotenv": "^8.2.0", "dotenv-expand": "^5.1.0", - "electron": "7.1.8", + "electron": "7.2.4", "electron-builder": "22.7.0", "electron-notarize": "0.3.0", "eslint": "6.8.0", "eslint-config-prettier": "6.11.0", - "eslint-plugin-import": "2.21.1", + "eslint-plugin-import": "2.22.0", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-prettier": "^3.1.2", - "eslint-plugin-react": "^7.18.3", - "eslint-plugin-sort-destructure-keys": "1.3.4", + "eslint-plugin-react": "7.20.3", + "eslint-plugin-sort-destructure-keys": "1.3.5", "ethereumjs-abi": "0.6.8", "husky": "^4.2.2", - "lint-staged": "10.2.9", + "lint-staged": "10.2.11", "node-sass": "^4.14.1", "prettier": "2.0.5", "react-app-rewired": "^2.1.6", - "truffle": "5.1.29", - "typescript": "^3.9.5", + "truffle": "5.1.33", + "typescript": "3.9.6", "wait-on": "5.0.1", "web3-core": "^1.2.9", "web3-eth-contract": "^1.2.9", diff --git a/public/build/icon.ico b/public/build/icon.ico new file mode 100644 index 00000000..8039e796 Binary files /dev/null and b/public/build/icon.ico differ diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index ecd3d604..8cf59811 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -40,6 +40,14 @@ export const greaterThan = (min: number | string) => (value: string) => { 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}` +} + 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) => { diff --git a/src/components/layout/Page/index.module.scss b/src/components/layout/Page/index.module.scss index 2e3e196f..26c03497 100644 --- a/src/components/layout/Page/index.module.scss +++ b/src/components/layout/Page/index.module.scss @@ -9,10 +9,17 @@ @media only screen and (max-width: #{$screenLg}px) { .page { - padding: 72px $lg 0px $lg; + padding: 72px $lg 0 $lg; } } +@media only screen and (min-width: #{$screenLg}px) and (max-width: 1360px) { + .page { + padding: 96px 120px 0 120px; + } +} + + .center { align-self: center; } diff --git a/src/config/index.ts b/src/config/index.ts index 9a826a98..8409d2b8 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,5 +1,6 @@ +import { checksumAddress } from 'src/utils/checksumAddress'; import { ensureOnce } from 'src/utils/singleton' -import { ETHEREUM_NETWORK, getWeb3 } from 'src/logic/wallets/getWeb3' +import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3' import { RELAY_API_URL, SIGNATURES_VIA_METAMASK, @@ -90,7 +91,7 @@ export const getSafeLastVersion = () => process.env.REACT_APP_LATEST_SAFE_VERSIO export const buildSafeCreationTxUrl = (safeAddress) => { const host = getTxServiceHost() - const address = getWeb3().utils.toChecksumAddress(safeAddress) + const address = checksumAddress(safeAddress) const base = getSafeCreationTxUri(address) return `${host}${base}` diff --git a/src/logic/collectibles/sources/OpenSea.ts b/src/logic/collectibles/sources/OpenSea.ts index 2fc74225..270db9e5 100644 --- a/src/logic/collectibles/sources/OpenSea.ts +++ b/src/logic/collectibles/sources/OpenSea.ts @@ -4,6 +4,58 @@ import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3' import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png' import { OPENSEA_API_KEY } from 'src/utils/constants' +export interface OpenSeaAssetContract { + address: string + name: string + image_url: string + symbol: string +} + +export interface OpenSeaCollection { + name: string + slug: string +} + +export interface OpenSeaAsset { + asset_contract: OpenSeaAssetContract + background_color: string + collection: OpenSeaCollection + description: string + image_thumbnail_url: string + name: string + token_id: string +} + +export type OpenSeaAssets = Array + +export interface NFTAsset { + address: string + assetContract: OpenSeaAssetContract + collection: OpenSeaCollection + description: string + image: string + name: string + numberOfTokens: number + slug: string + symbol: string +} +export type NFTAssets = Record + +export interface NFTToken { + assetAddress: string + color: string + description: string + image: string + name: string + tokenId: number | string +} +export type NFTTokens = Array + +export interface Collectibles { + nftAssets: NFTAssets + nftTokens: NFTTokens +} + class OpenSea { _rateLimit = async () => {} @@ -29,7 +81,7 @@ class OpenSea { this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true }) } - static extractAssets(assets) { + static extractAssets(assets: OpenSeaAssets): NFTAssets { const extractNFTAsset = (asset) => ({ address: asset.asset_contract.address, assetContract: asset.asset_contract, @@ -59,7 +111,7 @@ class OpenSea { }, {}) } - static extractTokens(assets) { + static extractTokens(assets: OpenSeaAssets): NFTTokens { return assets.map((asset) => ({ assetAddress: asset.asset_contract.address, color: asset.background_color, @@ -70,7 +122,7 @@ class OpenSea { })) } - static extractCollectiblesInfo(assetResponseJson) { + static extractCollectiblesInfo(assetResponseJson: { assets: OpenSeaAssets }): Collectibles { return { nftAssets: OpenSea.extractAssets(assetResponseJson.assets), nftTokens: OpenSea.extractTokens(assetResponseJson.assets), @@ -82,9 +134,9 @@ class OpenSea { * for the provided Safe Address in the specified Network * @param {string} safeAddress * @param {string} network - * @returns {Promise<{ nftAssets: Map, nftTokens: Array }>} + * @returns {Promise} */ - async fetchAllUserCollectiblesByCategoryAsync(safeAddress, network) { + async fetchAllUserCollectiblesByCategoryAsync(safeAddress: string, network: string): Promise { // eslint-disable-next-line no-underscore-dangle const metadataSourceUrl = this._endpointsUrls[network] const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}` diff --git a/src/logic/collectibles/sources/index.ts b/src/logic/collectibles/sources/index.ts index 2b49e0a7..14636bd2 100644 --- a/src/logic/collectibles/sources/index.ts +++ b/src/logic/collectibles/sources/index.ts @@ -2,9 +2,12 @@ import MockedOpenSea from 'src/logic/collectibles/sources/MockedOpenSea' import OpenSea from 'src/logic/collectibles/sources/OpenSea' import { COLLECTIBLES_SOURCE } from 'src/utils/constants' -const sources = { +const SOURCES = { opensea: new OpenSea({ rps: 4 }), mockedopensea: new MockedOpenSea({ rps: 4 }), } -export const getConfiguredSource = () => sources[COLLECTIBLES_SOURCE.toLowerCase()] +type Sources = typeof SOURCES + +export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] => + SOURCES[COLLECTIBLES_SOURCE.toLowerCase()] diff --git a/src/logic/collectibles/store/actions/fetchCollectibles.ts b/src/logic/collectibles/store/actions/fetchCollectibles.ts index c42db3fb..c87e51df 100644 --- a/src/logic/collectibles/store/actions/fetchCollectibles.ts +++ b/src/logic/collectibles/store/actions/fetchCollectibles.ts @@ -3,18 +3,21 @@ import { batch } from 'react-redux' import { getNetwork } from 'src/config' import { getConfiguredSource } from 'src/logic/collectibles/sources' import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles' -import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' +import { Dispatch } from 'redux' -const fetchCollectibles = () => async (dispatch, getState) => { - const network = getNetwork() - const safeAddress = safeParamAddressFromStateSelector(getState()) || '' - const source = getConfiguredSource() - const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network) +const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): Promise => { + try { + const network = getNetwork() + const source = getConfiguredSource() + const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network) - batch(() => { - dispatch(addNftAssets(collectibles.nftAssets)) - dispatch(addNftTokens(collectibles.nftTokens)) - }) + batch(() => { + dispatch(addNftAssets(collectibles.nftAssets)) + dispatch(addNftTokens(collectibles.nftTokens)) + }) + } catch (error) { + console.log('Error fetching collectibles:', error) + } } export default fetchCollectibles diff --git a/src/logic/collectibles/store/selectors/index.ts b/src/logic/collectibles/store/selectors/index.ts index c9a80c14..fb517812 100644 --- a/src/logic/collectibles/store/selectors/index.ts +++ b/src/logic/collectibles/store/selectors/index.ts @@ -1,11 +1,13 @@ import { List } from 'immutable' import { createSelector } from 'reselect' +import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea' +import { AppReduxState } from 'src/store' import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles' import { safeActiveAssetsSelector } from 'src/routes/safe/store/selectors' -export const nftAssetsSelector = (state) => state[NFT_ASSETS_REDUCER_ID] -export const nftTokensSelector = (state) => state[NFT_TOKENS_REDUCER_ID] +export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID] +export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID] export const nftAssetsListSelector = createSelector(nftAssetsSelector, (assets) => { return assets ? List(Object.entries(assets).map((item) => item[1])) : List([]) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index f8192404..c4e08d09 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -11,12 +11,18 @@ import { makeToken } from 'src/logic/tokens/store/model/token' import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens' import updateSafe from 'src/routes/safe/store/actions/updateSafe' import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe' +import { Dispatch } from 'redux' +import { backOff } from 'exponential-backoff' +import { AppReduxState } from 'src/store' const humanReadableBalance = (balance, decimals) => new BigNumber(balance).times(`1e-${decimals}`).toFixed() const noFunc = () => {} const updateSafeValue = (address) => (valueToUpdate) => updateSafe({ address, ...valueToUpdate }) -const fetchSafeTokens = (safeAddress) => async (dispatch, getState) => { +const fetchSafeTokens = (safeAddress: string) => async ( + dispatch: Dispatch, + getState: () => AppReduxState, +): Promise => { try { const state = getState() const safe = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress]) @@ -26,7 +32,7 @@ const fetchSafeTokens = (safeAddress) => async (dispatch, getState) => { return } - const result = await fetchTokenCurrenciesBalances(safeAddress) + const result = await backOff(() => fetchTokenCurrenciesBalances(safeAddress)) const currentEthBalance = safe.get('ethBalance') const safeBalances = safe.get('balances') const alreadyActiveTokens = safe.get('activeTokens') @@ -95,8 +101,6 @@ const fetchSafeTokens = (safeAddress) => async (dispatch, getState) => { } catch (err) { console.error('Error fetching active token list', err) } - - return null } export default fetchSafeTokens diff --git a/src/logic/tokens/store/actions/fetchTokens.ts b/src/logic/tokens/store/actions/fetchTokens.ts index 83d551f2..a2156698 100644 --- a/src/logic/tokens/store/actions/fetchTokens.ts +++ b/src/logic/tokens/store/actions/fetchTokens.ts @@ -9,7 +9,7 @@ import saveTokens from './saveTokens' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import { fetchTokenList } from 'src/logic/tokens/api' -import { makeToken } from 'src/logic/tokens/store/model/token' +import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { tokensSelector } from 'src/logic/tokens/store/selectors' import { getWeb3 } from 'src/logic/wallets/getWeb3' import { store } from 'src/store' @@ -57,7 +57,7 @@ const getTokenValues = (tokenAddress) => methods: ['decimals', 'name', 'symbol'], }) -export const getTokenInfos = async (tokenAddress) => { +export const getTokenInfos = async (tokenAddress: string): Promise => { if (!tokenAddress) { return null } diff --git a/src/logic/tokens/utils/tokenHelpers.ts b/src/logic/tokens/utils/tokenHelpers.ts index 4fab2c79..24ecffe4 100644 --- a/src/logic/tokens/utils/tokenHelpers.ts +++ b/src/logic/tokens/utils/tokenHelpers.ts @@ -10,6 +10,7 @@ import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi' import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' import { isEmptyData } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers' import { TxServiceModel } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' +import { Map } from 'immutable' export const ETH_ADDRESS = '0x000' export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e' @@ -39,11 +40,15 @@ export const isAddressAToken = async (tokenAddress: string): Promise => return call !== '0x' } -export const isTokenTransfer = (tx: any): boolean => { +export const isTokenTransfer = (tx: TxServiceModel): boolean => { return !isEmptyData(tx.data) && tx.data.substring(0, 10) === '0xa9059cbb' && Number(tx.value) === 0 } -export const isSendERC721Transaction = (tx: any, txCode: string, knownTokens: any) => { +export const isSendERC721Transaction = ( + tx: TxServiceModel, + txCode: string, + knownTokens: Map, +): boolean => { // "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" - ens token contract, includes safeTransferFrom // but no proper ERC721 standard implemented return ( @@ -79,9 +84,9 @@ export const getERC20DecimalsAndSymbol = async ( address: tokenAddress, methods: ['decimals', 'symbol'], }) - return { decimals: Number(tokenDecimals), symbol: tokenSymbol } } + return { decimals: storedTokenInfo.decimals as number, symbol: storedTokenInfo.symbol } } catch (err) { console.error(`Failed to retrieve token info for ERC20 token ${tokenAddress}`) } @@ -92,7 +97,7 @@ export const getERC20DecimalsAndSymbol = async ( export const isSendERC20Transaction = async ( tx: TxServiceModel, txCode: string, - knownTokens: any, + knownTokens: Map, ): Promise => { let isSendTokenTx = !isSendERC721Transaction(tx, txCode, knownTokens) && isTokenTransfer(tx) diff --git a/src/routes/load/container/Load.tsx b/src/routes/load/container/Load.tsx index ff96a306..0e3a6500 100644 --- a/src/routes/load/container/Load.tsx +++ b/src/routes/load/container/Load.tsx @@ -10,7 +10,6 @@ import selector from './selector' import Page from 'src/components/layout/Page' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { SAFES_KEY, saveSafes } from 'src/logic/safe/utils' -import { getWeb3 } from 'src/logic/wallets/getWeb3' import { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { buildSafe } from 'src/routes/safe/store/actions/fetchSafe' @@ -19,6 +18,7 @@ import { loadFromStorage } from 'src/utils/storage' import { Dispatch } from 'redux' import { SafeOwner } from '../../safe/store/models/safe' import { List } from 'immutable' +import { checksumAddress } from 'src/utils/checksumAddress' export const loadSafe = async ( safeName: string, @@ -39,14 +39,15 @@ export const loadSafe = async ( class Load extends React.Component { onLoadSafeSubmit = async (values) => { + let safeAddress = values[FIELD_LOAD_ADDRESS] + if (safeAddress) { + return + } + try { const { addSafe } = this.props - const web3 = getWeb3() const safeName = values[FIELD_LOAD_NAME] - let safeAddress = values[FIELD_LOAD_ADDRESS] - if (safeAddress) { - safeAddress = web3.utils.toChecksumAddress(safeAddress) - } + safeAddress = checksumAddress(safeAddress) const ownerNames = getNamesFrom(values) const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) diff --git a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx index d7d3e00e..fc6915ae 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -14,6 +14,19 @@ import { getAddressBookListSelector } from 'src/logic/addressBook/store/selector import { getAddressFromENS } from 'src/logic/wallets/getWeb3' import { isValidEnsName } from 'src/logic/wallets/ethAddresses' +export interface AddressBookProps { + fieldMutator: (address: string) => void + isCustomTx?: boolean + pristine: boolean + recipientAddress?: string + setSelectedEntry: ( + entry: { address?: string; name?: string } | React.SetStateAction<{ address: string; name: string }>, + ) => void + setIsValidAddress: (valid?: boolean) => void +} + +const useStyles = makeStyles(styles) + const textFieldLabelStyle = makeStyles(() => ({ root: { overflow: 'hidden', @@ -30,34 +43,39 @@ const textFieldInputStyle = makeStyles(() => ({ }, })) -const filterAddressBookWithContractAddresses = async (addressBook) => { +const filterAddressBookWithContractAddresses = async ( + addressBook: List<{ address: string }>, +): Promise> => { const abFlags = await Promise.all( - addressBook.map(async ({ address }) => { - return (await mustBeEthereumContractAddress(address)) === undefined - }), + addressBook.map( + async ({ address }: { address: string }): Promise => { + return (await mustBeEthereumContractAddress(address)) === undefined + }, + ), ) - return addressBook.filter((adbkEntry, index) => abFlags[index]) + + return addressBook.filter((_, index) => abFlags[index]) } const AddressBookInput = ({ - classes, fieldMutator, isCustomTx, pristine, recipientAddress, setIsValidAddress, setSelectedEntry, -}: any) => { +}: AddressBookProps) => { + const classes = useStyles() const addressBook = useSelector(getAddressBookListSelector) const [isValidForm, setIsValidForm] = useState(true) - const [validationText, setValidationText] = useState(true) + const [validationText, setValidationText] = useState('') const [inputTouched, setInputTouched] = useState(false) const [blurred, setBlurred] = useState(pristine) - const [adbkList, setADBKList] = useState(List([])) + const [adbkList, setADBKList] = useState>(List([])) const [inputAddValue, setInputAddValue] = useState(recipientAddress) - const onAddressInputChanged = async (addressValue) => { + const onAddressInputChanged = async (addressValue: string): Promise => { setInputAddValue(addressValue) let resolvedAddress = addressValue let isValidText @@ -99,7 +117,7 @@ const AddressBookInput = ({ } useEffect(() => { - const filterAdbkContractAddresses = async () => { + const filterAdbkContractAddresses = async (): Promise => { if (!isCustomTx) { setADBKList(addressBook) return diff --git a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style.ts b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style.ts index b7468d06..b6d2d076 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style.ts @@ -1,4 +1,6 @@ -export const styles = () => ({ +import { createStyles } from '@material-ui/core' + +export const styles = createStyles({ itemOptionList: { display: 'flex', }, diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx index 91ddf4df..0c11b16e 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput/index.tsx @@ -1,7 +1,9 @@ import { makeStyles } from '@material-ui/core/styles' -import React from 'react' +import React, { useState } from 'react' +import { useFormState, useField } from 'react-final-form' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' +import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' import Field from 'src/components/forms/Field' import TextField from 'src/components/forms/TextField' import { @@ -16,7 +18,7 @@ import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/Co const useStyles = makeStyles(styles) -export interface EthAddressProps { +export interface EthAddressInputProps { isContract?: boolean isRequired?: boolean name: string @@ -24,10 +26,24 @@ export interface EthAddressProps { text: string } -const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }: EthAddressProps) => { +const EthAddressInput = ({ + isContract = true, + isRequired = true, + name, + onScannedValue, + text, +}: EthAddressInputProps) => { const classes = useStyles() const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress] const validate = composeValidators(...validatorsList.filter((_) => _)) + const { pristine } = useFormState({ subscription: { pristine: true } }) + const { + input: { value }, + } = useField('contractAddress', { subscription: { value: true } }) + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({ + address: value, + name: '', + }) const handleScan = (value, closeQrModal) => { let scannedAddress = value @@ -44,15 +60,25 @@ const EthAddressInput = ({ isContract = true, isRequired = true, name, onScanned <> - + {selectedEntry?.address ? ( + + ) : ( + {}} + fieldMutator={onScannedValue} + isCustomTx + pristine={pristine} + /> + )} 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 69c172d4..a222e961 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, greaterThan } from 'src/components/forms/validator' +import { composeValidators, maxValue, mustBeFloat, equalOrGreaterThan } 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), greaterThan(0))} + validate={composeValidators(mustBeFloat, maxValue(ethBalance), equalOrGreaterThan(0))} /> diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx index 2c4b94a8..3630a2f1 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx @@ -100,7 +100,6 @@ const ContractInteraction: React.FC = ({ > {(submitting, validating, rest, mutators) => { setCallResults = mutators.setCallResults - return ( <> diff --git a/src/routes/safe/components/Balances/index.tsx b/src/routes/safe/components/Balances/index.tsx index 64923354..52763fea 100644 --- a/src/routes/safe/components/Balances/index.tsx +++ b/src/routes/safe/components/Balances/index.tsx @@ -47,7 +47,7 @@ const Balances = (props) => { const address = useSelector(safeParamAddressFromStateSelector) const featuresEnabled = useSelector(safeFeaturesEnabledSelector) - useFetchTokens() + useFetchTokens(address) useEffect(() => { const erc721Enabled = featuresEnabled && featuresEnabled.includes('ERC721') diff --git a/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx b/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx index 961feb5f..671d4cea 100644 --- a/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx @@ -4,16 +4,31 @@ import EtherScanLink from 'src/components/EtherscanLink' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' import Paragraph from 'src/components/layout/Paragraph' +import { useWindowDimensions } from '../../../../container/hooks/useWindowDimensions' +import { useEffect, useState } from 'react' const OwnerAddressTableCell = (props) => { const { address, knownAddress, showLinks, userName } = props + const [cut, setCut] = useState(undefined) + const { width } = useWindowDimensions() + + useEffect(() => { + if (width <= 900) { + setCut(6) + } else if (width <= 1024) { + setCut(12) + } else { + setCut(undefined) + } + }, [width]) + return ( {showLinks ? (
{userName} - +
) : ( {address} diff --git a/src/routes/safe/container/hooks/useCheckForUpdates.tsx b/src/routes/safe/container/hooks/useCheckForUpdates.tsx deleted file mode 100644 index 62a0da70..00000000 --- a/src/routes/safe/container/hooks/useCheckForUpdates.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect } from 'react' -import { batch, useDispatch, useSelector } from 'react-redux' - -import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles' -import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' -import fetchEtherBalance from 'src/routes/safe/store/actions/fetchEtherBalance' -import { checkAndUpdateSafe } from 'src/routes/safe/store/actions/fetchSafe' -import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions' -import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' -import { TIMEOUT } from 'src/utils/constants' - -export const useCheckForUpdates = () => { - const dispatch = useDispatch() - const safeAddress = useSelector(safeParamAddressFromStateSelector) - useEffect(() => { - if (safeAddress) { - const collectiblesInterval = setInterval(() => { - batch(() => { - dispatch(fetchEtherBalance(safeAddress)) - dispatch(fetchSafeTokens(safeAddress)) - dispatch(fetchTransactions(safeAddress)) - dispatch(fetchCollectibles) - dispatch(checkAndUpdateSafe(safeAddress)) - }) - }, TIMEOUT * 3) - return () => { - clearInterval(collectiblesInterval) - } - } - }, [dispatch, safeAddress]) -} diff --git a/src/routes/safe/container/hooks/useFetchTokens.tsx b/src/routes/safe/container/hooks/useFetchTokens.tsx index bab4bcf2..538ae235 100644 --- a/src/routes/safe/container/hooks/useFetchTokens.tsx +++ b/src/routes/safe/container/hooks/useFetchTokens.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { batch, useDispatch, useSelector } from 'react-redux' +import { batch, useDispatch } from 'react-redux' import { useLocation } from 'react-router-dom' import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles' @@ -8,11 +8,9 @@ import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAsse import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens' import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances' -import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' -export const useFetchTokens = (): void => { +export const useFetchTokens = (safeAddress: string): void => { const dispatch = useDispatch() - const address: string | null = useSelector(safeParamAddressFromStateSelector) const location = useLocation() useMemo(() => { @@ -20,17 +18,17 @@ export const useFetchTokens = (): void => { batch(() => { // fetch tokens there to get symbols for tokens in TXs list dispatch(fetchTokens()) - dispatch(fetchCurrencyValues(address)) - dispatch(fetchSafeTokens(address)) + dispatch(fetchCurrencyValues(safeAddress)) + dispatch(fetchSafeTokens(safeAddress)) }) } if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) { batch(() => { - dispatch(fetchCollectibles()).then(() => { - dispatch(activateAssetsByBalance(address)) + dispatch(fetchCollectibles(safeAddress)).then(() => { + dispatch(activateAssetsByBalance(safeAddress)) }) }) } - }, [address, dispatch, location]) + }, [dispatch, location.pathname, safeAddress]) } diff --git a/src/routes/safe/container/hooks/useSafeScheduledUpdates.tsx b/src/routes/safe/container/hooks/useSafeScheduledUpdates.tsx new file mode 100644 index 00000000..8ba8806d --- /dev/null +++ b/src/routes/safe/container/hooks/useSafeScheduledUpdates.tsx @@ -0,0 +1,46 @@ +import { useEffect, useRef } from 'react' +import { batch, useDispatch } from 'react-redux' + +import fetchCollectibles from 'src/logic/collectibles/store/actions/fetchCollectibles' +import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' +import fetchEtherBalance from 'src/routes/safe/store/actions/fetchEtherBalance' +import { checkAndUpdateSafe } from 'src/routes/safe/store/actions/fetchSafe' +import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions' +import { TIMEOUT } from 'src/utils/constants' + +export const useSafeScheduledUpdates = (safeAddress: string): void => { + const dispatch = useDispatch() + const timer = useRef(null) + + useEffect(() => { + // using this variable to prevent setting a timeout when the component is already unmounted or the effect + // has to run again + let mounted = true + const fetchSafeData = async (address: string): Promise => { + await batch(async () => { + await Promise.all([ + dispatch(fetchEtherBalance(address)), + dispatch(fetchSafeTokens(address)), + dispatch(fetchTransactions(address)), + dispatch(fetchCollectibles(address)), + dispatch(checkAndUpdateSafe(address)), + ]) + }) + + if (mounted) { + timer.current = setTimeout(() => { + fetchSafeData(safeAddress) + }, TIMEOUT * 3) + } + } + + if (safeAddress) { + fetchSafeData(safeAddress) + + return () => { + mounted = false + clearTimeout(timer.current) + } + } + }, [dispatch, safeAddress]) +} diff --git a/src/routes/safe/container/hooks/useWindowDimensions.tsx b/src/routes/safe/container/hooks/useWindowDimensions.tsx new file mode 100644 index 00000000..3a5cd9cf --- /dev/null +++ b/src/routes/safe/container/hooks/useWindowDimensions.tsx @@ -0,0 +1,24 @@ +import { useState, useEffect } from 'react' + +function getWindowDimensions() { + const { innerWidth: width, innerHeight: height } = window + return { + width, + height, + } +} + +export const useWindowDimensions = (): { width: number; height: number } => { + const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions()) + + useEffect(() => { + function handleResize() { + setWindowDimensions(getWindowDimensions()) + } + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + + return windowDimensions +} diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.tsx similarity index 91% rename from src/routes/safe/container/index.jsx rename to src/routes/safe/container/index.tsx index 672cf317..8bb4ef28 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.tsx @@ -7,7 +7,7 @@ import Page from 'src/components/layout/Page' import Layout from 'src/routes/safe/components/Layout' import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors' import { useLoadSafe } from './hooks/useLoadSafe' -import { useCheckForUpdates } from './hooks/useCheckForUpdates' +import { useSafeScheduledUpdates } from './hooks/useSafeScheduledUpdates' const INITIAL_STATE = { sendFunds: { @@ -17,12 +17,12 @@ const INITIAL_STATE = { showReceive: false, } -const SafeView = () => { +const SafeView = (): JSX.Element => { const [state, setState] = useState(INITIAL_STATE) const safeAddress = useSelector(safeParamAddressFromStateSelector) useLoadSafe(safeAddress) - useCheckForUpdates() + useSafeScheduledUpdates(safeAddress) const onShow = (action) => () => { setState((prevState) => ({ diff --git a/src/routes/safe/store/actions/fetchEtherBalance.ts b/src/routes/safe/store/actions/fetchEtherBalance.ts index f045ec2b..0b0e421e 100644 --- a/src/routes/safe/store/actions/fetchEtherBalance.ts +++ b/src/routes/safe/store/actions/fetchEtherBalance.ts @@ -1,12 +1,18 @@ import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3' import updateSafe from 'src/routes/safe/store/actions/updateSafe' import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe' +import { Dispatch } from 'redux' +import { backOff } from 'exponential-backoff' +import { AppReduxState } from 'src/store' -const fetchEtherBalance = (safeAddress) => async (dispatch, getState) => { +const fetchEtherBalance = (safeAddress: string) => async ( + dispatch: Dispatch, + getState: () => AppReduxState, +): Promise => { try { const state = getState() const ethBalance = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress, 'ethBalance']) - const newEthBalance = await getBalanceInEtherOf(safeAddress) + const newEthBalance = await backOff(() => getBalanceInEtherOf(safeAddress)) if (newEthBalance !== ethBalance) { dispatch(updateSafe({ address: safeAddress, ethBalance: newEthBalance })) } diff --git a/src/routes/safe/store/actions/fetchSafe.ts b/src/routes/safe/store/actions/fetchSafe.ts index ed8dca20..11297b44 100644 --- a/src/routes/safe/store/actions/fetchSafe.ts +++ b/src/routes/safe/store/actions/fetchSafe.ts @@ -14,6 +14,7 @@ import { makeOwner } from 'src/routes/safe/store/models/owner' import { checksumAddress } from 'src/utils/checksumAddress' import { SafeOwner } from '../models/safe' +import { Dispatch } from 'redux' const buildOwnersFrom = ( safeOwners, @@ -71,7 +72,7 @@ export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?: return safe } -export const checkAndUpdateSafe = (safeAdd) => async (dispatch) => { +export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch): Promise => { const safeAddress = checksumAddress(safeAdd) // Check if the owner's safe did change and update them const safeParams = ['getThreshold', 'nonce', 'getOwners'] diff --git a/src/routes/safe/store/actions/transactions/fetchTransactions/index.ts b/src/routes/safe/store/actions/transactions/fetchTransactions/index.ts index 6e20a5c4..c927a4a0 100644 --- a/src/routes/safe/store/actions/transactions/fetchTransactions/index.ts +++ b/src/routes/safe/store/actions/transactions/fetchTransactions/index.ts @@ -7,28 +7,39 @@ import { loadOutgoingTransactions } from './loadOutgoingTransactions' import { addOrUpdateCancellationTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions' import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions' +import { Dispatch } from 'redux' +import { backOff } from 'exponential-backoff' const noFunc = () => {} -export default (safeAddress: string) => async (dispatch) => { - const transactions = await loadOutgoingTransactions(safeAddress) +export default (safeAddress: string) => async (dispatch: Dispatch): Promise => { + try { + const transactions = await backOff(() => loadOutgoingTransactions(safeAddress)) - if (transactions) { - const { cancel, outgoing } = transactions - const updateCancellationTxs = cancel.size - ? addOrUpdateCancellationTransactions({ safeAddress, transactions: cancel }) - : noFunc - const updateOutgoingTxs = outgoing.size ? addOrUpdateTransactions({ safeAddress, transactions: outgoing }) : noFunc + if (transactions) { + const { cancel, outgoing } = transactions + const updateCancellationTxs = cancel.size + ? addOrUpdateCancellationTransactions({ safeAddress, transactions: cancel }) + : noFunc + const updateOutgoingTxs = outgoing.size + ? addOrUpdateTransactions({ + safeAddress, + transactions: outgoing, + }) + : noFunc - batch(() => { - dispatch(updateCancellationTxs) - dispatch(updateOutgoingTxs) - }) - } + batch(() => { + dispatch(updateCancellationTxs) + dispatch(updateOutgoingTxs) + }) + } - const incomingTransactions = await loadIncomingTransactions(safeAddress) + const incomingTransactions = await loadIncomingTransactions(safeAddress) - if (incomingTransactions.get(safeAddress).size) { - dispatch(addIncomingTransactions(incomingTransactions)) + if (incomingTransactions.get(safeAddress).size) { + dispatch(addIncomingTransactions(incomingTransactions)) + } + } catch (error) { + console.log('Error fetching transactions:', error) } } diff --git a/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts b/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts index 9b2f8000..b88911bc 100644 --- a/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts +++ b/src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions.ts @@ -66,7 +66,7 @@ export type OutgoingTxs = { export type BatchProcessTxsProps = OutgoingTxs & { currentUser?: string - knownTokens: Record + knownTokens: Map safe: SafeRecord } @@ -79,7 +79,10 @@ export type BatchProcessTxsProps = OutgoingTxs & { const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxServiceModel[]): OutgoingTxs => { return outgoingTxs.reduce( (acc, transaction) => { - if (isCancelTransaction(transaction, safeAddress)) { + if ( + isCancelTransaction(transaction, safeAddress) && + outgoingTxs.find((tx) => tx.nonce === transaction.nonce && !isCancelTransaction(tx, safeAddress)) + ) { if (!isNaN(Number(transaction.nonce))) { acc.cancellationTxs[transaction.nonce] = transaction } diff --git a/src/routes/safe/store/actions/transactions/utils/transactionHelpers.ts b/src/routes/safe/store/actions/transactions/utils/transactionHelpers.ts index ba8de692..6b96983d 100644 --- a/src/routes/safe/store/actions/transactions/utils/transactionHelpers.ts +++ b/src/routes/safe/store/actions/transactions/utils/transactionHelpers.ts @@ -85,7 +85,7 @@ export const isCustomTransaction = async ( tx: TxServiceModel, txCode: string, safeAddress: string, - knownTokens: Record, + knownTokens: Map, ): Promise => { return ( isOutgoingTransaction(tx, safeAddress) && @@ -340,7 +340,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state): Promi ...tx, } - const knownTokens: Record = state[TOKEN_REDUCER_ID] + const knownTokens: Map = state[TOKEN_REDUCER_ID] const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress]) const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map() const outgoingTxs = state[TRANSACTIONS_REDUCER_ID].get(safeAddress) || List() diff --git a/src/routes/safe/store/reducer/safe.ts b/src/routes/safe/store/reducer/safe.ts index 8b3e23ec..4bdeac13 100644 --- a/src/routes/safe/store/reducer/safe.ts +++ b/src/routes/safe/store/reducer/safe.ts @@ -14,6 +14,7 @@ import { UPDATE_SAFE } from 'src/routes/safe/store/actions/updateSafe' import { makeOwner } from 'src/routes/safe/store/models/owner' import makeSafe from 'src/routes/safe/store/models/safe' import { checksumAddress } from 'src/utils/checksumAddress' +import { SafeReducerMap } from './types/safe' export const SAFE_REDUCER_ID = 'safes' export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED' @@ -43,13 +44,17 @@ export const buildSafe = (storedSafe) => { export default handleActions( { - [UPDATE_SAFE]: (state, action) => { + [UPDATE_SAFE]: (state: SafeReducerMap, action) => { const safe = action.payload const safeAddress = safe.address - return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => prevSafe.merge(safe)) + return state.updateIn( + [SAFE_REDUCER_ID, safeAddress], + makeSafe({ name: 'LOADED SAFE', address: safeAddress }), + (prevSafe) => prevSafe.merge(safe), + ) }, - [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action) => { + [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => { const tokenAddress = action.payload return state.withMutations((map) => { @@ -64,7 +69,7 @@ export default handleActions( }) }) }, - [ADD_SAFE]: (state, action) => { + [ADD_SAFE]: (state: SafeReducerMap, action) => { const { safe } = action.payload // if you add a new Safe it needs to be set as a record @@ -77,12 +82,12 @@ export default handleActions( return state.setIn([SAFE_REDUCER_ID, safe.address], makeSafe(safe)) }, - [REMOVE_SAFE]: (state, action) => { + [REMOVE_SAFE]: (state: SafeReducerMap, action) => { const safeAddress = action.payload return state.deleteIn([SAFE_REDUCER_ID, safeAddress]) }, - [ADD_SAFE_OWNER]: (state, action) => { + [ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => { const { ownerAddress, ownerName, safeAddress } = action.payload return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => @@ -91,7 +96,7 @@ export default handleActions( }), ) }, - [REMOVE_SAFE_OWNER]: (state, action) => { + [REMOVE_SAFE_OWNER]: (state: SafeReducerMap, action) => { const { ownerAddress, safeAddress } = action.payload return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => @@ -100,7 +105,7 @@ export default handleActions( }), ) }, - [REPLACE_SAFE_OWNER]: (state, action) => { + [REPLACE_SAFE_OWNER]: (state: SafeReducerMap, action) => { const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => @@ -111,7 +116,7 @@ export default handleActions( }), ) }, - [EDIT_SAFE_OWNER]: (state, action) => { + [EDIT_SAFE_OWNER]: (state: SafeReducerMap, action) => { const { ownerAddress, ownerName, safeAddress } = action.payload return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => { @@ -131,3 +136,5 @@ export default handleActions( latestMasterContractVersion: '', }), ) + +export * from './types/safe.d' diff --git a/src/routes/safe/store/reducer/types/safe.d.ts b/src/routes/safe/store/reducer/types/safe.d.ts new file mode 100644 index 00000000..e5347bf7 --- /dev/null +++ b/src/routes/safe/store/reducer/types/safe.d.ts @@ -0,0 +1,19 @@ +import { SafeRecord } from 'src/routes/safe/store/models/safe' +import { Map } from 'immutable' + +export type SafesMap = Map + +export interface SafeReducerState { + defaultSafe: 'NOT_ASKED' | string | undefined + safes: SafesMap + latestMasterContractVersion: string +} + +interface SafeReducerStateSerialized extends SafeReducerState { + safes: Record +} + +export interface SafeReducerMap extends Map { + toJS(): SafeReducerStateSerialized + get(key: K): SafeReducerState[K] +} diff --git a/src/routes/safe/store/selectors/index.ts b/src/routes/safe/store/selectors/index.ts index ed26ffe0..8d63988e 100644 --- a/src/routes/safe/store/selectors/index.ts +++ b/src/routes/safe/store/selectors/index.ts @@ -1,20 +1,20 @@ import { List, Map, Set } from 'immutable' import { matchPath } from 'react-router-dom' import { createSelector } from 'reselect' - -import { getWeb3 } from 'src/logic/wallets/getWeb3' import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes' import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/cancellationTransactions' import { INCOMING_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/incomingTransactions' -import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe' +import { SAFE_REDUCER_ID, SafesMap } from 'src/routes/safe/store/reducer/safe' import { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions' +import { AppReduxState } from 'src/store' import { checksumAddress } from 'src/utils/checksumAddress' +import { SafeRecord } from 'src/routes/safe/store/models/safe' -const safesStateSelector = (state) => state[SAFE_REDUCER_ID] +const safesStateSelector = (state: AppReduxState) => state[SAFE_REDUCER_ID] -export const safesMapSelector = (state) => state[SAFE_REDUCER_ID].get('safes') +export const safesMapSelector = (state: AppReduxState): SafesMap => state[SAFE_REDUCER_ID].get('safes') export const safesListSelector = createSelector(safesMapSelector, (safes) => safes.toList()) @@ -26,18 +26,17 @@ export const latestMasterContractVersionSelector = createSelector(safesStateSele safeState.get('latestMasterContractVersion'), ) -const transactionsSelector = (state) => state[TRANSACTIONS_REDUCER_ID] +const transactionsSelector = (state: AppReduxState) => state[TRANSACTIONS_REDUCER_ID] -const cancellationTransactionsSelector = (state) => state[CANCELLATION_TRANSACTIONS_REDUCER_ID] +const cancellationTransactionsSelector = (state: AppReduxState) => state[CANCELLATION_TRANSACTIONS_REDUCER_ID] -const incomingTransactionsSelector = (state) => state[INCOMING_TRANSACTIONS_REDUCER_ID] +const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID] -export const safeParamAddressFromStateSelector = (state): string | null => { +export const safeParamAddressFromStateSelector = (state: AppReduxState): string | null => { const match = matchPath(state.router.location.pathname, { path: `${SAFELIST_ADDRESS}/:safeAddress` }) if (match) { - const web3 = getWeb3() - return web3.utils.toChecksumAddress(match.params.safeAddress) + return checksumAddress(match.params.safeAddress) } return null @@ -64,7 +63,7 @@ export const safeTransactionsSelector = createSelector( }, ) -export const addressBookQueryParamsSelector = (state) => { +export const addressBookQueryParamsSelector = (state: AppReduxState): string | null => { const { location } = state.router let entryAddressToEditOrCreateNew = null if (location && location.query) { @@ -116,20 +115,26 @@ export const safeSelector = createSelector(safesMapSelector, safeParamAddressFro return safe }) -export const safeActiveTokensSelector = createSelector(safeSelector, (safe) => { - if (!safe) { - return List() - } +export const safeActiveTokensSelector = createSelector( + safeSelector, + (safe): Set => { + if (!safe) { + return Set() + } - return safe.activeTokens -}) + return safe.activeTokens + }, +) -export const safeActiveAssetsSelector = createSelector(safeSelector, (safe) => { - if (!safe) { - return List() - } - return safe.activeAssets -}) +export const safeActiveAssetsSelector = createSelector( + safeSelector, + (safe): Set => { + if (!safe) { + return Set() + } + return safe.activeAssets + }, +) export const safeActiveAssetsListSelector = createSelector(safeActiveAssetsSelector, (safeList) => { if (!safeList) { @@ -154,20 +159,21 @@ export const safeBlacklistedAssetsSelector = createSelector(safeSelector, (safe) return safe.blacklistedAssets }) -export const safeActiveAssetsSelectorBySafe = (safeAddress, safes) => safes.get(safeAddress).get('activeAssets') +export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap) => + safes.get(safeAddress).get('activeAssets') export const safeBlacklistedAssetsSelectorBySafe = (safeAddress, safes) => safes.get(safeAddress).get('blacklistedAssets') export const safeBalancesSelector = createSelector(safeSelector, (safe) => { if (!safe) { - return List() + return Map() } return safe.balances }) -export const safeFieldSelector = (field) => (safe) => safe?.[field] +export const safeFieldSelector = (field: string) => (safe: SafeRecord) => safe?.[field] export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name')) diff --git a/src/store/index.ts b/src/store/index.ts index 8574ecc4..ea870736 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,6 +1,6 @@ -import { connectRouter, routerMiddleware } from 'connected-react-router' +import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router' import { createHashHistory } from 'history' -import { applyMiddleware, combineReducers, compose, createStore } from 'redux' +import { applyMiddleware, CombinedState, combineReducers, compose, createStore } from 'redux' import thunk from 'redux-thunk' import addressBookMiddleware from 'src/logic/addressBook/store/middleware/addressBookMiddleware' @@ -27,8 +27,12 @@ import cancellationTransactions, { import incomingTransactions, { INCOMING_TRANSACTIONS_REDUCER_ID, } from 'src/routes/safe/store/reducer/incomingTransactions' -import safe, { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe' +import safe, { SAFE_REDUCER_ID, SafeReducerMap } from 'src/routes/safe/store/reducer/safe' import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions' +import { Map } from 'immutable' +import { NFTAssets, NFTTokens } from '../logic/collectibles/sources/OpenSea' +import { ProviderRecord } from '../logic/wallets/store/model/provider' +import { Token } from 'src/logic/tokens/store/model/token' export const history = createHashHistory({ hashType: 'slash' }) @@ -63,6 +67,23 @@ const reducers = combineReducers({ [CURRENT_SESSION_REDUCER_ID]: currentSession, }) +export type AppReduxState = CombinedState<{ + [PROVIDER_REDUCER_ID]?: ProviderRecord + [SAFE_REDUCER_ID]: SafeReducerMap + [NFT_ASSETS_REDUCER_ID]?: NFTAssets + [NFT_TOKENS_REDUCER_ID]?: NFTTokens + [TOKEN_REDUCER_ID]?: Map + [TRANSACTIONS_REDUCER_ID]: Map + [CANCELLATION_TRANSACTIONS_REDUCER_ID]: Map + [INCOMING_TRANSACTIONS_REDUCER_ID]: Map + [NOTIFICATIONS_REDUCER_ID]: Map + [CURRENCY_VALUES_KEY]: Map + [COOKIES_REDUCER_ID]: Map + [ADDRESS_BOOK_REDUCER_ID]: Map + [CURRENT_SESSION_REDUCER_ID]: Map + router: RouterState +}> + export const store: any = createStore(reducers, finalCreateStore) export const aNewStore = (localState?: any) => createStore(reducers, localState, finalCreateStore) diff --git a/src/utils/checksumAddress.ts b/src/utils/checksumAddress.ts index 234cbec3..674509a3 100644 --- a/src/utils/checksumAddress.ts +++ b/src/utils/checksumAddress.ts @@ -1,6 +1,6 @@ import { getWeb3 } from 'src/logic/wallets/getWeb3' -export const checksumAddress = (address) => { +export const checksumAddress = (address: string): string => { if (!address) return null return getWeb3().utils.toChecksumAddress(address) }