diff --git a/src/components/CookiesBanner/index.jsx b/src/components/CookiesBanner/index.jsx index f857685b..b8dcd88e 100644 --- a/src/components/CookiesBanner/index.jsx +++ b/src/components/CookiesBanner/index.jsx @@ -30,7 +30,7 @@ const useStyles = makeStyles({ padding: '27px 15px', position: 'fixed', width: '100%', - zIndex: '5', + zIndex: '15', }, content: { maxWidth: '100%', diff --git a/src/components/Root/index.js b/src/components/Root/index.js index 8dac4d82..399c541f 100644 --- a/src/components/Root/index.js +++ b/src/components/Root/index.js @@ -4,7 +4,7 @@ import 'babel-polyfill' import { theme as styledTheme } from '@gnosis.pm/safe-react-components' import { MuiThemeProvider } from '@material-ui/core/styles' import { ConnectedRouter } from 'connected-react-router' -import React, { Suspense } from 'react' +import React from 'react' import { hot } from 'react-hot-loader/root' import { Provider } from 'react-redux' import { ThemeProvider } from 'styled-components' @@ -15,6 +15,7 @@ import PageFrame from '../layout/PageFrame' import AppRoutes from '~/routes' import { history, store } from '~/store' import theme from '~/theme/mui' +import { wrapInSuspense } from '~/utils/wrapInSuspense' import './index.scss' import './OnboardCustom.scss' @@ -24,11 +25,7 @@ const Root = () => ( - - }> - - - + {wrapInSuspense(, )} diff --git a/src/logic/addressBook/store/selectors/index.js b/src/logic/addressBook/store/selectors/index.js index edef7837..36c73f6b 100644 --- a/src/logic/addressBook/store/selectors/index.js +++ b/src/logic/addressBook/store/selectors/index.js @@ -1,7 +1,6 @@ /* eslint-disable import/named */ // @flow import { List, Map } from 'immutable' -import { useSelector } from 'react-redux' import { Selector, createSelector } from 'reselect' import type { AddressBook } from '~/logic/addressBook/model/addressBook' @@ -35,15 +34,3 @@ export const getAddressBookListSelector: Selector { - if (!userAddress) { - return null - } - const addressBook = useSelector(getAddressBook) - const result = addressBook.filter((addressBookItem) => addressBookItem.address === userAddress) - if (result.size > 0) { - return result.get(0).name - } - return null -} diff --git a/src/logic/addressBook/utils/index.js b/src/logic/addressBook/utils/index.js index 52d98617..cfd3b864 100644 --- a/src/logic/addressBook/utils/index.js +++ b/src/logic/addressBook/utils/index.js @@ -38,7 +38,7 @@ export const getNameFromAddressBook = (userAddress: string): string | null => { return null } const addressBook = useSelector(getAddressBook) - return getNameFromAdbk(addressBook, userAddress) + return addressBook ? getNameFromAdbk(addressBook, userAddress) : null } export const getOwnersWithNameFromAddressBook = (addressBook: AddressBook, ownerList: List) => { diff --git a/src/logic/collectibles/store/actions/fetchCollectibles.js b/src/logic/collectibles/store/actions/fetchCollectibles.js index 0ed61a56..7e326bec 100644 --- a/src/logic/collectibles/store/actions/fetchCollectibles.js +++ b/src/logic/collectibles/store/actions/fetchCollectibles.js @@ -1,4 +1,5 @@ // @flow +import { batch } from 'react-redux' import type { Dispatch } from 'redux' import { getNetwork } from '~/config' @@ -13,8 +14,10 @@ const fetchCollectibles = () => async (dispatch: Dispatch, getState const source = getConfiguredSource() const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network) - dispatch(addNftAssets(collectibles.nftAssets)) - dispatch(addNftTokens(collectibles.nftTokens)) + batch(() => { + dispatch(addNftAssets(collectibles.nftAssets)) + dispatch(addNftTokens(collectibles.nftTokens)) + }) } export default fetchCollectibles diff --git a/src/logic/contracts/generateBatchRequests.js b/src/logic/contracts/generateBatchRequests.js new file mode 100644 index 00000000..6a0d31b8 --- /dev/null +++ b/src/logic/contracts/generateBatchRequests.js @@ -0,0 +1,58 @@ +// @flow +import { getWeb3 } from '~/logic/wallets/getWeb3' + +/** + * Generates a batch request for grouping RPC calls + * @param {object} args + * @param {object} args.abi - contract ABI + * @param {string} args.address - contract address + * @param {object|undefined} args.batch - not required. If set, batch must be initialized outside (web3.BatchRequest) + * @param {object|undefined} args.context - not required. Can be any object, to be added to the batch response + * @param {array<{ args: [any], method: string, type: 'eth'|undefined } | string>} args.methods - methods to be called + * @returns {Promise<[*]>} + */ +const generateBatchRequests = ({ abi, address, batch, context, methods }) => { + const web3 = getWeb3() + const contractInstance = new web3.eth.Contract(abi, address) + const localBatch = batch ? null : new web3.BatchRequest() + + const values = methods.map((methodObject) => { + let method, type, args = [] + + if (typeof methodObject === 'string') { + method = methodObject + } else { + ;({ method, type, args = [] } = methodObject) + } + + return new Promise((resolve) => { + const resolver = (error, result) => { + if (error) { + resolve(null) + } else { + resolve(result) + } + } + + try { + let request + if (type !== undefined) { + request = web3[type][method].request(...args, resolver) + } else { + request = contractInstance.methods[method](...args).call.request(resolver) + } + batch ? batch.add(request) : localBatch.add(request) + } catch (e) { + resolve(null) + } + }) + }) + + localBatch && localBatch.execute() + + const returnValues = context ? [context, ...values] : values + + return Promise.all(returnValues) +} + +export default generateBatchRequests diff --git a/src/logic/contracts/methodIds.js b/src/logic/contracts/methodIds.js index a946cf38..1f8d6eab 100644 --- a/src/logic/contracts/methodIds.js +++ b/src/logic/contracts/methodIds.js @@ -53,8 +53,8 @@ const METHOD_TO_ID = { '0x694e80c3': SAFE_METHODS_NAMES.CHANGE_THRESHOLD, } -export const decodeParamsFromSafeMethod = async (data: string) => { - const web3 = await getWeb3() +export const decodeParamsFromSafeMethod = (data: string) => { + const web3 = getWeb3() const [methodId, params] = [data.slice(0, 10), data.slice(10)] switch (methodId) { diff --git a/src/logic/contracts/safeContracts.js b/src/logic/contracts/safeContracts.js index c82c94fe..7e53953c 100644 --- a/src/logic/contracts/safeContracts.js +++ b/src/logic/contracts/safeContracts.js @@ -3,7 +3,7 @@ import contract from 'truffle-contract' import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json' 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 '~/utils/singleton' +import { ensureOnce, ensureOnceAsync } from '~/utils/singleton' import { simpleMemoize } from '~/components/forms/validator' import { getWeb3, getNetworkIdFrom } from '~/logic/wallets/getWeb3' import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions' @@ -95,13 +95,12 @@ export const estimateGasForDeployingSafe = async ( return gas * parseInt(gasPrice, 10) } -export const getGnosisSafeInstanceAt = async (safeAddress: string) => { +export const getGnosisSafeInstanceAt = simpleMemoize(async (safeAddress: string) => { const web3 = getWeb3() const GnosisSafe = await getGnosisSafeContract(web3) const gnosisSafe = await GnosisSafe.at(safeAddress) - return gnosisSafe -} +}) const cleanByteCodeMetadata = (bytecode: string): string => { const metaData = 'a165' diff --git a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.js b/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.js index b3c15e0b..7fa21013 100644 --- a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.js +++ b/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.js @@ -10,7 +10,11 @@ const fetchTokenCurrenciesBalances = (safeAddress: string) => { const apiUrl = getTxServiceHost() const url = `${apiUrl}safes/${safeAddress}/balances/usd/` - return axios.get(url) + return axios.get(url, { + params: { + limit: 3000, + }, + }) } export default fetchTokenCurrenciesBalances diff --git a/src/logic/currencyValues/store/actions/fetchCurrencySelectedValue.js b/src/logic/currencyValues/store/actions/fetchCurrencySelectedValue.js index 9dd73823..c4129f9a 100644 --- a/src/logic/currencyValues/store/actions/fetchCurrencySelectedValue.js +++ b/src/logic/currencyValues/store/actions/fetchCurrencySelectedValue.js @@ -1,37 +1,21 @@ // @flow -import { List } from 'immutable' import { Dispatch as ReduxDispatch } from 'redux' import fetchCurrenciesRates from '~/logic/currencyValues/api/fetchCurrenciesRates' -import { setCurrencyBalances } from '~/logic/currencyValues/store/actions/setCurrencyBalances' +import { setCurrencyRate } from '~/logic/currencyValues/store/actions/setCurrencyRate' import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues' -import { currencyValuesListSelector } from '~/logic/currencyValues/store/selectors' import type { GlobalState } from '~/store' // eslint-disable-next-line max-len -const fetchCurrencySelectedValue = (currencyValueSelected: AVAILABLE_CURRENCIES) => async ( +const fetchCurrencySelectedValue = (currencyValueSelected: $Keys) => async ( dispatch: ReduxDispatch, - getState: Function, ) => { - const state = getState() - const currencyBalancesList = currencyValuesListSelector(state) - const selectedCurrencyRateInBaseCurrency = await fetchCurrenciesRates(AVAILABLE_CURRENCIES.USD, currencyValueSelected) - - const newList = [] - for (const currencyValue of currencyBalancesList) { - const { balanceInBaseCurrency } = currencyValue - - const balanceInSelectedCurrency = balanceInBaseCurrency * selectedCurrencyRateInBaseCurrency - - const updatedValue = currencyValue.merge({ - currencyName: currencyValueSelected, - balanceInSelectedCurrency, - }) - - newList.push(updatedValue) + if (AVAILABLE_CURRENCIES.USD === currencyValueSelected) { + return dispatch(setCurrencyRate('1')) } - dispatch(setCurrencyBalances(List(newList))) + const selectedCurrencyRateInBaseCurrency = await fetchCurrenciesRates(AVAILABLE_CURRENCIES.USD, currencyValueSelected) + dispatch(setCurrencyRate(selectedCurrencyRateInBaseCurrency)) } export default fetchCurrencySelectedValue diff --git a/src/logic/currencyValues/store/actions/fetchCurrencyValues.js b/src/logic/currencyValues/store/actions/fetchCurrencyValues.js index 8ad14f95..d71b18c0 100644 --- a/src/logic/currencyValues/store/actions/fetchCurrencyValues.js +++ b/src/logic/currencyValues/store/actions/fetchCurrencyValues.js @@ -1,43 +1,32 @@ // @flow -import { List } from 'immutable' +import { batch } from 'react-redux' import type { Dispatch as ReduxDispatch } from 'redux' -import fetchTokenCurrenciesBalances from '~/logic/currencyValues/api/fetchTokenCurrenciesBalances' import fetchCurrencySelectedValue from '~/logic/currencyValues/store/actions/fetchCurrencySelectedValue' import { CURRENCY_SELECTED_KEY } from '~/logic/currencyValues/store/actions/saveCurrencySelected' -import { setCurrencyBalances } from '~/logic/currencyValues/store/actions/setCurrencyBalances' +import { setCurrencyRate } from '~/logic/currencyValues/store/actions/setCurrencyRate' import { setCurrencySelected } from '~/logic/currencyValues/store/actions/setCurrencySelected' -import { AVAILABLE_CURRENCIES, makeBalanceCurrency } from '~/logic/currencyValues/store/model/currencyValues' +import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues' import type { GlobalState } from '~/store' import { loadFromStorage } from '~/utils/storage' -export const fetchCurrencyValues = (safeAddress: string) => async (dispatch: ReduxDispatch) => { +export const fetchCurrencyValues = () => async (dispatch: ReduxDispatch) => { try { - const tokensFetched = await fetchTokenCurrenciesBalances(safeAddress) - - // eslint-disable-next-line max-len - const currencyList = List( - tokensFetched.data - .filter((currencyBalance) => currencyBalance.balanceUsd) - .map((currencyBalance) => { - const { balanceUsd, tokenAddress } = currencyBalance - return makeBalanceCurrency({ - currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null, - tokenAddress, - balanceInBaseCurrency: balanceUsd, - balanceInSelectedCurrency: balanceUsd, - }) - }), - ) - - dispatch(setCurrencyBalances(currencyList)) const currencyStored = await loadFromStorage(CURRENCY_SELECTED_KEY) + if (!currencyStored) { - return dispatch(setCurrencySelected(AVAILABLE_CURRENCIES.USD)) + return batch(() => { + dispatch(setCurrencySelected(AVAILABLE_CURRENCIES.USD)) + dispatch(setCurrencyRate(1)) + }) } + const { currencyValueSelected } = currencyStored - dispatch(fetchCurrencySelectedValue(currencyValueSelected)) - dispatch(setCurrencySelected(currencyValueSelected)) + + batch(() => { + dispatch(setCurrencySelected(currencyValueSelected)) + dispatch(fetchCurrencySelectedValue(currencyValueSelected)) + }) } catch (err) { console.error('Error fetching tokens price list', err) } diff --git a/src/logic/currencyValues/store/actions/setCurrencyRate.js b/src/logic/currencyValues/store/actions/setCurrencyRate.js new file mode 100644 index 00000000..c0cee64c --- /dev/null +++ b/src/logic/currencyValues/store/actions/setCurrencyRate.js @@ -0,0 +1,12 @@ +// @flow +import { createAction } from 'redux-actions' + +import type { CurrencyValuesProps } from '~/logic/currencyValues/store/model/currencyValues' + +export const SET_CURRENCY_RATE = 'SET_CURRENCY_RATE' + +// eslint-disable-next-line max-len +export const setCurrencyRate = createAction( + SET_CURRENCY_RATE, + (currencyRate: string): CurrencyValuesProps => ({ currencyRate }), +) diff --git a/src/logic/currencyValues/store/actions/setCurrencySelected.js b/src/logic/currencyValues/store/actions/setCurrencySelected.js index f8426f34..be0f1c2b 100644 --- a/src/logic/currencyValues/store/actions/setCurrencySelected.js +++ b/src/logic/currencyValues/store/actions/setCurrencySelected.js @@ -9,5 +9,5 @@ export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY' // eslint-disable-next-line max-len export const setCurrencySelected = createAction( SET_CURRENT_CURRENCY, - (currencyValueSelected: AVAILABLE_CURRENCIES): CurrencyValuesProps => ({ currencyValueSelected }), + (currencyValueSelected: $Keys): CurrencyValuesProps => ({ currencyValueSelected }), ) diff --git a/src/logic/currencyValues/store/model/currencyValues.js b/src/logic/currencyValues/store/model/currencyValues.js index a2d07581..f9d813a9 100644 --- a/src/logic/currencyValues/store/model/currencyValues.js +++ b/src/logic/currencyValues/store/model/currencyValues.js @@ -39,7 +39,7 @@ export const AVAILABLE_CURRENCIES = { } export type BalanceCurrencyType = { - currencyName: AVAILABLE_CURRENCIES, + currencyName: $Keys, tokenAddress: string, balanceInBaseCurrency: string, balanceInSelectedCurrency: string, @@ -53,7 +53,8 @@ export const makeBalanceCurrency = Record({ }) export type CurrencyValuesProps = { - currencyValueSelected: AVAILABLE_CURRENCIES, + currencyValueSelected: $Keys, + currencyRate: string, currencyValuesList: BalanceCurrencyType[], } diff --git a/src/logic/currencyValues/store/reducer/currencyValues.js b/src/logic/currencyValues/store/reducer/currencyValues.js index 8d24aff6..2d690fcd 100644 --- a/src/logic/currencyValues/store/reducer/currencyValues.js +++ b/src/logic/currencyValues/store/reducer/currencyValues.js @@ -2,8 +2,8 @@ import { Map } from 'immutable' import { type ActionType, handleActions } from 'redux-actions' -import { SET_CURRENCY_BALANCES } from '../actions/setCurrencyBalances' - +import { SET_CURRENCY_BALANCES } from '~/logic/currencyValues/store/actions/setCurrencyBalances' +import { SET_CURRENCY_RATE } from '~/logic/currencyValues/store/actions/setCurrencyRate' import { SET_CURRENT_CURRENCY } from '~/logic/currencyValues/store/actions/setCurrencySelected' import type { State } from '~/logic/tokens/store/reducer/tokens' @@ -11,19 +11,20 @@ export const CURRENCY_VALUES_KEY = 'currencyValues' export default handleActions( { + [SET_CURRENCY_RATE]: (state: State, action: ActionType): State => { + const { currencyRate } = action.payload + + return state.set('currencyRate', currencyRate) + }, [SET_CURRENCY_BALANCES]: (state: State, action: ActionType): State => { const { currencyBalances } = action.payload - const newState = state.set('currencyBalances', currencyBalances) - - return newState + return state.set('currencyBalances', currencyBalances) }, [SET_CURRENT_CURRENCY]: (state: State, action: ActionType): State => { const { currencyValueSelected } = action.payload - const newState = state.set('currencyValueSelected', currencyValueSelected) - - return newState + return state.set('currencyValueSelected', currencyValueSelected) }, }, Map(), diff --git a/src/logic/currencyValues/store/selectors/index.js b/src/logic/currencyValues/store/selectors/index.js index 35d4c3f1..429bfef5 100644 --- a/src/logic/currencyValues/store/selectors/index.js +++ b/src/logic/currencyValues/store/selectors/index.js @@ -7,4 +7,7 @@ import { type GlobalState } from '~/store' export const currencyValuesListSelector = (state: GlobalState) => state[CURRENCY_VALUES_KEY].get('currencyBalances') ? state[CURRENCY_VALUES_KEY].get('currencyBalances') : List([]) + export const currentCurrencySelector = (state: GlobalState) => state[CURRENCY_VALUES_KEY].get('currencyValueSelected') + +export const currencyRateSelector = (state: GlobalState) => state[CURRENCY_VALUES_KEY].get('currencyRate') diff --git a/src/logic/tokens/store/actions/activateAssetsByBalance.js b/src/logic/tokens/store/actions/activateAssetsByBalance.js index 3cffa977..61811460 100644 --- a/src/logic/tokens/store/actions/activateAssetsByBalance.js +++ b/src/logic/tokens/store/actions/activateAssetsByBalance.js @@ -17,9 +17,14 @@ const activateAssetsByBalance = (safeAddress: string) => async ( getState: GetState, ) => { try { - await dispatch(fetchCollectibles()) const state = getState() const safes = safesMapSelector(state) + + if (safes.size === 0) { + return + } + + await dispatch(fetchCollectibles()) const availableAssets = nftAssetsSelector(state) const alreadyActiveAssets = safeActiveAssetsSelectorBySafe(safeAddress, safes) const blacklistedAssets = safeBlacklistedAssetsSelectorBySafe(safeAddress, safes) diff --git a/src/logic/tokens/store/actions/activateTokensByBalance.js b/src/logic/tokens/store/actions/activateTokensByBalance.js deleted file mode 100644 index da377195..00000000 --- a/src/logic/tokens/store/actions/activateTokensByBalance.js +++ /dev/null @@ -1,61 +0,0 @@ -// @flow -import { Set } from 'immutable' -import type { Dispatch as ReduxDispatch } from 'redux' - -import fetchTokenBalanceList from '~/logic/tokens/api/fetchTokenBalanceList' -import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens' -import updateSafe from '~/routes/safe/store/actions/updateSafe' -import { - safeActiveTokensSelectorBySafe, - safeBlacklistedTokensSelectorBySafe, - safesMapSelector, -} from '~/routes/safe/store/selectors' -import { type GetState, type GlobalState } from '~/store' - -const activateTokensByBalance = (safeAddress: string) => async ( - dispatch: ReduxDispatch, - getState: GetState, -) => { - try { - const result = await fetchTokenBalanceList(safeAddress) - const safes = safesMapSelector(getState()) - const alreadyActiveTokens = safeActiveTokensSelectorBySafe(safeAddress, safes) - const blacklistedTokens = safeBlacklistedTokensSelectorBySafe(safeAddress, safes) - - // addresses: potentially active tokens by balance - // balances: tokens' balance returned by the backend - const { addresses, balances } = result.data.reduce( - (acc, { balance, tokenAddress }) => ({ - addresses: [...acc.addresses, tokenAddress], - balances: [[tokenAddress, balance]], - }), - { - addresses: [], - balances: [], - }, - ) - - // update balance list for the safe - dispatch( - updateSafe({ - address: safeAddress, - balances: Set(balances), - }), - ) - - // active tokens by balance, excluding those already blacklisted and the `null` address - const activeByBalance = addresses.filter((address) => address !== null && !blacklistedTokens.includes(address)) - - // need to persist those already active tokens, despite its balances - const activeTokens = alreadyActiveTokens.toSet().union(activeByBalance) - - // update list of active tokens - dispatch(updateActiveTokens(safeAddress, activeTokens)) - } catch (err) { - console.error('Error fetching active token list', err) - } - - return null -} - -export default activateTokensByBalance diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.js b/src/logic/tokens/store/actions/fetchSafeTokens.js new file mode 100644 index 00000000..276a9ae2 --- /dev/null +++ b/src/logic/tokens/store/actions/fetchSafeTokens.js @@ -0,0 +1,100 @@ +// @flow +import { BigNumber } from 'bignumber.js' +import { List, Map } from 'immutable' +import { batch } from 'react-redux' +import type { Dispatch as ReduxDispatch } from 'redux' + +import fetchTokenCurrenciesBalances from '~/logic/currencyValues/api/fetchTokenCurrenciesBalances' +import { setCurrencyBalances } from '~/logic/currencyValues/store/actions/setCurrencyBalances' +import { AVAILABLE_CURRENCIES, makeBalanceCurrency } from '~/logic/currencyValues/store/model/currencyValues' +import { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues' +import addTokens from '~/logic/tokens/store/actions/saveTokens' +import { makeToken } from '~/logic/tokens/store/model/token' +import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens' +import updateSafe from '~/routes/safe/store/actions/updateSafe' +import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe' +import { type GetState, type GlobalState } from '~/store' + +const humanReadableBalance = (balance, decimals) => BigNumber(balance).times(`1e-${decimals}`).toFixed() +const noFunc = () => {} +const updateSafeValue = (address) => (valueToUpdate) => updateSafe({ address, ...valueToUpdate }) + +const fetchSafeTokens = (safeAddress: string) => async (dispatch: ReduxDispatch, getState: GetState) => { + try { + const state = getState() + const safe = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress]) + const currentTokens = state[TOKEN_REDUCER_ID] + + if (!safe) { + return + } + + const result = await fetchTokenCurrenciesBalances(safeAddress) + const currentEthBalance = safe.get('ethBalance') + const safeBalances = safe.get('balances') + const alreadyActiveTokens = safe.get('activeTokens') + const blacklistedTokens = safe.get('blacklistedTokens') + const currencyValues = state[CURRENCY_VALUES_KEY] + const storedCurrencyBalances = currencyValues.get('currencyBalances') + + const { balances, currencyList, ethBalance, tokens } = result.data.reduce( + (acc, { balance, balanceUsd, token, tokenAddress }) => { + if (tokenAddress === null) { + acc.ethBalance = humanReadableBalance(balance, 18) + } else { + acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableBalance(balance, token.decimals) }) + + if (currentTokens && !currentTokens.get(tokenAddress)) { + acc.tokens = acc.tokens.push(makeToken({ address: tokenAddress, ...token })) + } + } + + acc.currencyList = acc.currencyList.push( + makeBalanceCurrency({ + currencyName: balanceUsd ? AVAILABLE_CURRENCIES.USD : null, + tokenAddress, + balanceInBaseCurrency: balanceUsd, + balanceInSelectedCurrency: balanceUsd, + }), + ) + + return acc + }, + { + balances: Map(), + currencyList: List(), + ethBalance: '0', + tokens: List(), + }, + ) + + // need to persist those already active tokens, despite its balances + const activeTokens = alreadyActiveTokens.toSet().union( + // active tokens by balance, excluding those already blacklisted and the `null` address + balances.keySeq().toSet().subtract(blacklistedTokens), + ) + + const update = updateSafeValue(safeAddress) + const updateActiveTokens = activeTokens.equals(alreadyActiveTokens) ? noFunc : update({ activeTokens }) + const updateBalances = balances.equals(safeBalances) ? noFunc : update({ balances }) + const updateEthBalance = ethBalance === currentEthBalance ? noFunc : update({ ethBalance }) + + const updateCurrencies = currencyList.equals(storedCurrencyBalances) ? noFunc : setCurrencyBalances(currencyList) + + const updateTokens = tokens.size === 0 ? noFunc : addTokens(tokens) + + batch(() => { + dispatch(updateActiveTokens) + dispatch(updateBalances) + dispatch(updateEthBalance) + dispatch(updateCurrencies) + dispatch(updateTokens) + }) + } catch (err) { + console.error('Error fetching active token list', err) + } + + return null +} + +export default fetchSafeTokens diff --git a/src/logic/tokens/store/actions/fetchTokens.js b/src/logic/tokens/store/actions/fetchTokens.js index fdff81a6..1971bb0f 100644 --- a/src/logic/tokens/store/actions/fetchTokens.js +++ b/src/logic/tokens/store/actions/fetchTokens.js @@ -38,7 +38,10 @@ const createERC721TokenContract = async () => { return erc721Token } -const OnlyBalanceToken = { +// For the `batchRequest` of balances, we're just using the `balanceOf` method call. +// So having a simple ABI only with `balanceOf` prevents errors +// when instantiating non-standard ERC-20 Tokens. +export const OnlyBalanceToken = { contractName: 'OnlyBalanceToken', abi: [ { @@ -82,23 +85,12 @@ const OnlyBalanceToken = { ], } -// For the `batchRequest` of balances, we're just using the `balanceOf` method call. -// So having a simple ABI only with `balanceOf` prevents errors -// when instantiating non-standard ERC-20 Tokens. -const createOnlyBalanceToken = () => { - const web3 = getWeb3() - const contract = new web3.eth.Contract(OnlyBalanceToken.abi) - return contract -} - export const getHumanFriendlyToken = ensureOnce(createHumanFriendlyTokenContract) export const getStandardTokenContract = ensureOnce(createStandardTokenContract) export const getERC721TokenContract = ensureOnce(createERC721TokenContract) -export const getOnlyBalanceToken = ensureOnce(createOnlyBalanceToken) - export const containsMethodByHash = async (contractAddress: string, methodHash: string) => { const web3 = getWeb3() const byteCode = await web3.eth.getCode(contractAddress) diff --git a/src/logic/tokens/utils/alternativeAbi.js b/src/logic/tokens/utils/alternativeAbi.js index c6a8abfb..074ff62c 100644 --- a/src/logic/tokens/utils/alternativeAbi.js +++ b/src/logic/tokens/utils/alternativeAbi.js @@ -23,7 +23,7 @@ export const ALTERNATIVE_TOKEN_ABI = [ outputs: [ { name: '', - type: 'bytes32', + type: 'string', }, ], payable: false, diff --git a/src/routes/index.js b/src/routes/index.js index 46e78d99..2e274dcf 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,30 +1,32 @@ // @flow import React, { useEffect, useState } from 'react' -import { connect } from 'react-redux' +import { useSelector } from 'react-redux' import { Redirect, Route, Switch, withRouter } from 'react-router-dom' import { LOAD_ADDRESS, OPEN_ADDRESS, SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS } from './routes' -import Welcome from './welcome/container' import Loader from '~/components/Loader' import { defaultSafeSelector } from '~/routes/safe/store/selectors' -import { withTracker } from '~/utils/googleAnalytics' +import { useAnalytics } from '~/utils/googleAnalytics' -const Safe = React.lazy(() => import('./safe/container')) +const Welcome = React.lazy(() => import('./welcome/container')) const Open = React.lazy(() => import('./open/container/Open')) +const Safe = React.lazy(() => import('./safe/container')) + const Load = React.lazy(() => import('./load/container/Load')) const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}` type RoutesProps = { - defaultSafe?: string, location: Object, } -const Routes = ({ defaultSafe, location }: RoutesProps) => { +const Routes = ({ location }: RoutesProps) => { const [isInitialLoad, setInitialLoad] = useState(true) + const defaultSafe = useSelector(defaultSafeSelector) + const { trackPage } = useAnalytics() useEffect(() => { if (location.pathname !== '/') { @@ -32,6 +34,11 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => { } }, []) + useEffect(() => { + const page = location.pathname + location.search + trackPage(page) + }, [location.pathname, trackPage]) + return ( { return } - setInitialLoad(false) if (defaultSafe) { return } @@ -54,17 +60,13 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => { return }} /> - - - - + + + + ) } -// $FlowFixMe -export default connect( - (state) => ({ defaultSafe: defaultSafeSelector(state) }), - null, -)(withRouter(Routes)) +export default withRouter(Routes) diff --git a/src/routes/safe/components/AddressBook/index.jsx b/src/routes/safe/components/AddressBook/index.jsx index 10ebc8f7..1b776314 100644 --- a/src/routes/safe/components/AddressBook/index.jsx +++ b/src/routes/safe/components/AddressBook/index.jsx @@ -53,12 +53,13 @@ const AddressBookTable = ({ classes }: Props) => { const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) const dispatch = useDispatch() + const safesList = useSelector(safesListSelector) + const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector) const addressBook = useSelector(getAddressBookListSelector) const [selectedEntry, setSelectedEntry] = useState(null) const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false) const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false) const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false) - const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector) useEffect(() => { if (entryAddressToEditOrCreateNew) { @@ -86,8 +87,6 @@ const AddressBookTable = ({ classes }: Props) => { } }, [addressBook]) - const safesList = useSelector(safesListSelector) - const newEntryModalHandler = (entry: AddressBookEntry) => { setEditCreateEntryModalOpen(false) dispatch(addAddressBookEntry(makeAddressBookEntry(entry))) @@ -160,7 +159,8 @@ const AddressBookTable = ({ classes }: Props) => { className={classes.editEntryButton} onClick={() => { setSelectedEntry({ - entry: { ...row, isOwnerAddress: userOwner }, + entry: row, + isOwnerAddress: userOwner, }) setEditCreateEntryModalOpen(true) }} diff --git a/src/routes/safe/components/Apps/index.jsx b/src/routes/safe/components/Apps/index.jsx index 37d0eec1..4b30eda0 100644 --- a/src/routes/safe/components/Apps/index.jsx +++ b/src/routes/safe/components/Apps/index.jsx @@ -2,7 +2,8 @@ import { Card, FixedDialog, FixedIcon, IconText, Menu, Text, Title } from '@gnosis.pm/safe-react-components' import { withSnackbar } from 'notistack' import React, { useCallback, useEffect, useState } from 'react' -import { withRouter } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' import styled from 'styled-components' import ManageApps from './ManageApps' @@ -11,7 +12,14 @@ import sendTransactions from './sendTransactions' import { getAppInfoFromUrl, staticAppsList } from './utils' import { ListContentLayout as LCL, Loader } from '~/components-v2' +import { networkSelector } from '~/logic/wallets/store/selectors' import { SAFELIST_ADDRESS } from '~/routes/routes' +import { grantedSelector } from '~/routes/safe/container/selector' +import { + safeEthBalanceSelector, + safeNameSelector, + safeParamAddressFromStateSelector, +} from '~/routes/safe/store/selectors' import { loadFromStorage, saveToStorage } from '~/utils/storage' const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' @@ -35,40 +43,26 @@ const operations = { } type Props = { - web3: any, - safeAddress: String, - safeName: String, - ethBalance: String, - history: Object, - network: String, - granted: Boolean, - createTransaction: any, enqueueSnackbar: Function, closeSnackbar: Function, openModal: () => {}, closeModal: () => {}, } -function Apps({ - closeModal, - closeSnackbar, - createTransaction, - enqueueSnackbar, - ethBalance, - granted, - history, - network, - openModal, - safeAddress, - safeName, - web3, -}: Props) { +function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }: Props) { const [appList, setAppList] = useState([]) const [legalDisclaimerAccepted, setLegalDisclaimerAccepted] = useState(false) const [selectedApp, setSelectedApp] = useState() const [loading, setLoading] = useState(true) const [appIsLoading, setAppIsLoading] = useState(true) const [iframeEl, setIframeEl] = useState(null) + const history = useHistory() + const granted = useSelector(grantedSelector) + const safeName = useSelector(safeNameSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const network = useSelector(networkSelector) + const ethBalance = useSelector(safeEthBalanceSelector) + const dispatch = useDispatch() const getSelectedApp = () => appList.find((e) => e.id === selectedApp) @@ -87,15 +81,7 @@ function Apps({ const onConfirm = async () => { closeModal() - await sendTransactions( - web3, - createTransaction, - safeAddress, - data.data, - enqueueSnackbar, - closeSnackbar, - getSelectedApp().id, - ) + await sendTransactions(dispatch, safeAddress, data.data, enqueueSnackbar, closeSnackbar, getSelectedApp().id) } confirmTransactions( @@ -408,4 +394,4 @@ function Apps({ ) } -export default withSnackbar(withRouter(Apps)) +export default withSnackbar(Apps) diff --git a/src/routes/safe/components/Apps/sendTransactions.js b/src/routes/safe/components/Apps/sendTransactions.js index 5a9d7c61..278686d4 100644 --- a/src/routes/safe/components/Apps/sendTransactions.js +++ b/src/routes/safe/components/Apps/sendTransactions.js @@ -1,5 +1,7 @@ // @flow import { DELEGATE_CALL } from '~/logic/safe/transactions/send' +import { getWeb3 } from '~/logic/wallets/getWeb3' +import createTransaction from '~/routes/safe/store/actions/createTransaction' const multiSendAddress = '0xB522a9f781924eD250A11C54105E51840B138AdD' const multiSendAbi = [ @@ -15,14 +17,14 @@ const multiSendAbi = [ ] const sendTransactions = ( - web3: any, - createTransaction: any, + dispatch: Function, safeAddress: String, txs: Array, enqueueSnackbar: Function, closeSnackbar: Function, origin: string, ) => { + const web3 = getWeb3() const multiSend = new web3.eth.Contract(multiSendAbi, multiSendAddress) const encodeMultiSendCalldata = multiSend.methods @@ -41,17 +43,19 @@ const sendTransactions = ( ) .encodeABI() - return createTransaction({ - safeAddress, - to: multiSendAddress, - valueInWei: 0, - txData: encodeMultiSendCalldata, - notifiedTransaction: 'STANDARD_TX', - enqueueSnackbar, - closeSnackbar, - operation: DELEGATE_CALL, - // navigateToTransactionsTab: false, - origin, - }) + return dispatch( + createTransaction({ + safeAddress, + to: multiSendAddress, + valueInWei: 0, + txData: encodeMultiSendCalldata, + notifiedTransaction: 'STANDARD_TX', + enqueueSnackbar, + closeSnackbar, + operation: DELEGATE_CALL, + // navigateToTransactionsTab: false, + origin, + }), + ) } export default sendTransactions diff --git a/src/routes/safe/components/Balances/Coins/index.jsx b/src/routes/safe/components/Balances/Coins/index.jsx index 5f2c20eb..315b34db 100644 --- a/src/routes/safe/components/Balances/Coins/index.jsx +++ b/src/routes/safe/components/Balances/Coins/index.jsx @@ -6,6 +6,7 @@ import { makeStyles } from '@material-ui/core/styles' import CallMade from '@material-ui/icons/CallMade' import CallReceived from '@material-ui/icons/CallReceived' import classNames from 'classnames/bind' +import { List } from 'immutable' import React from 'react' import { useSelector } from 'react-redux' @@ -16,7 +17,11 @@ import type { Column } from '~/components/Table/TableHead' import { cellWidth } from '~/components/Table/TableHead' import Button from '~/components/layout/Button' import Row from '~/components/layout/Row' -import { currencyValuesListSelector, currentCurrencySelector } from '~/logic/currencyValues/store/selectors' +import { + currencyRateSelector, + currencyValuesListSelector, + currentCurrencySelector, +} from '~/logic/currencyValues/store/selectors' import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances' import AssetTableCell from '~/routes/safe/components/Balances/AssetTableCell' import type { BalanceRow } from '~/routes/safe/components/Balances/dataFetcher' @@ -42,10 +47,15 @@ const Coins = (props: Props) => { const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) const currencySelected = useSelector(currentCurrencySelector) + const currencyRate = useSelector(currencyRateSelector) const activeTokens = useSelector(extendedSafeTokensSelector) const currencyValues = useSelector(currencyValuesListSelector) const granted = useSelector(grantedSelector) - const filteredData = getBalanceData(activeTokens, currencySelected, currencyValues) + const [filteredData, setFilteredData] = React.useState(List()) + + React.useMemo(() => { + setFilteredData(getBalanceData(activeTokens, currencySelected, currencyValues, currencyRate)) + }, [currencySelected, currencyRate, activeTokens.hashCode(), currencyValues.hashCode()]) return ( diff --git a/src/routes/safe/components/Balances/Receive/index.jsx b/src/routes/safe/components/Balances/Receive/index.jsx index 3c970b4d..03657944 100644 --- a/src/routes/safe/components/Balances/Receive/index.jsx +++ b/src/routes/safe/components/Balances/Receive/index.jsx @@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import QRCode from 'qrcode.react' import * as React from 'react' +import { useSelector } from 'react-redux' import CopyBtn from '~/components/CopyBtn' import EtherscanBtn from '~/components/EtherscanBtn' @@ -14,6 +15,7 @@ import Col from '~/components/layout/Col' import Hairline from '~/components/layout/Hairline' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' +import { safeNameSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' import { lg, md, screenSm, secondaryText, sm } from '~/theme/variables' import { copyToClipboard } from '~/utils/clipboard' @@ -75,53 +77,55 @@ const styles = () => ({ type Props = { onClose: () => void, classes: Object, - safeName: string, - safeAddress: string, } -const Receive = ({ classes, onClose, safeAddress, safeName }: Props) => ( - <> - - - Receive funds - - - - - - - - This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send - ETH and ERC-20 tokens to this address! - - - - {safeName} - - - - - - - { - copyToClipboard(safeAddress) - }} - > - {safeAddress} +const Receive = ({ classes, onClose }: Props) => { + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeName = useSelector(safeNameSelector) + return ( + <> + + + Receive funds - - - - - - - - - -) + + + + + + + This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send + ETH and ERC-20 tokens to this address! + + + + {safeName} + + + + + + + { + copyToClipboard(safeAddress) + }} + > + {safeAddress} + + + + + + + + + + + ) +} export default withStyles(styles)(Receive) diff --git a/src/routes/safe/components/Balances/dataFetcher.js b/src/routes/safe/components/Balances/dataFetcher.js index 13ea18c5..15fd7d40 100644 --- a/src/routes/safe/components/Balances/dataFetcher.js +++ b/src/routes/safe/components/Balances/dataFetcher.js @@ -1,4 +1,5 @@ // @flow +import { BigNumber } from 'bignumber.js' import { List } from 'immutable' import { type Column } from '~/components/Table/TableHead' @@ -23,38 +24,38 @@ export type BalanceRow = SortRow // eslint-disable-next-line max-len const getTokenPriceInCurrency = ( token: Token, - currencySelected: typeof AVAILABLE_CURRENCIES, + currencySelected: $Keys, currencyValues: List, + currencyRate: string, ): string => { if (!currencySelected) { return '' } - // eslint-disable-next-line no-restricted-syntax - for (const tokenPriceIterator of currencyValues) { - const { balanceInSelectedCurrency, currencyName, tokenAddress } = tokenPriceIterator - if (token.address === tokenAddress && currencySelected === currencyName) { - const balance = balanceInSelectedCurrency - ? parseFloat(balanceInSelectedCurrency, 10).toFixed(2) - : balanceInSelectedCurrency - return `${balance} ${currencySelected}` - } - // ETH token + const currencyValue = currencyValues.find(({ tokenAddress }) => { if (token.address === ETH_ADDRESS && !tokenAddress) { - const balance = balanceInSelectedCurrency - ? parseFloat(balanceInSelectedCurrency, 10).toFixed(2) - : balanceInSelectedCurrency - return `${balance} ${currencySelected}` + return true } + + return token.address === tokenAddress + }) + + if (!currencyValue) { + return '' } - return null + + const { balanceInBaseCurrency } = currencyValue + const balance = BigNumber(balanceInBaseCurrency).times(currencyRate).toFixed(2) + + return `${balance} ${currencySelected}` } // eslint-disable-next-line max-len export const getBalanceData = ( activeTokens: List, - currencySelected: string, + currencySelected: $Keys, currencyValues: List, + currencyRate: string, ): List => { const rows = activeTokens.map((token: Token) => ({ [BALANCE_TABLE_ASSET_ID]: { @@ -66,7 +67,7 @@ export const getBalanceData = ( [BALANCE_TABLE_BALANCE_ID]: `${formatAmount(token.balance)} ${token.symbol}`, [buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: Number(token.balance), [FIXED]: token.get('symbol') === 'ETH', - [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues), + [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues, currencyRate), })) return rows diff --git a/src/routes/safe/components/Balances/index.jsx b/src/routes/safe/components/Balances/index.jsx index d89f25c9..e6fb1532 100644 --- a/src/routes/safe/components/Balances/index.jsx +++ b/src/routes/safe/components/Balances/index.jsx @@ -1,7 +1,7 @@ // @flow import { withStyles } from '@material-ui/core/styles' -import { List } from 'immutable' -import * as React from 'react' +import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' import Receive from './Receive' import Tokens from './Tokens' @@ -13,14 +13,15 @@ import Col from '~/components/layout/Col' import Divider from '~/components/layout/Divider' import Link from '~/components/layout/Link' import Row from '~/components/layout/Row' -import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues' -import { type Token } from '~/logic/tokens/store/model/token' import { SAFELIST_ADDRESS } from '~/routes/routes' -import Coins from '~/routes/safe/components/Balances/Coins' -import Collectibles from '~/routes/safe/components/Balances/Collectibles' import SendModal from '~/routes/safe/components/Balances/SendModal' import DropdownCurrency from '~/routes/safe/components/DropdownCurrency' +import { useFetchTokens } from '~/routes/safe/container/Hooks/useFetchTokens' +import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' import { history } from '~/store' +import { wrapInSuspense } from '~/utils/wrapInSuspense' +const Collectibles = React.lazy(() => import('~/routes/safe/components/Balances/Collectibles')) +const Coins = React.lazy(() => import('~/routes/safe/components/Balances/Coins')) export const MANAGE_TOKENS_BUTTON_TEST_ID = 'manage-tokens-btn' export const BALANCE_ROW_TEST_ID = 'balance-row' @@ -37,71 +38,52 @@ type State = { } type Props = { - activateTokensByBalance: Function, - activateAssetsByBalance: Function, - activeTokens: List, - blacklistedTokens: List, classes: Object, - createTransaction: Function, - currencySelected: string, - currencyValues: BalanceCurrencyType[], - ethBalance: string, - featuresEnabled: string[], - fetchCurrencyValues: Function, - fetchTokens: Function, - granted: boolean, - safeAddress: string, - safeName: string, - tokens: List, } type Action = 'Token' | 'Send' | 'Receive' | 'ManageCollectibleModal' -class Balances extends React.Component { - constructor(props) { - super(props) - this.state = { - erc721Enabled: false, - subMenuOptions: [], - showToken: false, - showManageCollectibleModal: false, - sendFunds: { - isOpen: false, - selectedToken: undefined, - }, - showCoins: true, - showCollectibles: false, - showReceive: false, - } - props.fetchTokens() - } +const INITIAL_STATE: State = { + erc721Enabled: false, + subMenuOptions: [], + showToken: false, + showManageCollectibleModal: false, + sendFunds: { + isOpen: false, + selectedToken: undefined, + }, + showCoins: true, + showCollectibles: false, + showReceive: false, +} - static isCoinsLocation = /\/balances\/?$/ - static isCollectiblesLocation = /\/balances\/collectibles$/ +export const COINS_LOCATION_REGEX = /\/balances\/?$/ +export const COLLECTIBLES_LOCATION_REGEX = /\/balances\/collectibles$/ - componentDidMount(): void { - const { activateAssetsByBalance, activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props - fetchCurrencyValues(safeAddress) - activateTokensByBalance(safeAddress) - activateAssetsByBalance(safeAddress) +const Balances = (props: Props) => { + const [state, setState] = useState(INITIAL_STATE) - const showCollectibles = Balances.isCollectiblesLocation.test(history.location.pathname) - const showCoins = Balances.isCoinsLocation.test(history.location.pathname) + const address = useSelector(safeParamAddressFromStateSelector) + const featuresEnabled = useSelector(safeFeaturesEnabledSelector) + + useFetchTokens() + + useEffect(() => { + const showCollectibles = COLLECTIBLES_LOCATION_REGEX.test(history.location.pathname) + const showCoins = COINS_LOCATION_REGEX.test(history.location.pathname) + const subMenuOptions = [{ enabled: showCoins, legend: 'Coins', url: `${SAFELIST_ADDRESS}/${address}/balances` }] if (!showCollectibles && !showCoins) { - history.replace(`${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances`) + history.replace(`${SAFELIST_ADDRESS}/${address}/balances`) } - const subMenuOptions = [ - { enabled: showCoins, legend: 'Coins', url: `${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances` }, - ] - const erc721Enabled = this.props.featuresEnabled.includes('ERC721') + const erc721Enabled = featuresEnabled && featuresEnabled.includes('ERC721') if (erc721Enabled) { subMenuOptions.push({ enabled: showCollectibles, legend: 'Collectibles', - url: `${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances/collectibles`, + url: `${SAFELIST_ADDRESS}/${address}/balances/collectibles`, }) } else { if (showCollectibles) { @@ -109,124 +91,129 @@ class Balances extends React.Component { } } - this.setState({ + setState((prevState) => ({ + ...prevState, showCoins, showCollectibles, erc721Enabled, subMenuOptions, - }) + })) + }, [history.location.pathname, featuresEnabled]) + + const onShow = (action: Action) => { + setState((prevState) => ({ ...prevState, [`show${action}`]: true })) } - onShow = (action: Action) => () => { - this.setState(() => ({ [`show${action}`]: true })) + const onHide = (action: Action) => { + setState((prevState) => ({ ...prevState, [`show${action}`]: false })) } - onHide = (action: Action) => () => { - this.setState(() => ({ [`show${action}`]: false })) - } - - showSendFunds = (tokenAddress: string) => { - this.setState({ + const showSendFunds = (tokenAddress: string) => { + setState((prevState) => ({ + ...prevState, sendFunds: { isOpen: true, selectedToken: tokenAddress, }, - }) + })) } - hideSendFunds = () => { - this.setState({ + const hideSendFunds = () => { + setState((prevState) => ({ + ...prevState, sendFunds: { isOpen: false, selectedToken: undefined, }, - }) + })) } - render() { - const { - erc721Enabled, - sendFunds, - showCoins, - showCollectibles, - showManageCollectibleModal, - showReceive, - showToken, - subMenuOptions, - } = this.state - const { activeTokens, classes, createTransaction, ethBalance, safeAddress, safeName } = this.props + const { + assetDivider, + assetTab, + assetTabActive, + assetTabs, + controls, + manageTokensButton, + receiveModal, + tokenControls, + } = props.classes + const { + erc721Enabled, + sendFunds, + showCoins, + showCollectibles, + showManageCollectibleModal, + showReceive, + showToken, + subMenuOptions, + } = state - return ( - <> - - - {subMenuOptions.length > 1 && - subMenuOptions.map(({ enabled, legend, url }, index) => ( - - {index > 0 && } - - {legend} - - - ))} - - - {showCoins && } - - Manage List - - - - - - - {showCoins && } - {erc721Enabled && showCollectibles && } - - - - - - ) - } + return ( + <> + + + {subMenuOptions.length > 1 && + subMenuOptions.map(({ enabled, legend, url }, index) => ( + + {index > 0 && } + + {legend} + + + ))} + + + {showCoins && } + onShow('ManageCollectibleModal') : () => onShow('Token')} + size="lg" + testId="manage-tokens-btn" + > + Manage List + + onHide('ManageCollectibleModal') : () => onHide('Token')} + open={showToken || showManageCollectibleModal} + title="Manage List" + > + onHide('ManageCollectibleModal') : () => onHide('Token')} + safeAddress={address} + /> + + + + {showCoins && wrapInSuspense( onShow('Receive')} showSendFunds={showSendFunds} />)} + {erc721Enabled && showCollectibles && wrapInSuspense()} + + onHide('Receive')} + open={showReceive} + paperClassName={receiveModal} + title="Receive Tokens" + > + onHide('Receive')} /> + + + ) } export default withStyles(styles)(Balances) diff --git a/src/routes/safe/components/Layout.jsx b/src/routes/safe/components/Layout.jsx deleted file mode 100644 index 3a34bacd..00000000 --- a/src/routes/safe/components/Layout.jsx +++ /dev/null @@ -1,399 +0,0 @@ -// @flow -import Badge from '@material-ui/core/Badge' -import Tab from '@material-ui/core/Tab' -import Tabs from '@material-ui/core/Tabs' -import { withStyles } from '@material-ui/core/styles' -import CallMade from '@material-ui/icons/CallMade' -import CallReceived from '@material-ui/icons/CallReceived' -import classNames from 'classnames/bind' -import React, { useState } from 'react' -import { Redirect, Route, Switch, withRouter } from 'react-router-dom' - -import { type Actions } from '../container/actions' - -import Balances from './Balances' -import Receive from './Balances/Receive' -import Settings from './Settings' -import Transactions from './Transactions' -import { AddressBookIcon } from './assets/AddressBookIcon' -import { AppsIcon } from './assets/AppsIcon' -import { BalancesIcon } from './assets/BalancesIcon' -import { SettingsIcon } from './assets/SettingsIcon' -import { TransactionsIcon } from './assets/TransactionsIcon' -import { styles } from './style' - -import { GenericModal } from '~/components-v2' -import CopyBtn from '~/components/CopyBtn' -import EtherscanBtn from '~/components/EtherscanBtn' -import Identicon from '~/components/Identicon' -import Modal from '~/components/Modal' -import NoSafe from '~/components/NoSafe' -import Block from '~/components/layout/Block' -import Button from '~/components/layout/Button' -import Hairline from '~/components/layout/Hairline' -import Heading from '~/components/layout/Heading' -import Paragraph from '~/components/layout/Paragraph' -import Row from '~/components/layout/Row' -import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3' -import AddressBookTable from '~/routes/safe/components/AddressBook' -import SendModal from '~/routes/safe/components/Balances/SendModal' -import { type SelectorProps } from '~/routes/safe/container/selector' -import { border } from '~/theme/variables' - -export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn' -export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn' -export const TRANSACTIONS_TAB_BTN_TEST_ID = 'transactions-tab-btn' -export const ADDRESS_BOOK_TAB_BTN_TEST_ID = 'address-book-tab-btn' -export const SAFE_VIEW_NAME_HEADING_TEST_ID = 'safe-name-heading' - -const Apps = React.lazy(() => import('./Apps')) - -type Props = SelectorProps & - Actions & { - classes: Object, - granted: boolean, - sendFunds: Object, - showReceive: boolean, - onShow: Function, - onHide: Function, - showSendFunds: Function, - hideSendFunds: Function, - match: Object, - location: Object, - history: Object, - fetchCurrencyValues: Function, - updateAddressBookEntry: Function, - } - -const Layout = (props: Props) => { - const { - activateAssetsByBalance, - activateTokensByBalance, - activeTokens, - addressBook, - blacklistedTokens, - cancellationTransactions, - classes, - createTransaction, - currencySelected, - currencyValues, - fetchCurrencyValues, - fetchTokens, - granted, - hideSendFunds, - location, - match, - network, - onHide, - onShow, - processTransaction, - provider, - safe, - sendFunds, - showReceive, - showSendFunds, - tokens, - transactions, - updateAddressBookEntry, - updateSafe, - userAddress, - } = props - - const [modal, setModal] = useState({ - isOpen: false, - title: null, - body: null, - footer: null, - onClose: null, - }) - - const handleCallToRouter = (_, value) => { - const { history } = props - - history.push(value) - } - - if (!safe) { - return - } - - const { address, ethBalance, featuresEnabled, name } = safe - const etherScanLink = getEtherScanLink('address', address) - const web3Instance = getWeb3() - - const openGenericModal = (modalConfig) => { - setModal({ ...modalConfig, isOpen: true }) - } - - const closeGenericModal = () => { - if (modal.onClose) { - modal.onClose() - } - - setModal({ - isOpen: false, - title: null, - body: null, - footer: null, - onClose: null, - }) - } - - const labelAddressBook = ( - <> - - Address Book - - ) - - const labelApps = ( - <> - - Apps - - ) - - const labelSettings = ( - <> - - - Settings - - - ) - const labelBalances = ( - <> - - Assets - - ) - const labelTransactions = ( - <> - - Transactions - - ) - - const renderAppsTab = () => ( - - - - ) - - const tabsValue = () => { - const balanceLocation = `${match.url}/balances` - const isInBalance = new RegExp(`^${balanceLocation}.*$`) - const { pathname } = location - - if (isInBalance.test(pathname)) { - return balanceLocation - } - - return pathname - } - - return ( - <> - - - - - - - {name} - - {!granted && Read Only} - - - - {address} - - - - - - - - - - - - - - - {process.env.REACT_APP_ENV !== 'production' && ( - - )} - - - - - - ( - - )} - /> - ( - - )} - /> - - ( - - )} - /> - } /> - - - - - - - - {modal.isOpen && } - - ) -} - -export default withStyles(styles)(withRouter(Layout)) diff --git a/src/routes/safe/components/Layout/Header/index.jsx b/src/routes/safe/components/Layout/Header/index.jsx new file mode 100644 index 00000000..728cf493 --- /dev/null +++ b/src/routes/safe/components/Layout/Header/index.jsx @@ -0,0 +1,82 @@ +// @flow +import { withStyles } from '@material-ui/core/styles' +import CallMade from '@material-ui/icons/CallMade' +import CallReceived from '@material-ui/icons/CallReceived' +import classNames from 'classnames/bind' +import React from 'react' +import { useSelector } from 'react-redux' + +import { styles } from './style' + +import CopyBtn from '~/components/CopyBtn' +import EtherscanBtn from '~/components/EtherscanBtn' +import Identicon from '~/components/Identicon' +import Block from '~/components/layout/Block' +import Button from '~/components/layout/Button' +import Heading from '~/components/layout/Heading' +import Paragraph from '~/components/layout/Paragraph' +import Row from '~/components/layout/Row' +import { SAFE_VIEW_NAME_HEADING_TEST_ID } from '~/routes/safe/components/Layout' +import { grantedSelector } from '~/routes/safe/container/selector' +import { safeNameSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' + +type Props = { + classes: Object, + showSendFunds: Function, + onShow: Function, +} + +const LayoutHeader = (props: Props) => { + const { classes, onShow, showSendFunds } = props + const address = useSelector(safeParamAddressFromStateSelector) + const granted = useSelector(grantedSelector) + const name = useSelector(safeNameSelector) + if (!address) return null + + return ( + + + + + + + {name} + + {!granted && Read Only} + + + + {address} + + + + + + + + + + + + ) +} +export default withStyles(styles)(LayoutHeader) diff --git a/src/routes/safe/components/style.js b/src/routes/safe/components/Layout/Header/style.js similarity index 73% rename from src/routes/safe/components/style.js rename to src/routes/safe/components/Layout/Header/style.js index b21c13db..36e48e87 100644 --- a/src/routes/safe/components/style.js +++ b/src/routes/safe/components/Layout/Header/style.js @@ -1,5 +1,5 @@ // @flow -import { screenSm, secondary, secondaryText, sm, smallFontSize, xs } from '~/theme/variables' +import { screenSm, secondaryText, sm, smallFontSize, xs } from '~/theme/variables' export const styles = () => ({ container: { @@ -34,19 +34,6 @@ export const styles = () => ({ user: { justifyContent: 'left', }, - receiveModal: { - height: 'auto', - maxWidth: 'calc(100% - 30px)', - minHeight: '544px', - overflow: 'hidden', - }, - open: { - paddingLeft: sm, - width: 'auto', - '&:hover': { - cursor: 'pointer', - }, - }, readonly: { backgroundColor: secondaryText, borderRadius: xs, @@ -99,22 +86,6 @@ export const styles = () => ({ leftIcon: { marginRight: sm, }, - tabWrapper: { - display: 'flex', - flexDirection: 'row', - '& svg': { - display: 'block', - marginRight: '5px', - }, - '& .fill': { - fill: 'rgba(0, 0, 0, 0.54)', - }, - }, - tabWrapperSelected: { - '& .fill': { - fill: secondary, - }, - }, nameText: { overflowWrap: 'break-word', wordBreak: 'break-word', diff --git a/src/routes/safe/components/Layout/Tabs/SettingsTab/index.jsx b/src/routes/safe/components/Layout/Tabs/SettingsTab/index.jsx new file mode 100644 index 00000000..9379bdcb --- /dev/null +++ b/src/routes/safe/components/Layout/Tabs/SettingsTab/index.jsx @@ -0,0 +1,30 @@ +// @flow +import Badge from '@material-ui/core/Badge' +import React from 'react' +import { useSelector } from 'react-redux' + +import { SettingsIcon } from '~/routes/safe/components/assets/SettingsIcon' +import { grantedSelector } from '~/routes/safe/container/selector' +import { safeNeedsUpdateSelector } from '~/routes/safe/store/selectors' + +const SettingsTab = () => { + const needsUpdate = useSelector(safeNeedsUpdateSelector) + const granted = useSelector(grantedSelector) + + return ( + <> + + + Settings + + + ) +} + +export default SettingsTab diff --git a/src/routes/safe/components/Layout/Tabs/index.jsx b/src/routes/safe/components/Layout/Tabs/index.jsx new file mode 100644 index 00000000..3ade3bd6 --- /dev/null +++ b/src/routes/safe/components/Layout/Tabs/index.jsx @@ -0,0 +1,135 @@ +// @flow +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 { styles } from './style' + +import { + ADDRESS_BOOK_TAB_BTN_TEST_ID, + BALANCES_TAB_BTN_TEST_ID, + SETTINGS_TAB_BTN_TEST_ID, + TRANSACTIONS_TAB_BTN_TEST_ID, +} from '~/routes/safe/components/Layout' +import SettingsTab from '~/routes/safe/components/Layout/Tabs/SettingsTab' +import { AddressBookIcon } from '~/routes/safe/components/assets/AddressBookIcon' +import { AppsIcon } from '~/routes/safe/components/assets/AppsIcon' +import { BalancesIcon } from '~/routes/safe/components/assets/BalancesIcon' +import { TransactionsIcon } from '~/routes/safe/components/assets/TransactionsIcon' + +type Props = { + classes: Object, + match: Object, + history: Object, + location: Object, +} + +const TabsComponent = (props: Props) => { + const { classes, location, match } = props + + const handleCallToRouter = (_, value) => { + const { history } = props + + history.push(value) + } + + const tabsValue = () => { + const balanceLocation = `${match.url}/balances` + const isInBalance = new RegExp(`^${balanceLocation}.*$`) + const { pathname } = location + + if (isInBalance.test(pathname)) { + return balanceLocation + } + + return pathname + } + + const labelBalances = ( + <> + + Assets + + ) + + const labelAddressBook = ( + <> + + Address Book + + ) + + const labelApps = ( + <> + + Apps + + ) + + const labelTransactions = ( + <> + + Transactions + + ) + return ( + + + + {process.env.REACT_APP_ENV !== 'production' && ( + + )} + + } + value={`${match.url}/settings`} + /> + + ) +} +export default withStyles(styles)(withRouter(TabsComponent)) diff --git a/src/routes/safe/components/Layout/Tabs/style.js b/src/routes/safe/components/Layout/Tabs/style.js new file mode 100644 index 00000000..9a098b31 --- /dev/null +++ b/src/routes/safe/components/Layout/Tabs/style.js @@ -0,0 +1,21 @@ +// @flow +import { secondary } from '~/theme/variables' + +export const styles = () => ({ + tabWrapper: { + display: 'flex', + flexDirection: 'row', + '& svg': { + display: 'block', + marginRight: '5px', + }, + '& .fill': { + fill: 'rgba(0, 0, 0, 0.54)', + }, + }, + tabWrapperSelected: { + '& .fill': { + fill: secondary, + }, + }, +}) diff --git a/src/routes/safe/components/Layout/index.jsx b/src/routes/safe/components/Layout/index.jsx new file mode 100644 index 00000000..39912905 --- /dev/null +++ b/src/routes/safe/components/Layout/index.jsx @@ -0,0 +1,126 @@ +// @flow +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 Receive from '../Balances/Receive' + +import { styles } from './style' + +import { GenericModal } from '~/components-v2' +import Modal from '~/components/Modal' +import NoSafe from '~/components/NoSafe' +import Hairline from '~/components/layout/Hairline' +import { providerNameSelector } from '~/logic/wallets/store/selectors' +import SendModal from '~/routes/safe/components/Balances/SendModal' +import LayoutHeader from '~/routes/safe/components/Layout/Header' +import TabsComponent from '~/routes/safe/components/Layout/Tabs' +import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' +import { border } from '~/theme/variables' +import { wrapInSuspense } from '~/utils/wrapInSuspense' + +export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn' +export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn' +export const TRANSACTIONS_TAB_BTN_TEST_ID = 'transactions-tab-btn' +export const ADDRESS_BOOK_TAB_BTN_TEST_ID = 'address-book-tab-btn' +export const SAFE_VIEW_NAME_HEADING_TEST_ID = 'safe-name-heading' + +const Apps = React.lazy(() => import('../Apps')) +const Settings = React.lazy(() => import('../Settings')) +const Balances = React.lazy(() => import('../Balances')) +const TxsTable = React.lazy(() => import('~/routes/safe/components/Transactions/TxsTable')) +const AddressBookTable = React.lazy(() => import('~/routes/safe/components/AddressBook')) + +type Props = { + classes: Object, + sendFunds: Object, + showReceive: boolean, + onShow: Function, + onHide: Function, + showSendFunds: Function, + hideSendFunds: Function, + match: Object, + location: Object, + history: Object, +} + +const useStyles = makeStyles(styles) + +const Layout = (props: Props) => { + const classes = useStyles() + const { hideSendFunds, match, onHide, onShow, sendFunds, showReceive, showSendFunds } = props + + const [modal, setModal] = useState({ + isOpen: false, + title: null, + body: null, + footer: null, + onClose: null, + }) + + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const provider = useSelector(providerNameSelector) + if (!safeAddress) { + return + } + + const openGenericModal = (modalConfig) => { + setModal({ ...modalConfig, isOpen: true }) + } + + const closeGenericModal = () => { + if (modal.onClose) { + modal.onClose() + } + + setModal({ + isOpen: false, + title: null, + body: null, + footer: null, + onClose: null, + }) + } + + return ( + <> + + + + + wrapInSuspense(, null)} /> + wrapInSuspense(, null)} /> + {process.env.REACT_APP_ENV !== 'production' && ( + wrapInSuspense(, null)} + /> + )} + wrapInSuspense(, null)} /> + wrapInSuspense(, null)} /> + + + + + + + + {modal.isOpen && } + + ) +} + +export default withRouter(Layout) diff --git a/src/routes/safe/components/Layout/style.js b/src/routes/safe/components/Layout/style.js new file mode 100644 index 00000000..0e48bc0f --- /dev/null +++ b/src/routes/safe/components/Layout/style.js @@ -0,0 +1,24 @@ +// @flow +import { screenSm, sm } from '~/theme/variables' + +export const styles = () => ({ + receiveModal: { + height: 'auto', + maxWidth: 'calc(100% - 30px)', + minHeight: '544px', + overflow: 'hidden', + }, + receive: { + borderRadius: '4px', + marginLeft: sm, + width: '50%', + + '& > span': { + fontSize: '14px', + }, + [`@media (min-width: ${screenSm}px)`]: { + minWidth: '95px', + width: 'auto', + }, + }, +}) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx index 1939aeb9..e76e1a4d 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.jsx @@ -3,7 +3,7 @@ import { withStyles } from '@material-ui/core/styles' import { List } from 'immutable' import { withSnackbar } from 'notistack' import React, { useEffect, useState } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import OwnerForm from './screens/OwnerForm' import ReviewAddOwner from './screens/Review' @@ -13,7 +13,10 @@ import Modal from '~/components/Modal' import { addOrUpdateAddressBookEntry } from '~/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' +import addSafeOwner from '~/routes/safe/store/actions/addSafeOwner' +import createTransaction from '~/routes/safe/store/actions/createTransaction' import { type Owner } from '~/routes/safe/store/models/owner' +import { safeOwnersSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' const styles = () => ({ biggerModalWindow: { @@ -28,12 +31,6 @@ type Props = { onClose: () => void, classes: Object, isOpen: boolean, - safeAddress: string, - safeName: string, - owners: List, - threshold: number, - addSafeOwner: Function, - createTransaction: Function, enqueueSnackbar: Function, closeSnackbar: Function, } @@ -45,43 +42,34 @@ export const sendAddOwner = async ( ownersOld: List, enqueueSnackbar: Function, closeSnackbar: Function, - createTransaction: Function, - addSafeOwner: Function, + dispatch: Function, ) => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const txData = gnosisSafe.contract.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() - const txHash = await createTransaction({ - safeAddress, - to: safeAddress, - valueInWei: 0, - txData, - notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - }) + const txHash = await dispatch( + createTransaction({ + safeAddress, + to: safeAddress, + valueInWei: 0, + txData, + notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, + enqueueSnackbar, + closeSnackbar, + }), + ) if (txHash) { - addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress }) + dispatch(addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress })) } } -const AddOwner = ({ - addSafeOwner, - classes, - closeSnackbar, - createTransaction, - enqueueSnackbar, - isOpen, - onClose, - owners, - safeAddress, - safeName, - threshold, -}: Props) => { - const dispatch = useDispatch() +const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose }: Props) => { const [activeScreen, setActiveScreen] = useState('selectOwner') const [values, setValues] = useState({}) + const dispatch = useDispatch() + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const owners = useSelector(safeOwnersSelector) useEffect( () => () => { @@ -120,8 +108,7 @@ const AddOwner = ({ onClose() try { - await sendAddOwner(values, safeAddress, owners, enqueueSnackbar, closeSnackbar, createTransaction, addSafeOwner) - + await sendAddOwner(values, safeAddress, owners, enqueueSnackbar, closeSnackbar, dispatch) dispatch( addOrUpdateAddressBookEntry(values.ownerAddress, { name: values.ownerName, address: values.ownerAddress }), ) @@ -139,26 +126,12 @@ const AddOwner = ({ title="Add owner to Safe" > <> - {activeScreen === 'selectOwner' && } + {activeScreen === 'selectOwner' && } {activeScreen === 'selectThreshold' && ( - + )} {activeScreen === 'reviewAddOwner' && ( - + )} diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx index 5409529e..106692fd 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx @@ -2,8 +2,8 @@ import IconButton from '@material-ui/core/IconButton' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import { List } from 'immutable' import React from 'react' +import { useSelector } from 'react-redux' import { styles } from './style' @@ -18,7 +18,7 @@ import Col from '~/components/layout/Col' import Hairline from '~/components/layout/Hairline' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' -import { type Owner } from '~/routes/safe/store/models/owner' +import { safeOwnersSelector } from '~/routes/safe/store/selectors' export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input' export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid' @@ -34,13 +34,13 @@ type Props = { onClose: () => void, classes: Object, onSubmit: Function, - owners: List, } -const OwnerForm = ({ classes, onClose, onSubmit, owners }: Props) => { +const OwnerForm = ({ classes, onClose, onSubmit }: Props) => { const handleSubmit = (values) => { onSubmit(values) } + const owners = useSelector(safeOwnersSelector) const ownerDoesntExist = uniqueAddress(owners.map((o) => o.address)) return ( diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx index e30a67f4..d66b5422 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.jsx @@ -3,8 +3,8 @@ import IconButton from '@material-ui/core/IconButton' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames' -import { List } from 'immutable' import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' import { styles } from './style' @@ -21,23 +21,23 @@ import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { getWeb3 } from '~/logic/wallets/getWeb3' -import type { Owner } from '~/routes/safe/store/models/owner' +import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn' type Props = { onClose: () => void, classes: Object, - safeName: string, - owners: List, values: Object, onClickBack: Function, onSubmit: Function, - safeAddress: string, } -const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, owners, safeAddress, safeName, values }: Props) => { +const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }: Props) => { const [gasCosts, setGasCosts] = useState('< 0.001') + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeName = useSelector(safeNameSelector) + const owners = useSelector(safeOwnersSelector) useEffect(() => { let isCurrent = true const estimateGas = async () => { diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.jsx index 0d57ac9c..8318d80a 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.jsx @@ -3,8 +3,8 @@ import IconButton from '@material-ui/core/IconButton' import MenuItem from '@material-ui/core/MenuItem' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import { List } from 'immutable' import React from 'react' +import { useSelector } from 'react-redux' import { styles } from './style' @@ -18,7 +18,7 @@ import Col from '~/components/layout/Col' import Hairline from '~/components/layout/Hairline' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' -import type { Owner } from '~/routes/safe/store/models/owner' +import { safeOwnersSelector, safeThresholdSelector } from '~/routes/safe/store/selectors' export const ADD_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'add-owner-threshold-next-btn' @@ -27,11 +27,11 @@ type Props = { onClickBack: Function, onClose: () => void, onSubmit: Function, - owners: List, - threshold: number, } -const ThresholdForm = ({ classes, onClickBack, onClose, onSubmit, owners, threshold }: Props) => { +const ThresholdForm = ({ classes, onClickBack, onClose, onSubmit }: Props) => { + const threshold = useSelector(safeThresholdSelector) + const owners = useSelector(safeOwnersSelector) const handleSubmit = (values) => { onSubmit(values) } diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx index 19058820..399ffb96 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.jsx @@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import { withSnackbar } from 'notistack' import React from 'react' +import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' @@ -21,8 +22,11 @@ import Hairline from '~/components/layout/Hairline' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' import { makeAddressBookEntry } from '~/logic/addressBook/model/addressBook' +import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/updateAddressBookEntry' import { getNotificationsFromTxType, showSnackbar } from '~/logic/notifications' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' +import editSafeOwner from '~/routes/safe/store/actions/editSafeOwner' +import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' import { sm } from '~/theme/variables' export const RENAME_OWNER_INPUT_TEST_ID = 'rename-owner-input' @@ -32,31 +36,27 @@ type Props = { onClose: () => void, classes: Object, isOpen: boolean, - safeAddress: string, ownerAddress: string, selectedOwnerName: string, - editSafeOwner: Function, enqueueSnackbar: Function, closeSnackbar: Function, - updateAddressBookEntry: Function, } const EditOwnerComponent = ({ classes, closeSnackbar, - editSafeOwner, enqueueSnackbar, isOpen, onClose, ownerAddress, - safeAddress, selectedOwnerName, - updateAddressBookEntry, }: Props) => { + const dispatch = useDispatch() + const safeAddress = useSelector(safeParamAddressFromStateSelector) const handleSubmit = (values) => { const { ownerName } = values - editSafeOwner({ safeAddress, ownerAddress, ownerName }) - updateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })) + dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName })) + dispatch(updateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName }))) const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.OWNER_NAME_CHANGE_TX) showSnackbar(notification.afterExecution.noMoreConfirmationsNeeded, enqueueSnackbar, closeSnackbar) @@ -131,6 +131,4 @@ const EditOwnerComponent = ({ ) } -const EditOwnerModal = withStyles(styles)(withSnackbar(EditOwnerComponent)) - -export default EditOwnerModal +export default withStyles(styles)(withSnackbar(EditOwnerComponent)) diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx index a8454383..4b5e9ceb 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.jsx @@ -3,6 +3,7 @@ import { withStyles } from '@material-ui/core/styles' import { List } from 'immutable' import { withSnackbar } from 'notistack' import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import CheckOwner from './screens/CheckOwner' import ReviewRemoveOwner from './screens/Review' @@ -11,8 +12,14 @@ import ThresholdForm from './screens/ThresholdForm' import Modal from '~/components/Modal' import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' +import createTransaction from '~/routes/safe/store/actions/createTransaction' +import removeSafeOwner from '~/routes/safe/store/actions/removeSafeOwner' import { type Owner } from '~/routes/safe/store/models/owner' -import type { Safe } from '~/routes/safe/store/models/safe' +import { + safeOwnersSelector, + safeParamAddressFromStateSelector, + safeThresholdSelector, +} from '~/routes/safe/store/selectors' const styles = () => ({ biggerModalWindow: { @@ -27,17 +34,10 @@ type Props = { onClose: () => void, classes: Object, isOpen: boolean, - safeAddress: string, - safeName: string, ownerAddress: string, ownerName: string, - owners: List, - threshold: number, - createTransaction: Function, - removeSafeOwner: Function, enqueueSnackbar: Function, closeSnackbar: Function, - safe: Safe, } type ActiveScreen = 'checkOwner' | 'selectThreshold' | 'reviewRemoveOwner' @@ -50,9 +50,8 @@ export const sendRemoveOwner = async ( ownersOld: List, enqueueSnackbar: Function, closeSnackbar: Function, - createTransaction: Function, - removeSafeOwner: Function, - safe: Safe, + threshold: string, + dispatch: Function, ) => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const safeOwners = await gnosisSafe.getOwners() @@ -64,39 +63,30 @@ export const sendRemoveOwner = async ( .removeOwner(prevAddress, ownerAddressToRemove, values.threshold) .encodeABI() - const txHash = await createTransaction({ - safeAddress, - to: safeAddress, - valueInWei: 0, - txData, - notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - }) + const txHash = await dispatch( + createTransaction({ + safeAddress, + to: safeAddress, + valueInWei: 0, + txData, + notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, + enqueueSnackbar, + closeSnackbar, + }), + ) - if (txHash && safe.threshold === 1) { - removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove }) + if (txHash && threshold === 1) { + dispatch(removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove })) } } -const RemoveOwner = ({ - classes, - closeSnackbar, - createTransaction, - enqueueSnackbar, - isOpen, - onClose, - ownerAddress, - ownerName, - owners, - removeSafeOwner, - safe, - safeAddress, - safeName, - threshold, -}: Props) => { +const RemoveOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, ownerAddress, ownerName }: Props) => { const [activeScreen, setActiveScreen] = useState('checkOwner') const [values, setValues] = useState({}) + const dispatch = useDispatch() + const owners = useSelector(safeOwnersSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const threshold = useSelector(safeThresholdSelector) useEffect( () => () => { @@ -134,9 +124,8 @@ const RemoveOwner = ({ owners, enqueueSnackbar, closeSnackbar, - createTransaction, - removeSafeOwner, - safe, + threshold, + dispatch, ) } @@ -153,13 +142,7 @@ const RemoveOwner = ({ )} {activeScreen === 'selectThreshold' && ( - + )} {activeScreen === 'reviewRemoveOwner' && ( )} diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx index fe080865..8de748b8 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.jsx @@ -3,8 +3,8 @@ import IconButton from '@material-ui/core/IconButton' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames' -import { List } from 'immutable' import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' import { styles } from './style' @@ -21,36 +21,25 @@ import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from '~/logic/contracts/saf import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { getWeb3 } from '~/logic/wallets/getWeb3' -import type { Owner } from '~/routes/safe/store/models/owner' +import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn' type Props = { onClose: () => void, classes: Object, - safeName: string, - owners: List, values: Object, ownerAddress: string, ownerName: string, onClickBack: Function, onSubmit: Function, - safeAddress: string, } -const ReviewRemoveOwner = ({ - classes, - onClickBack, - onClose, - onSubmit, - ownerAddress, - ownerName, - owners, - safeAddress, - safeName, - values, -}: Props) => { +const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddress, ownerName, values }: Props) => { const [gasCosts, setGasCosts] = useState('< 0.001') + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeName = useSelector(safeNameSelector) + const owners = useSelector(safeOwnersSelector) useEffect(() => { let isCurrent = true diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.jsx index 3d4d015d..7052743d 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.jsx @@ -3,8 +3,8 @@ import IconButton from '@material-ui/core/IconButton' import MenuItem from '@material-ui/core/MenuItem' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import { List } from 'immutable' import React from 'react' +import { useSelector } from 'react-redux' import { styles } from './style' @@ -18,7 +18,7 @@ import Col from '~/components/layout/Col' import Hairline from '~/components/layout/Hairline' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' -import type { Owner } from '~/routes/safe/store/models/owner' +import { safeOwnersSelector, safeThresholdSelector } from '~/routes/safe/store/selectors' export const REMOVE_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'remove-owner-threshold-next-btn' @@ -27,11 +27,11 @@ type Props = { onClickBack: Function, onClose: () => void, onSubmit: Function, - owners: List, - threshold: number, } -const ThresholdForm = ({ classes, onClickBack, onClose, onSubmit, owners, threshold }: Props) => { +const ThresholdForm = ({ classes, onClickBack, onClose, onSubmit }: Props) => { + const owners = useSelector(safeOwnersSelector) + const threshold = useSelector(safeThresholdSelector) const handleSubmit = (values) => { onSubmit(values) } diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx index c1f414a1..fdcdb5f1 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.jsx @@ -1,9 +1,8 @@ // @flow import { withStyles } from '@material-ui/core/styles' -import { List } from 'immutable' import { withSnackbar } from 'notistack' import React, { useEffect, useState } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import OwnerForm from './screens/OwnerForm' import ReviewReplaceOwner from './screens/Review' @@ -12,8 +11,9 @@ import Modal from '~/components/Modal' import { addOrUpdateAddressBookEntry } from '~/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' -import { type Owner } from '~/routes/safe/store/models/owner' -import type { Safe } from '~/routes/safe/store/models/safe' +import createTransaction from '~/routes/safe/store/actions/createTransaction' +import replaceSafeOwner from '~/routes/safe/store/actions/replaceSafeOwner' +import { safeParamAddressFromStateSelector, safeThresholdSelector } from '~/routes/safe/store/selectors' const styles = () => ({ biggerModalWindow: { @@ -28,17 +28,10 @@ type Props = { onClose: () => void, classes: Object, isOpen: boolean, - safeAddress: string, - safeName: string, - ownerAddress: string, - ownerName: string, - owners: List, - threshold: string, - createTransaction: Function, - replaceSafeOwner: Function, enqueueSnackbar: Function, closeSnackbar: Function, - safe: Safe, + ownerAddress: string, + ownerName: string, } type ActiveScreen = 'checkOwner' | 'reviewReplaceOwner' @@ -48,9 +41,8 @@ export const sendReplaceOwner = async ( ownerAddressToRemove: string, enqueueSnackbar: Function, closeSnackbar: Function, - createTransaction: Function, - replaceSafeOwner: Function, - safe: Safe, + threshold: string, + dispatch: Function, ) => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const safeOwners = await gnosisSafe.getOwners() @@ -62,45 +54,36 @@ export const sendReplaceOwner = async ( .swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress) .encodeABI() - const txHash = await createTransaction({ - safeAddress, - to: safeAddress, - valueInWei: 0, - txData, - notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - }) - - if (txHash && safe.threshold === 1) { - replaceSafeOwner({ + const txHash = await dispatch( + createTransaction({ safeAddress, - oldOwnerAddress: ownerAddressToRemove, - ownerAddress: values.ownerAddress, - ownerName: values.ownerName, - }) + to: safeAddress, + valueInWei: 0, + txData, + notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, + enqueueSnackbar, + closeSnackbar, + }), + ) + + if (txHash && threshold === 1) { + dispatch( + replaceSafeOwner({ + safeAddress, + oldOwnerAddress: ownerAddressToRemove, + ownerAddress: values.ownerAddress, + ownerName: values.ownerName, + }), + ) } } -const ReplaceOwner = ({ - classes, - closeSnackbar, - createTransaction, - enqueueSnackbar, - isOpen, - onClose, - ownerAddress, - ownerName, - owners, - replaceSafeOwner, - safe, - safeAddress, - safeName, - threshold, -}: Props) => { - const dispatch = useDispatch() +const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, ownerAddress, ownerName }: Props) => { const [activeScreen, setActiveScreen] = useState('checkOwner') const [values, setValues] = useState({}) + const dispatch = useDispatch() + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const threshold = useSelector(safeThresholdSelector) useEffect( () => () => { @@ -121,18 +104,8 @@ const ReplaceOwner = ({ const onReplaceOwner = async () => { onClose() - try { - await sendReplaceOwner( - values, - safeAddress, - ownerAddress, - enqueueSnackbar, - closeSnackbar, - createTransaction, - replaceSafeOwner, - safe, - ) + await sendReplaceOwner(values, safeAddress, ownerAddress, enqueueSnackbar, closeSnackbar, threshold, dispatch) dispatch( // Needs the `address` field because we need to provide the minimum required values to ADD a new entry @@ -155,13 +128,7 @@ const ReplaceOwner = ({ > <> {activeScreen === 'checkOwner' && ( - + )} {activeScreen === 'reviewReplaceOwner' && ( )} diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx index 13f876b3..34344b16 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx @@ -3,8 +3,8 @@ import IconButton from '@material-ui/core/IconButton' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames/bind' -import { List } from 'immutable' import React from 'react' +import { useSelector } from 'react-redux' import { styles } from './style' @@ -22,7 +22,7 @@ import Col from '~/components/layout/Col' import Hairline from '~/components/layout/Hairline' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' -import { type Owner } from '~/routes/safe/store/models/owner' +import { safeOwnersSelector } from '~/routes/safe/store/selectors' export const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input' export const REPLACE_OWNER_ADDRESS_INPUT_TEST_ID = 'replace-owner-address-testid' @@ -40,13 +40,13 @@ type Props = { ownerAddress: string, ownerName: string, onSubmit: Function, - owners: List, } -const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName, owners }: Props) => { +const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }: Props) => { const handleSubmit = (values) => { onSubmit(values) } + const owners = useSelector(safeOwnersSelector) const ownerDoesntExist = uniqueAddress(owners.map((o) => o.address)) return ( diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.jsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.jsx index 9b1588ee..953a8c67 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.jsx @@ -3,8 +3,8 @@ import IconButton from '@material-ui/core/IconButton' import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames' -import { List } from 'immutable' import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' import { styles } from './style' @@ -21,38 +21,33 @@ import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from '~/logic/contracts/saf import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { getWeb3 } from '~/logic/wallets/getWeb3' -import type { Owner } from '~/routes/safe/store/models/owner' +import type { Safe } from '~/routes/safe/store/models/safe' +import { + safeNameSelector, + safeOwnersSelector, + safeParamAddressFromStateSelector, + safeThresholdSelector, +} from '~/routes/safe/store/selectors' export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn' type Props = { onClose: () => void, classes: Object, - safeName: string, - owners: List, values: Object, ownerAddress: string, ownerName: string, onClickBack: Function, onSubmit: Function, - threshold: string, - safeAddress: string, + safe: Safe, } -const ReviewRemoveOwner = ({ - classes, - onClickBack, - onClose, - onSubmit, - ownerAddress, - ownerName, - owners, - safeAddress, - safeName, - threshold, - values, -}: Props) => { +const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddress, ownerName, values }: Props) => { const [gasCosts, setGasCosts] = useState('< 0.001') + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeName = useSelector(safeNameSelector) + const owners = useSelector(safeOwnersSelector) + const threshold = useSelector(safeThresholdSelector) useEffect(() => { let isCurrent = true diff --git a/src/routes/safe/components/Settings/ManageOwners/index.jsx b/src/routes/safe/components/Settings/ManageOwners/index.jsx index 9b3876ef..2b6a56ad 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.jsx @@ -38,7 +38,6 @@ import Row from '~/components/layout/Row' import type { AddressBook } from '~/logic/addressBook/model/addressBook' import { getOwnersWithNameFromAddressBook } from '~/logic/addressBook/utils' import type { Owner } from '~/routes/safe/store/models/owner' -import type { Safe } from '~/routes/safe/store/models/safe' export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn' export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn' @@ -48,21 +47,9 @@ export const OWNERS_ROW_TEST_ID = 'owners-row' type Props = { classes: Object, - safeAddress: string, - safeName: string, owners: List, - network: string, - threshold: number, - userAddress: string, - createTransaction: Function, - addSafeOwner: Function, - removeSafeOwner: Function, - replaceSafeOwner: Function, - editSafeOwner: Function, - granted: boolean, - safe: Safe, addressBook: AddressBook, - updateAddressBookEntry: Function, + granted: boolean, } type State = { @@ -107,24 +94,7 @@ class ManageOwners extends React.Component { } render() { - const { - addSafeOwner, - addressBook, - classes, - createTransaction, - editSafeOwner, - granted, - network, - owners, - removeSafeOwner, - replaceSafeOwner, - safe, - safeAddress, - safeName, - threshold, - updateAddressBookEntry, - userAddress, - } = this.props + const { addressBook, classes, granted, owners } = this.props const { selectedOwnerAddress, selectedOwnerName, @@ -133,7 +103,6 @@ class ManageOwners extends React.Component { showRemoveOwner, showReplaceOwner, } = this.state - const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook, owners) @@ -232,55 +201,24 @@ class ManageOwners extends React.Component { )} - + ) diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/index.jsx b/src/routes/safe/components/Settings/RemoveSafeModal/index.jsx index 9c2ac62e..2261407f 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/index.jsx +++ b/src/routes/safe/components/Settings/RemoveSafeModal/index.jsx @@ -5,9 +5,9 @@ import Close from '@material-ui/icons/Close' import OpenInNew from '@material-ui/icons/OpenInNew' import classNames from 'classnames' import React from 'react' -import { connect } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' -import actions, { type Actions } from './actions' +import { type Actions } from './actions' import { styles } from './style' import Identicon from '~/components/Identicon' @@ -19,7 +19,10 @@ import Hairline from '~/components/layout/Hairline' import Link from '~/components/layout/Link' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' +import { getEtherScanLink } from '~/logic/wallets/getWeb3' import { SAFELIST_ADDRESS } from '~/routes/routes' +import removeSafe from '~/routes/safe/store/actions/removeSafe' +import { safeNameSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' import { history } from '~/store' import { md, secondary } from '~/theme/variables' @@ -32,79 +35,81 @@ type Props = Actions & { onClose: () => void, classes: Object, isOpen: boolean, - safeAddress: string, - etherScanLink: string, - safeName: string, } -const RemoveSafeComponent = ({ classes, etherScanLink, isOpen, onClose, removeSafe, safeAddress, safeName }: Props) => ( - - - - Remove Safe - - - - - - - - - - - - - - - {safeName} - - - - {safeAddress} - - - - - - - +const RemoveSafeComponent = ({ classes, isOpen, onClose }: Props) => { + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeName = useSelector(safeNameSelector) + const dispatch = useDispatch() + const etherScanLink = getEtherScanLink('address', safeAddress) + + return ( + + + + Remove Safe + + + + - - - Removing a Safe only removes it from your interface. It does not delete the Safe. You can always add it - back using the Safe's address. - + + + + + + + + + {safeName} + + + + {safeAddress} + + + + + + + + + + + + Removing a Safe only removes it from your interface. It does not delete the Safe. You can always add + it back using the Safe's address. + + + + + + + - - - - - - - -) + + ) +} -const RemoveSafeModal = withStyles(styles)(RemoveSafeComponent) - -export default connect(undefined, actions)(RemoveSafeModal) +export const RemoveSafeModal = withStyles(styles)(RemoveSafeComponent) diff --git a/src/routes/safe/components/Settings/SafeDetails/index.jsx b/src/routes/safe/components/Settings/SafeDetails/index.jsx index ef30a51c..04a6e9a5 100644 --- a/src/routes/safe/components/Settings/SafeDetails/index.jsx +++ b/src/routes/safe/components/Settings/SafeDetails/index.jsx @@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles' import { withSnackbar } from 'notistack' import React from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' @@ -21,20 +21,21 @@ import { getNotificationsFromTxType, showSnackbar } from '~/logic/notifications' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import UpdateSafeModal from '~/routes/safe/components/Settings/UpdateSafeModal' import { grantedSelector } from '~/routes/safe/container/selector' -import { latestMasterContractVersionSelector } from '~/routes/safe/store/selectors' +import updateSafe from '~/routes/safe/store/actions/updateSafe' +import { + latestMasterContractVersionSelector, + safeCurrentVersionSelector, + safeNameSelector, + safeNeedsUpdateSelector, + safeParamAddressFromStateSelector, +} from '~/routes/safe/store/selectors' export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input' export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn' export const SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID = 'update-safe-name-btn' type Props = { - safeAddress: string, - safeCurrentVersion: string, - safeName: string, - safeNeedsUpdate: boolean, - updateSafe: Function, enqueueSnackbar: Function, - createTransaction: Function, closeSnackbar: Function, } @@ -44,24 +45,21 @@ const SafeDetails = (props: Props) => { const classes = useStyles() const isUserOwner = useSelector(grantedSelector) const latestMasterContractVersion = useSelector(latestMasterContractVersionSelector) - const { - closeSnackbar, - enqueueSnackbar, - safeAddress, - safeCurrentVersion, - safeName, - safeNeedsUpdate, - updateSafe, - } = props + const dispatch = useDispatch() + const safeName = useSelector(safeNameSelector) + const safeNeedsUpdate = useSelector(safeNeedsUpdateSelector) + const safeCurrentVersion = useSelector(safeCurrentVersionSelector) + const { closeSnackbar, enqueueSnackbar } = props const [isModalOpen, setModalOpen] = React.useState(false) + const safeAddress = useSelector(safeParamAddressFromStateSelector) const toggleModal = () => { setModalOpen((prevOpen) => !prevOpen) } const handleSubmit = (values) => { - updateSafe({ address: safeAddress, name: values.safeName }) + dispatch(updateSafe({ address: safeAddress, name: values.safeName })) const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX) showSnackbar(notification.afterExecution.noMoreConfirmationsNeeded, enqueueSnackbar, closeSnackbar) diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.jsx b/src/routes/safe/components/Settings/ThresholdSettings/index.jsx index bd364365..aa5023d5 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.jsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.jsx @@ -1,8 +1,8 @@ // @flow import { withStyles } from '@material-ui/core/styles' -import { List } from 'immutable' import { withSnackbar } from 'notistack' import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import ChangeThreshold from './ChangeThreshold' import { styles } from './style' @@ -16,30 +16,27 @@ import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' -import type { Owner } from '~/routes/safe/store/models/owner' +import { grantedSelector } from '~/routes/safe/container/selector' +import createTransaction from '~/routes/safe/store/actions/createTransaction' +import { + safeOwnersSelector, + safeParamAddressFromStateSelector, + safeThresholdSelector, +} from '~/routes/safe/store/selectors' type Props = { - owners: List, - threshold: number, classes: Object, - createTransaction: Function, - safeAddress: string, - granted: boolean, enqueueSnackbar: Function, closeSnackbar: Function, } -const ThresholdSettings = ({ - classes, - closeSnackbar, - createTransaction, - enqueueSnackbar, - granted, - owners, - safeAddress, - threshold, -}: Props) => { +const ThresholdSettings = ({ classes, closeSnackbar, enqueueSnackbar }: Props) => { const [isModalOpen, setModalOpen] = useState(false) + const dispatch = useDispatch() + const threshold = useSelector(safeThresholdSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const owners = useSelector(safeOwnersSelector) + const granted = useSelector(grantedSelector) const toggleModal = () => { setModalOpen((prevOpen) => !prevOpen) @@ -49,15 +46,17 @@ const ThresholdSettings = ({ const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI() - createTransaction({ - safeAddress, - to: safeAddress, - valueInWei: 0, - txData, - notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - }) + dispatch( + createTransaction({ + safeAddress, + to: safeAddress, + valueInWei: 0, + txData, + notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, + enqueueSnackbar, + closeSnackbar, + }), + ) } return ( diff --git a/src/routes/safe/components/Settings/actions.js b/src/routes/safe/components/Settings/actions.js deleted file mode 100644 index 686d5d16..00000000 --- a/src/routes/safe/components/Settings/actions.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import addSafeOwner from '~/routes/safe/store/actions/addSafeOwner' -import editSafeOwner from '~/routes/safe/store/actions/editSafeOwner' -import removeSafeOwner from '~/routes/safe/store/actions/removeSafeOwner' -import replaceSafeOwner from '~/routes/safe/store/actions/replaceSafeOwner' - -export type Actions = { - addSafeOwner: Function, - removeSafeOwner: Function, - replaceSafeOwner: Function, - editSafeOwner: Function, -} - -export default { - addSafeOwner, - removeSafeOwner, - replaceSafeOwner, - editSafeOwner, -} diff --git a/src/routes/safe/components/Settings/index.jsx b/src/routes/safe/components/Settings/index.jsx index 73d734b5..cf29994f 100644 --- a/src/routes/safe/components/Settings/index.jsx +++ b/src/routes/safe/components/Settings/index.jsx @@ -2,21 +2,21 @@ import Badge from '@material-ui/core/Badge' import { withStyles } from '@material-ui/core/styles' import cn from 'classnames' -import { List } from 'immutable' import * as React from 'react' -import { connect } from 'react-redux' +import { useState } from 'react' +import { useSelector } from 'react-redux' import ManageOwners from './ManageOwners' -import RemoveSafeModal from './RemoveSafeModal' +import { RemoveSafeModal } from './RemoveSafeModal' import SafeDetails from './SafeDetails' import ThresholdSettings from './ThresholdSettings' -import actions, { type Actions } from './actions' import { OwnersIcon } from './assets/icons/OwnersIcon' import { RequiredConfirmationsIcon } from './assets/icons/RequiredConfirmationsIcon' import { SafeDetailsIcon } from './assets/icons/SafeDetailsIcon' import RemoveSafeIcon from './assets/icons/bin.svg' import { styles } from './style' +import Loader from '~/components/Loader' import Block from '~/components/layout/Block' import ButtonLink from '~/components/layout/ButtonLink' import Col from '~/components/layout/Col' @@ -25,189 +25,101 @@ import Img from '~/components/layout/Img' import Paragraph from '~/components/layout/Paragraph' import Row from '~/components/layout/Row' import Span from '~/components/layout/Span' -import type { AddressBook } from '~/logic/addressBook/model/addressBook' -import { type Owner } from '~/routes/safe/store/models/owner' -import type { Safe } from '~/routes/safe/store/models/safe' +import { getAddressBook } from '~/logic/addressBook/store/selectors' +import { safeNeedsUpdate } from '~/logic/safe/utils/safeVersion' +import { grantedSelector } from '~/routes/safe/container/selector' +import { safeOwnersSelector } from '~/routes/safe/store/selectors' export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab' -type State = { - showRemoveSafe: boolean, - menuOptionIndex: number, -} - -type Props = Actions & { - addSafeOwner: Function, - addressBook: AddressBook, +type Props = { classes: Object, - createTransaction: Function, - editSafeOwner: Function, - etherScanLink: string, - granted: boolean, - network: string, - owners: List, - removeSafeOwner: Function, - replaceSafeOwner: Function, - safe: Safe, - safeAddress: string, - safeName: string, - threshold: number, - updateAddressBookEntry: Function, - updateSafe: Function, - userAddress: string, +} +const INITIAL_STATE = { + showRemoveSafe: false, + menuOptionIndex: 1, } type Action = 'RemoveSafe' -class Settings extends React.Component { - constructor(props) { - super(props) +const Settings = (props: Props) => { + const [state, setState] = useState(INITIAL_STATE) + const owners = useSelector(safeOwnersSelector) + const needsUpdate = useSelector(safeNeedsUpdate) + const granted = useSelector(grantedSelector) + const addressBook = useSelector(getAddressBook) - this.state = { - showRemoveSafe: false, - menuOptionIndex: 1, - } + const handleChange = (menuOptionIndex) => () => { + setState((prevState) => ({ ...prevState, menuOptionIndex })) } - handleChange = (menuOptionIndex) => () => { - this.setState({ menuOptionIndex }) + const onShow = (action: Action) => () => { + setState((prevState) => ({ ...prevState, [`show${action}`]: true })) } - onShow = (action: Action) => () => { - this.setState(() => ({ [`show${action}`]: true })) + const onHide = (action: Action) => () => { + setState((prevState) => ({ ...prevState, [`show${action}`]: false })) } - onHide = (action: Action) => () => { - this.setState(() => ({ [`show${action}`]: false })) - } + const { menuOptionIndex, showRemoveSafe } = state + const { classes } = props - render() { - const { menuOptionIndex, showRemoveSafe } = this.state - const { - addSafeOwner, - addressBook, - classes, - createTransaction, - editSafeOwner, - etherScanLink, - granted, - network, - owners, - removeSafeOwner, - replaceSafeOwner, - safe, - safeAddress, - safeName, - threshold, - updateAddressBookEntry, - updateSafe, - userAddress, - } = this.props - - return ( - <> - - - Remove Safe - Trash Icon - - - - - - - + ) : ( + <> + + + Remove Safe + Trash Icon + + + + + + + + + - - - Safe details - - - - - - Owners - - {owners.size} - - - - - - Policies - - - - - - - {menuOptionIndex === 1 && ( - - )} - {menuOptionIndex === 2 && ( - - )} - {menuOptionIndex === 3 && ( - - )} - - - - - ) - } + Safe details + + + + + + Owners + + {owners.size} + + + + + + Policies + + + + + + + {menuOptionIndex === 1 && } + {menuOptionIndex === 2 && } + {menuOptionIndex === 3 && } + + + + + ) } -const settingsComponent = withStyles(styles)(Settings) - -export default connect(undefined, actions)(settingsComponent) +export default withStyles(styles)(Settings) diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx index fbde98e1..472637fe 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx @@ -6,6 +6,7 @@ import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import { withSnackbar } from 'notistack' import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' @@ -20,7 +21,10 @@ import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { getWeb3 } from '~/logic/wallets/getWeb3' +import { userAccountSelector } from '~/logic/wallets/store/selectors' +import processTransaction from '~/routes/safe/store/actions/processTransaction' import { type Transaction } from '~/routes/safe/store/models/transaction' +import { safeParamAddressFromStateSelector, safeThresholdSelector } from '~/routes/safe/store/selectors' export const APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID = 'approve-tx-modal-submit-btn' export const REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID = 'reject-tx-modal-submit-btn' @@ -30,13 +34,8 @@ type Props = { classes: Object, isOpen: boolean, isCancelTx?: boolean, - processTransaction: Function, tx: Transaction, - nonce: string, - safeAddress: string, - threshold: number, thresholdReached: boolean, - userAddress: string, canExecute: boolean, enqueueSnackbar: Function, closeSnackbar: Function, @@ -73,13 +72,13 @@ const ApproveTxModal = ({ isCancelTx, isOpen, onClose, - processTransaction, - safeAddress, - threshold, thresholdReached, tx, - userAddress, }: Props) => { + const dispatch = useDispatch() + const userAddress = useSelector(userAccountSelector) + const threshold = useSelector(safeThresholdSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) const [approveAndExecute, setApproveAndExecute] = useState(canExecute) const [gasCosts, setGasCosts] = useState('< 0.001') const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx) @@ -117,15 +116,17 @@ const ApproveTxModal = ({ const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute) const approveTx = () => { - processTransaction({ - safeAddress, - tx, - userAddress, - notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX, - enqueueSnackbar, - closeSnackbar, - approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted, - }) + dispatch( + processTransaction({ + safeAddress, + tx, + userAddress, + notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX, + enqueueSnackbar, + closeSnackbar, + approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted, + }), + ) onClose() } diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.jsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.jsx index 054e8234..524e29fb 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.jsx @@ -1,8 +1,8 @@ // @flow import { withStyles } from '@material-ui/core/styles' import cn from 'classnames' -import { List } from 'immutable' import React from 'react' +import { useSelector } from 'react-redux' import OwnersList from './OwnersList' import CheckLargeFilledGreenCircle from './assets/check-large-filled-green.svg' @@ -17,8 +17,9 @@ import Col from '~/components/layout/Col' import Img from '~/components/layout/Img' import Paragraph from '~/components/layout/Paragraph/index' import { TX_TYPE_CONFIRMATION } from '~/logic/safe/transactions/send' -import { type Owner } from '~/routes/safe/store/models/owner' +import { userAccountSelector } from '~/logic/wallets/store/selectors' import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction' +import { safeOwnersSelector, safeThresholdSelector } from '~/routes/safe/store/selectors' type Props = { canExecute: boolean, @@ -29,11 +30,8 @@ type Props = { onTxReject: Function, onTxConfirm: Function, onTxExecute: Function, - owners: List, - threshold: number, thresholdReached: boolean, tx: Transaction, - userAddress: string, } function getOwnersConfirmations(tx, userAddress) { @@ -71,10 +69,7 @@ function getPendingOwnersConfirmations(owners, tx, userAddress) { const OwnersColumn = ({ tx, cancelTx = makeTransaction(), - owners, classes, - threshold, - userAddress, thresholdReached, cancelThresholdReached, onTxConfirm, @@ -90,7 +85,9 @@ const OwnersColumn = ({ } else { showOlderTxAnnotation = (thresholdReached && !canExecute) || (cancelThresholdReached && !canExecuteCancel) } - + const owners = useSelector(safeOwnersSelector) + const threshold = useSelector(safeThresholdSelector) + const userAddress = useSelector(userAccountSelector) const [ownersWhoConfirmed, currentUserAlreadyConfirmed] = getOwnersConfirmations(tx, userAddress) const [ownersUnconfirmed, userIsUnconfirmedOwner] = getPendingOwnersConfirmations(owners, tx, userAddress) const [ownersWhoConfirmedCancel, currentUserAlreadyConfirmedCancel] = getOwnersConfirmations(cancelTx, userAddress) diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/style.js b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/style.js index 1e91c63b..3250f993 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/style.js +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/style.js @@ -19,7 +19,7 @@ export const styles = () => ({ position: 'absolute', top: '-27px', width: '2px', - zIndex: '10', + zIndex: '12', }, verticalLinePending: { backgroundColor: secondaryText, @@ -78,7 +78,7 @@ export const styles = () => ({ justifyContent: 'center', marginRight: '18px', width: '20px', - zIndex: '100', + zIndex: '13', '& > img': { display: 'block', diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.jsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.jsx index 0a408207..980e1f95 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.jsx @@ -4,6 +4,7 @@ import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import { withSnackbar } from 'notistack' import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' @@ -19,31 +20,23 @@ import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew' import { formatAmount } from '~/logic/tokens/utils/formatAmount' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { getWeb3 } from '~/logic/wallets/getWeb3' +import createTransaction from '~/routes/safe/store/actions/createTransaction' import { type Transaction } from '~/routes/safe/store/models/transaction' +import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' type Props = { onClose: () => void, classes: Object, isOpen: boolean, - createTransaction: Function, tx: Transaction, - safeAddress: string, enqueueSnackbar: Function, closeSnackbar: Function, } -const RejectTxModal = ({ - classes, - closeSnackbar, - createTransaction, - enqueueSnackbar, - isOpen, - onClose, - safeAddress, - tx, -}: Props) => { +const RejectTxModal = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, tx }: Props) => { const [gasCosts, setGasCosts] = useState('< 0.001') - + const dispatch = useDispatch() + const safeAddress = useSelector(safeParamAddressFromStateSelector) useEffect(() => { let isCurrent = true const estimateGasCosts = async () => { @@ -66,16 +59,18 @@ const RejectTxModal = ({ }, []) const sendReplacementTransaction = () => { - createTransaction({ - safeAddress, - to: safeAddress, - valueInWei: 0, - notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX, - enqueueSnackbar, - closeSnackbar, - txNonce: tx.nonce, - origin: tx.origin, - }) + dispatch( + createTransaction({ + safeAddress, + to: safeAddress, + valueInWei: 0, + notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX, + enqueueSnackbar, + closeSnackbar, + txNonce: tx.nonce, + origin: tx.origin, + }), + ) onClose() } diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.jsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.jsx index 3fdc384a..fdaaeee3 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.jsx @@ -1,8 +1,8 @@ // @flow import { makeStyles } from '@material-ui/core/styles' import cn from 'classnames' -import { List } from 'immutable' import React, { useState } from 'react' +import { useSelector } from 'react-redux' import { formatDate } from '../columns' @@ -22,39 +22,22 @@ import Row from '~/components/layout/Row' import Span from '~/components/layout/Span' import IncomingTxDescription from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/IncomingTxDescription' import { INCOMING_TX_TYPES } from '~/routes/safe/store/models/incomingTransaction' -import { type Owner } from '~/routes/safe/store/models/owner' import { type Transaction } from '~/routes/safe/store/models/transaction' +import { safeNonceSelector, safeThresholdSelector } from '~/routes/safe/store/selectors' type Props = { tx: Transaction, cancelTx: Transaction, - threshold: number, - owners: List, - granted: boolean, - userAddress: string, - safeAddress: string, - createTransaction: Function, - processTransaction: Function, - nonce: number, } type OpenModal = 'rejectTx' | 'approveTx' | 'executeRejectTx' | null const useStyles = makeStyles(styles) -const ExpandedTx = ({ - cancelTx, - createTransaction, - granted, - nonce, - owners, - processTransaction, - safeAddress, - threshold, - tx, - userAddress, -}: Props) => { +const ExpandedTx = ({ cancelTx, tx }: Props) => { const classes = useStyles() + const nonce = useSelector(safeNonceSelector) + const threshold = useSelector(safeThresholdSelector) const [openModal, setOpenModal] = useState(null) const openApproveModal = () => setOpenModal('approveTx') const closeModal = () => setOpenModal(null) @@ -138,16 +121,11 @@ const ExpandedTx = ({ cancelTx={cancelTx} canExecute={canExecute} canExecuteCancel={canExecuteCancel} - granted={granted} onTxConfirm={openApproveModal} onTxExecute={openApproveModal} onTxReject={openRejectModal} - owners={owners} - safeAddress={safeAddress} - threshold={threshold} thresholdReached={thresholdReached} tx={tx} - userAddress={userAddress} /> )} @@ -157,35 +135,19 @@ const ExpandedTx = ({ canExecute={canExecute} isOpen onClose={closeModal} - processTransaction={processTransaction} - safeAddress={safeAddress} - threshold={threshold} thresholdReached={thresholdReached} tx={tx} - userAddress={userAddress} - /> - )} - {openModal === 'rejectTx' && ( - )} + {openModal === 'rejectTx' && } {openModal === 'executeRejectTx' && ( )} diff --git a/src/routes/safe/components/Transactions/TxsTable/index.jsx b/src/routes/safe/components/Transactions/TxsTable/index.jsx index 6745c643..f1358261 100644 --- a/src/routes/safe/components/Transactions/TxsTable/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/index.jsx @@ -8,8 +8,8 @@ import { withStyles } from '@material-ui/core/styles' import ExpandLess from '@material-ui/icons/ExpandLess' import ExpandMore from '@material-ui/icons/ExpandMore' import cn from 'classnames' -import { List } from 'immutable' import React, { useState } from 'react' +import { useSelector } from 'react-redux' import ExpandedTxComponent from './ExpandedTx' import Status from './Status' @@ -27,9 +27,8 @@ import Table from '~/components/Table' import { type Column, cellWidth } from '~/components/Table/TableHead' import Block from '~/components/layout/Block' import Row from '~/components/layout/Row' -import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction' -import { type Owner } from '~/routes/safe/store/models/owner' -import { type Transaction } from '~/routes/safe/store/models/transaction' +import { extendedTransactionsSelector } from '~/routes/safe/container/selector' +import { safeCancellationTransactionsSelector } from '~/routes/safe/store/selectors' export const TRANSACTION_ROW_TEST_ID = 'transaction-row' @@ -40,32 +39,12 @@ const expandCellStyle = { type Props = { classes: Object, - transactions: List, - cancellationTransactions: List, - threshold: number, - owners: List, - userAddress: string, - granted: boolean, - safeAddress: string, - nonce: number, - createTransaction: Function, - processTransaction: Function, } -const TxsTable = ({ - cancellationTransactions, - classes, - createTransaction, - granted, - nonce, - owners, - processTransaction, - safeAddress, - threshold, - transactions, - userAddress, -}: Props) => { +const TxsTable = ({ classes }: Props) => { const [expandedTx, setExpandedTx] = useState(null) + const cancellationTransactions = useSelector(safeCancellationTransactionsSelector) + const transactions = useSelector(extendedTransactionsSelector) const handleTxExpand = (safeTxHash) => { setExpandedTx((prevTx) => (prevTx === safeTxHash ? null : safeTxHash)) @@ -156,18 +135,10 @@ const TxsTable = ({ diff --git a/src/routes/safe/components/Transactions/index.jsx b/src/routes/safe/components/Transactions/index.jsx deleted file mode 100644 index 386df602..00000000 --- a/src/routes/safe/components/Transactions/index.jsx +++ /dev/null @@ -1,52 +0,0 @@ -// @flow -import { List } from 'immutable' -import React from 'react' - -import TxsTable from '~/routes/safe/components/Transactions/TxsTable' -import { type IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction' -import { type Owner } from '~/routes/safe/store/models/owner' -import { type Transaction } from '~/routes/safe/store/models/transaction' - -type Props = { - safeAddress: string, - threshold: number, - transactions: List, - cancellationTransactions: List, - owners: List, - userAddress: string, - granted: boolean, - createTransaction: Function, - processTransaction: Function, - currentNetwork: string, - nonce: number, -} - -const Transactions = ({ - transactions = List(), - cancellationTransactions = List(), - owners, - threshold, - userAddress, - granted, - safeAddress, - createTransaction, - processTransaction, - currentNetwork, - nonce, -}: Props) => ( - -) - -export default Transactions diff --git a/src/routes/safe/container/Hooks/useCheckForUpdates.jsx b/src/routes/safe/container/Hooks/useCheckForUpdates.jsx new file mode 100644 index 00000000..6cfdc4bb --- /dev/null +++ b/src/routes/safe/container/Hooks/useCheckForUpdates.jsx @@ -0,0 +1,32 @@ +// @flow +import { useEffect } from 'react' +import { batch, useDispatch, useSelector } from 'react-redux' + +import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles' +import fetchSafeTokens from '~/logic/tokens/store/actions/fetchSafeTokens' +import fetchEtherBalance from '~/routes/safe/store/actions/fetchEtherBalance' +import { checkAndUpdateSafe } from '~/routes/safe/store/actions/fetchSafe' +import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' +import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' +import { TIMEOUT } from '~/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) + } + } + }, [safeAddress]) +} diff --git a/src/routes/safe/container/Hooks/useFetchTokens.jsx b/src/routes/safe/container/Hooks/useFetchTokens.jsx new file mode 100644 index 00000000..7065bf0e --- /dev/null +++ b/src/routes/safe/container/Hooks/useFetchTokens.jsx @@ -0,0 +1,30 @@ +// @flow +import { useMemo } from 'react' +import { batch, useDispatch, useSelector } from 'react-redux' + +import { fetchCurrencyValues } from '~/logic/currencyValues/store/actions/fetchCurrencyValues' +import activateAssetsByBalance from '~/logic/tokens/store/actions/activateAssetsByBalance' +import fetchSafeTokens from '~/logic/tokens/store/actions/fetchSafeTokens' +import { fetchTokens } from '~/logic/tokens/store/actions/fetchTokens' +import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from '~/routes/safe/components/Balances' +import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' +import { history } from '~/store' + +export const useFetchTokens = () => { + const dispatch = useDispatch() + const address = useSelector(safeParamAddressFromStateSelector) + useMemo(() => { + if (COINS_LOCATION_REGEX.test(history.location.pathname)) { + batch(() => { + // fetch tokens there to get symbols for tokens in TXs list + dispatch(fetchTokens()) + dispatch(fetchCurrencyValues(address)) + dispatch(fetchSafeTokens(address)) + }) + } + + if (COLLECTIBLES_LOCATION_REGEX.test(history.location.pathname)) { + dispatch(activateAssetsByBalance(address)) + } + }, [history.location.pathname]) +} diff --git a/src/routes/safe/container/Hooks/useLoadSafe.jsx b/src/routes/safe/container/Hooks/useLoadSafe.jsx new file mode 100644 index 00000000..31c12110 --- /dev/null +++ b/src/routes/safe/container/Hooks/useLoadSafe.jsx @@ -0,0 +1,30 @@ +// @flow +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' + +import loadAddressBookFromStorage from '~/logic/addressBook/store/actions/loadAddressBookFromStorage' +import addViewedSafe from '~/logic/currentSession/store/actions/addViewedSafe' +import fetchSafeTokens from '~/logic/tokens/store/actions/fetchSafeTokens' +import fetchLatestMasterContractVersion from '~/routes/safe/store/actions/fetchLatestMasterContractVersion' +import fetchSafe from '~/routes/safe/store/actions/fetchSafe' +import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' + +export const useLoadSafe = (safeAddress: ?string) => { + const dispatch = useDispatch() + + useEffect(() => { + const fetchData = () => { + if (safeAddress) { + dispatch(fetchLatestMasterContractVersion()) + .then(() => dispatch(fetchSafe(safeAddress))) + .then(() => { + dispatch(fetchSafeTokens(safeAddress)) + dispatch(loadAddressBookFromStorage()) + return dispatch(fetchTransactions(safeAddress)) + }) + .then(() => dispatch(addViewedSafe(safeAddress))) + } + } + fetchData() + }, [safeAddress]) +} diff --git a/src/routes/safe/container/actions.js b/src/routes/safe/container/actions.js deleted file mode 100644 index 4118b9fb..00000000 --- a/src/routes/safe/container/actions.js +++ /dev/null @@ -1,57 +0,0 @@ -// @flow -import loadAddressBookFromStorage from '~/logic/addressBook/store/actions/loadAddressBookFromStorage' -import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/updateAddressBookEntry' -import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles' -import fetchCurrencyValues from '~/logic/currencyValues/store/actions/fetchCurrencyValues' -import addViewedSafe from '~/logic/currentSession/store/actions/addViewedSafe' -import activateAssetsByBalance from '~/logic/tokens/store/actions/activateAssetsByBalance' -import activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance' -import fetchTokens from '~/logic/tokens/store/actions/fetchTokens' -import createTransaction from '~/routes/safe/store/actions/createTransaction' -import fetchEtherBalance from '~/routes/safe/store/actions/fetchEtherBalance' -import fetchLatestMasterContractVersion from '~/routes/safe/store/actions/fetchLatestMasterContractVersion' -import fetchSafe, { checkAndUpdateSafe } from '~/routes/safe/store/actions/fetchSafe' -import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances' -import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' -import processTransaction from '~/routes/safe/store/actions/processTransaction' -import updateSafe from '~/routes/safe/store/actions/updateSafe' - -export type Actions = { - fetchSafe: typeof fetchSafe, - fetchTokenBalances: typeof fetchTokenBalances, - createTransaction: typeof createTransaction, - fetchTransactions: typeof fetchTransactions, - updateSafe: typeof updateSafe, - fetchCollectibles: typeof fetchCollectibles, - fetchTokens: typeof fetchTokens, - processTransaction: typeof processTransaction, - fetchEtherBalance: typeof fetchEtherBalance, - fetchLatestMasterContractVersion: typeof fetchLatestMasterContractVersion, - activateTokensByBalance: typeof activateTokensByBalance, - activateAssetsByBalance: typeof activateAssetsByBalance, - checkAndUpdateSafe: typeof checkAndUpdateSafe, - fetchCurrencyValues: typeof fetchCurrencyValues, - loadAddressBook: typeof loadAddressBookFromStorage, - updateAddressBookEntry: typeof updateAddressBookEntry, - addViewedSafe: typeof addViewedSafe, -} - -export default { - fetchSafe, - fetchTokenBalances, - createTransaction, - processTransaction, - fetchCollectibles, - fetchTokens, - fetchTransactions, - activateTokensByBalance, - activateAssetsByBalance, - updateSafe, - fetchEtherBalance, - fetchLatestMasterContractVersion, - fetchCurrencyValues, - checkAndUpdateSafe, - loadAddressBook: loadAddressBookFromStorage, - updateAddressBookEntry, - addViewedSafe, -} diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index 8f2cce7b..f4787a13 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -1,199 +1,79 @@ // @flow import * as React from 'react' -import { connect } from 'react-redux' - -import actions, { type Actions } from './actions' -import selector, { type SelectorProps } from './selector' +import { useState } from 'react' +import { useSelector } from 'react-redux' import Page from '~/components/layout/Page' import { type Token } from '~/logic/tokens/store/model/token' import Layout from '~/routes/safe/components/Layout' - -type State = { - showReceive: boolean, - sendFunds: Object, -} +import { useCheckForUpdates } from '~/routes/safe/container/Hooks/useCheckForUpdates' +import { useLoadSafe } from '~/routes/safe/container/Hooks/useLoadSafe' +import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors' type Action = 'Send' | 'Receive' -export type Props = Actions & - SelectorProps & { - granted: boolean, +const INITIAL_STATE = { + sendFunds: { + isOpen: false, + selectedToken: undefined, + }, + showReceive: false, +} + +const SafeView = () => { + const [state, setState] = useState(INITIAL_STATE) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + + useLoadSafe(safeAddress) + useCheckForUpdates() + + const onShow = (action: Action) => () => { + setState((prevState) => ({ + ...prevState, + [`show${action}`]: true, + })) } -const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000 - -class SafeView extends React.Component { - state = { - sendFunds: { - isOpen: false, - selectedToken: undefined, - }, - showReceive: false, + const onHide = (action: Action) => () => { + setState((prevState) => ({ + ...prevState, + [`show${action}`]: false, + })) } - intervalId: IntervalID - - longIntervalId: IntervalID - - componentDidMount() { - const { - activeTokens, - addViewedSafe, - fetchCollectibles, - fetchCurrencyValues, - fetchLatestMasterContractVersion, - fetchSafe, - fetchTokenBalances, - fetchTokens, - fetchTransactions, - loadAddressBook, - safeUrl, - } = this.props - - fetchLatestMasterContractVersion() - .then(() => fetchSafe(safeUrl)) - .then(() => { - // The safe needs to be loaded before fetching the transactions - fetchTransactions(safeUrl) - addViewedSafe(safeUrl) - fetchCollectibles() - }) - fetchTokenBalances(safeUrl, activeTokens) - // fetch tokens there to get symbols for tokens in TXs list - fetchTokens() - fetchCurrencyValues(safeUrl) - loadAddressBook() - - this.intervalId = setInterval(() => { - this.checkForUpdates() - }, TIMEOUT) - - this.longIntervalId = setInterval(() => { - fetchCollectibles() - }, TIMEOUT * 3) - } - - componentDidUpdate(prevProps) { - const { activeTokens, fetchTransactions, safeUrl } = this.props - const oldActiveTokensSize = prevProps.activeTokens.size - - if (oldActiveTokensSize > 0 && activeTokens.size > oldActiveTokensSize) { - this.checkForUpdates() - } - - if (safeUrl !== prevProps.safeUrl) { - fetchTransactions(safeUrl) - } - } - - componentWillUnmount() { - clearInterval(this.intervalId) - clearInterval(this.longIntervalId) - } - - onShow = (action: Action) => () => { - this.setState(() => ({ [`show${action}`]: true })) - } - - onHide = (action: Action) => () => { - this.setState(() => ({ [`show${action}`]: false })) - } - - showSendFunds = (token: Token) => { - this.setState({ + const showSendFunds = (token: Token) => { + setState((prevState) => ({ + ...prevState, sendFunds: { isOpen: true, selectedToken: token, }, - }) + })) } - hideSendFunds = () => { - this.setState({ + const hideSendFunds = () => { + setState((prevState) => ({ + ...prevState, sendFunds: { isOpen: false, selectedToken: undefined, }, - }) + })) } + const { sendFunds, showReceive } = state - checkForUpdates() { - const { - activeTokens, - checkAndUpdateSafe, - fetchEtherBalance, - fetchTokenBalances, - fetchTransactions, - safe, - safeUrl, - } = this.props - checkAndUpdateSafe(safeUrl) - fetchTokenBalances(safeUrl, activeTokens) - fetchEtherBalance(safe) - fetchTransactions(safeUrl) - } - - render() { - const { sendFunds, showReceive } = this.state - const { - activateAssetsByBalance, - activateTokensByBalance, - activeTokens, - addressBook, - blacklistedTokens, - cancellationTransactions, - createTransaction, - currencySelected, - currencyValues, - fetchCurrencyValues, - fetchTokens, - granted, - network, - processTransaction, - provider, - safe, - tokens, - transactions, - updateAddressBookEntry, - updateSafe, - userAddress, - } = this.props - - return ( - - - - ) - } + return ( + + + + ) } -export default connect(selector, actions)(SafeView) +export default SafeView diff --git a/src/routes/safe/container/selector.js b/src/routes/safe/container/selector.js index cfb16f39..54cabdeb 100644 --- a/src/routes/safe/container/selector.js +++ b/src/routes/safe/container/selector.js @@ -1,27 +1,19 @@ // @flow import { List, Map } from 'immutable' -import { type Selector, createSelector, createStructuredSelector } from 'reselect' +import { type Selector, createSelector } from 'reselect' -import { safeParamAddressSelector } from '../store/selectors' - -import type { AddressBook } from '~/logic/addressBook/model/addressBook' -import { getAddressBook } from '~/logic/addressBook/store/selectors' -import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues' -import { currencyValuesListSelector, currentCurrencySelector } from '~/logic/currencyValues/store/selectors' import { type Token } from '~/logic/tokens/store/model/token' -import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors' +import { tokensSelector } from '~/logic/tokens/store/selectors' import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers' import { isUserOwner } from '~/logic/wallets/ethAddresses' -import { networkSelector, providerNameSelector, userAccountSelector } from '~/logic/wallets/store/selectors' +import { userAccountSelector } from '~/logic/wallets/store/selectors' import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction' import { type Safe } from '~/routes/safe/store/models/safe' import { type Transaction, type TransactionStatus } from '~/routes/safe/store/models/transaction' import { type RouterProps, - type SafeSelectorProps, safeActiveTokensSelector, safeBalancesSelector, - safeBlacklistedTokensSelector, safeCancellationTransactionsSelector, safeIncomingTransactionsSelector, safeSelector, @@ -29,22 +21,6 @@ import { } from '~/routes/safe/store/selectors' import { type GlobalState } from '~/store' -export type SelectorProps = { - safe: SafeSelectorProps, - provider: string, - tokens: List, - activeTokens: List, - blacklistedTokens: List, - userAddress: string, - network: string, - safeUrl: string, - currencySelected: string, - currencyValues: BalanceCurrencyType[], - transactions: List, - cancellationTransactions: List, - addressBook: AddressBook, -} - const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): TransactionStatus => { let txStatus if (tx.executionTxHash) { @@ -112,7 +88,7 @@ export const extendedSafeTokensSelector: Selector, @@ -143,20 +119,3 @@ const extendedTransactionsSelector: Selector< return List([...extendedTransactions, ...incomingTransactions]) }, ) - -export default createStructuredSelector({ - safe: safeSelector, - provider: providerNameSelector, - tokens: orderedTokenListSelector, - activeTokens: extendedSafeTokensSelector, - blacklistedTokens: safeBlacklistedTokensSelector, - granted: grantedSelector, - userAddress: userAccountSelector, - network: networkSelector, - safeUrl: safeParamAddressSelector, - transactions: extendedTransactionsSelector, - cancellationTransactions: safeCancellationTransactionsSelector, - currencySelected: currentCurrencySelector, - currencyValues: currencyValuesListSelector, - addressBook: getAddressBook, -}) diff --git a/src/routes/safe/store/actions/fetchEtherBalance.js b/src/routes/safe/store/actions/fetchEtherBalance.js index 9fdb8519..634cd053 100644 --- a/src/routes/safe/store/actions/fetchEtherBalance.js +++ b/src/routes/safe/store/actions/fetchEtherBalance.js @@ -3,16 +3,17 @@ import type { Dispatch as ReduxDispatch } from 'redux' import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3' import updateSafe from '~/routes/safe/store/actions/updateSafe' -import type { Safe } from '~/routes/safe/store/models/safe' +import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe' +import type { GetState } from '~/store' import { type GlobalState } from '~/store' -const fetchEtherBalance = (safe: Safe) => async (dispatch: ReduxDispatch) => { +const fetchEtherBalance = (safeAddress: string) => async (dispatch: ReduxDispatch, getState: GetState) => { try { - const { address, ethBalance } = safe - const newEthBalance = await getBalanceInEtherOf(address) - + const state = getState() + const ethBalance = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress, 'ethBalance']) + const newEthBalance = await getBalanceInEtherOf(safeAddress) if (newEthBalance !== ethBalance) { - dispatch(updateSafe({ address, ethBalance: newEthBalance })) + dispatch(updateSafe({ address: safeAddress, ethBalance: newEthBalance })) } } catch (err) { // eslint-disable-next-line diff --git a/src/routes/safe/store/actions/fetchSafe.js b/src/routes/safe/store/actions/fetchSafe.js index 76b21bb1..df9b29f3 100644 --- a/src/routes/safe/store/actions/fetchSafe.js +++ b/src/routes/safe/store/actions/fetchSafe.js @@ -1,8 +1,9 @@ // @flow +import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import { List } from 'immutable' import type { Dispatch as ReduxDispatch } from 'redux' -import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' +import generateBatchRequests from '~/logic/contracts/generateBatchRequests' import { getLocalSafe, getSafeName } from '~/logic/safe/utils' import { enabledFeatures, safeNeedsUpdate } from '~/logic/safe/utils/safeVersion' import { sameAddress } from '~/logic/wallets/ethAddresses' @@ -13,7 +14,7 @@ import removeSafeOwner from '~/routes/safe/store/actions/removeSafeOwner' import updateSafe from '~/routes/safe/store/actions/updateSafe' import { makeOwner } from '~/routes/safe/store/models/owner' import type { SafeProps } from '~/routes/safe/store/models/safe' -import { type GlobalState } from '~/store/index' +import { type GlobalState } from '~/store' const buildOwnersFrom = ( safeOwners: string[], @@ -37,14 +38,22 @@ const buildOwnersFrom = ( export const buildSafe = async (safeAdd: string, safeName: string, latestMasterContractVersion: string) => { const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd) - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) - const ethBalance = await getBalanceInEtherOf(safeAddress) - const threshold = Number(await gnosisSafe.getThreshold()) - const nonce = Number(await gnosisSafe.nonce()) - const owners = List(buildOwnersFrom(await gnosisSafe.getOwners(), await getLocalSafe(safeAddress))) - const currentVersion = await gnosisSafe.VERSION() - const needsUpdate = await safeNeedsUpdate(currentVersion, latestMasterContractVersion) + const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners'] + const [[thresholdStr, nonceStr, currentVersion, remoteOwners], localSafe, ethBalance] = await Promise.all([ + generateBatchRequests({ + abi: GnosisSafeSol.abi, + address: safeAddress, + methods: safeParams, + }), + getLocalSafe(safeAddress), + getBalanceInEtherOf(safeAddress), + ]) + + const threshold = Number(thresholdStr) + const nonce = Number(nonceStr) + const owners = List(buildOwnersFrom(remoteOwners, localSafe)) + const needsUpdate = safeNeedsUpdate(currentVersion, latestMasterContractVersion) const featuresEnabled = enabledFeatures(currentVersion) const safe: SafeProps = { @@ -65,24 +74,27 @@ export const buildSafe = async (safeAdd: string, safeName: string, latestMasterC export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDispatch<*>) => { const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd) // Check if the owner's safe did change and update them - const [gnosisSafe, localSafe] = await Promise.all([getGnosisSafeInstanceAt(safeAddress), getLocalSafe(safeAddress)]) - - const [remoteOwners, remoteNonce, remoteThreshold] = await Promise.all([ - gnosisSafe.getOwners(), - gnosisSafe.nonce(), - gnosisSafe.getThreshold(), + const safeParams = ['getThreshold', 'nonce', 'getOwners'] + const [[remoteThreshold, remoteNonce, remoteOwners], localSafe] = await Promise.all([ + generateBatchRequests({ + abi: GnosisSafeSol.abi, + address: safeAddress, + methods: safeParams, + }), + getLocalSafe(safeAddress), ]) + // Converts from [ { address, ownerName} ] to address array const localOwners = localSafe ? localSafe.owners.map((localOwner) => localOwner.address) : undefined const localThreshold = localSafe ? localSafe.threshold : undefined const localNonce = localSafe ? localSafe.nonce : undefined - if (localNonce !== remoteNonce.toNumber()) { - dispatch(updateSafe({ address: safeAddress, nonce: remoteNonce.toNumber() })) + if (localNonce !== Number(remoteNonce)) { + dispatch(updateSafe({ address: safeAddress, nonce: Number(remoteNonce) })) } - if (localThreshold !== remoteThreshold.toNumber()) { - dispatch(updateSafe({ address: safeAddress, threshold: remoteThreshold.toNumber() })) + if (localThreshold !== Number(remoteThreshold)) { + dispatch(updateSafe({ address: safeAddress, threshold: Number(remoteThreshold) })) } // If the remote owners does not contain a local address, we remove that local owner diff --git a/src/routes/safe/store/actions/fetchTokenBalances.js b/src/routes/safe/store/actions/fetchTokenBalances.js deleted file mode 100644 index ed8a0174..00000000 --- a/src/routes/safe/store/actions/fetchTokenBalances.js +++ /dev/null @@ -1,102 +0,0 @@ -// @flow -import { BigNumber } from 'bignumber.js' -import { List, Map } from 'immutable' -import type { Dispatch as ReduxDispatch } from 'redux' - -import updateSafe from './updateSafe' - -import { getOnlyBalanceToken, getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens' -import { type Token } from '~/logic/tokens/store/model/token' -import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers' -import { sameAddress } from '~/logic/wallets/ethAddresses' -import { ETHEREUM_NETWORK, getWeb3 } from '~/logic/wallets/getWeb3' -import { type GlobalState } from '~/store/index' -import { NETWORK } from '~/utils/constants' - -// List of all the non-standard ERC20 tokens -const nonStandardERC20 = [ - // DATAcoin - { network: ETHEREUM_NETWORK.RINKEBY, address: '0x0cf0ee63788a0849fe5297f3407f701e122cc023' }, -] - -// This is done due to an issues with DATAcoin contract in Rinkeby -// https://rinkeby.etherscan.io/address/0x0cf0ee63788a0849fe5297f3407f701e122cc023#readContract -// It doesn't have a `balanceOf` method implemented. -const isStandardERC20 = (address: string): boolean => { - return !nonStandardERC20.find((token) => sameAddress(address, token.address) && sameAddress(NETWORK, token.network)) -} - -const getTokenBalances = (tokens: List, safeAddress: string) => { - const web3 = getWeb3() - const batch = new web3.BatchRequest() - - const safeTokens = tokens.toJS().filter(({ address }) => address !== ETH_ADDRESS) - const safeTokensBalances = safeTokens.map(({ address, decimals }: any) => { - const onlyBalanceToken = getOnlyBalanceToken() - onlyBalanceToken.options.address = address - - // As a fallback, we're using `balances` - const method = isStandardERC20(address) ? 'balanceOf' : 'balances' - - return new Promise((resolve) => { - const request = onlyBalanceToken.methods[method](safeAddress).call.request((error, balance) => { - if (error) { - // if there's no balance, we log the error, but `resolve` with a default '0' - console.error('No balance method found', error) - resolve('0') - } else { - resolve({ - address, - balance: new BigNumber(balance).div(`1e${decimals}`).toFixed(), - }) - } - }) - - batch.add(request) - }) - }) - - batch.execute() - - return Promise.all(safeTokensBalances) -} - -export const calculateBalanceOf = async (tokenAddress: string, safeAddress: string, decimals: number = 18) => { - if (tokenAddress === ETH_ADDRESS) { - return '0' - } - const erc20Token = await getStandardTokenContract() - let balance = 0 - - try { - const token = await erc20Token.at(tokenAddress) - balance = await token.balanceOf(safeAddress) - } catch (err) { - console.error('Failed to fetch token balances: ', tokenAddress, err) - } - - return new BigNumber(balance).div(10 ** decimals).toString() -} - -const fetchTokenBalances = (safeAddress: string, tokens: List) => async ( - dispatch: ReduxDispatch, -) => { - if (!safeAddress || !tokens || !tokens.size) { - return - } - try { - const withBalances = await getTokenBalances(tokens, safeAddress) - - const balances = Map().withMutations((map) => { - withBalances.forEach(({ address, balance }) => { - map.set(address, balance) - }) - }) - - dispatch(updateSafe({ address: safeAddress, balances })) - } catch (err) { - console.error('Error when fetching token balances:', err) - } -} - -export default fetchTokenBalances diff --git a/src/routes/safe/store/actions/fetchTransactions.js b/src/routes/safe/store/actions/fetchTransactions.js index dd6413df..df08296f 100644 --- a/src/routes/safe/store/actions/fetchTransactions.js +++ b/src/routes/safe/store/actions/fetchTransactions.js @@ -1,4 +1,5 @@ // @flow +import ERC20Detailed from '@openzeppelin/contracts/build/contracts/ERC20Detailed.json' import axios from 'axios' import bn from 'bignumber.js' import { List, Map, type RecordInstance } from 'immutable' @@ -8,15 +9,15 @@ import type { Dispatch as ReduxDispatch } from 'redux' import { addIncomingTransactions } from './addIncomingTransactions' import { addTransactions } from './addTransactions' +import generateBatchRequests from '~/logic/contracts/generateBatchRequests' import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds' import { buildIncomingTxServiceUrl } from '~/logic/safe/transactions/incomingTxHistory' import { type TxServiceType, buildTxServiceUrl } from '~/logic/safe/transactions/txHistory' import { getLocalSafe } from '~/logic/safe/utils' -import { getTokenInfos } from '~/logic/tokens/store/actions/fetchTokens' +import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens' import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi' import { SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH, - hasDecimalsMethod, isMultisendTransaction, isTokenTransfer, isUpgradeTransaction, @@ -73,7 +74,15 @@ type IncomingTxServiceModel = { from: string, } -export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceModel): Promise => { +export const buildTransactionFrom = async ( + safeAddress: string, + tx: TxServiceModel, + knownTokens, + txTokenDecimals, + txTokenSymbol, + txTokenName, + code, +): Promise => { const localSafe = await getLocalSafe(safeAddress) const confirmations = List( @@ -98,10 +107,9 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod ) const modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data - const code = tx.to ? await web3.eth.getCode(tx.to) : '' const isERC721Token = - code.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) || - (isTokenTransfer(tx.data, Number(tx.value)) && !(await hasDecimalsMethod(tx.to))) + (code && code.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) || + (isTokenTransfer(tx.data, Number(tx.value)) && !knownTokens.get(tx.to) && txTokenDecimals !== null) let isSendTokenTx = !isERC721Token && isTokenTransfer(tx.data, Number(tx.value)) const isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value)) const isUpgradeTx = isMultiSendTx && isUpgradeTransaction(tx.data) @@ -109,14 +117,8 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod let refundParams = null if (tx.gasPrice > 0) { - let refundSymbol = 'ETH' - let decimals = 18 - if (tx.gasToken !== ZERO_ADDRESS) { - const gasToken = await getTokenInfos(tx.gasToken) - refundSymbol = gasToken.symbol - decimals = gasToken.decimals - } - + const refundSymbol = txTokenSymbol || 'ETH' + const decimals = txTokenName || 18 const feeString = (tx.gasPrice * (tx.baseGas + tx.safeTxGas)).toString().padStart(decimals, 0) const whole = feeString.slice(0, feeString.length - decimals) || '0' const fraction = feeString.slice(feeString.length - decimals) @@ -128,31 +130,27 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod } } - let symbol = 'ETH' - let decimals = 18 + let symbol = txTokenSymbol || 'ETH' + let decimals = txTokenDecimals || 18 let decodedParams - if (isSendTokenTx) { + if (isSendTokenTx && (txTokenSymbol === null || txTokenDecimals === null)) { try { - const tokenInstance = await getTokenInfos(tx.to) - symbol = tokenInstance.symbol - decimals = tokenInstance.decimals - } catch (err) { - try { - const alternativeTokenInstance = new web3.eth.Contract(ALTERNATIVE_TOKEN_ABI, tx.to) - const [tokenSymbol, tokenDecimals] = await Promise.all([ - alternativeTokenInstance.methods.symbol().call(), - alternativeTokenInstance.methods.decimals().call(), - ]) + const [tokenSymbol, tokenDecimals] = await Promise.all( + generateBatchRequests({ + abi: ALTERNATIVE_TOKEN_ABI, + address: tx.to, + methods: ['symbol', 'decimals'], + }), + ) - symbol = web3.utils.toAscii(tokenSymbol) - decimals = tokenDecimals - } catch (e) { - // some contracts may implement the same methods as in ERC20 standard - // we may falsely treat them as tokens, so in case we get any errors when getting token info - // we fallback to displaying custom transaction - isSendTokenTx = false - customTx = true - } + symbol = tokenSymbol + decimals = tokenDecimals + } catch (e) { + // some contracts may implement the same methods as in ERC20 standard + // we may falsely treat them as tokens, so in case we get any errors when getting token info + // we fallback to displaying custom transaction + isSendTokenTx = false + customTx = true } const params = web3.eth.abi.decodeParameters(['address', 'uint256'], tx.data.slice(10)) @@ -161,9 +159,9 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod value: params[1], } } else if (modifySettingsTx && tx.data) { - decodedParams = await decodeParamsFromSafeMethod(tx.data) + decodedParams = decodeParamsFromSafeMethod(tx.data) } else if (customTx && tx.data) { - decodedParams = await decodeParamsFromSafeMethod(tx.data) + decodedParams = decodeParamsFromSafeMethod(tx.data) } return makeTransaction({ @@ -227,36 +225,62 @@ const addMockSafeCreationTx = (safeAddress): Array => [ }, ] -export const buildIncomingTransactionFrom = async (tx: IncomingTxServiceModel) => { - let symbol = 'ETH' - let decimals = 18 +const batchRequestTxsData = (txs: any[]) => { + const web3Batch = new web3.BatchRequest() - const fee = await web3.eth - .getTransaction(tx.transactionHash) - .then(({ gas, gasPrice }) => bn(gas).div(gasPrice).toFixed()) + const whenTxsValues = txs.map((tx) => { + const methods = ['decimals', { method: 'getCode', type: 'eth', args: [tx.to] }, 'symbol', 'name'] + return generateBatchRequests({ + abi: ERC20Detailed.abi, + address: tx.to, + batch: web3Batch, + context: tx, + methods, + }) + }) - if (tx.tokenAddress) { - try { - const tokenInstance = await getTokenInfos(tx.tokenAddress) - symbol = tokenInstance.symbol - decimals = tokenInstance.decimals - } catch (err) { - try { - const { methods } = new web3.eth.Contract(ALTERNATIVE_TOKEN_ABI, tx.tokenAddress) - const [tokenSymbol, tokenDecimals] = await Promise.all( - [methods.symbol, methods.decimals].map((m) => m().call()), - ) - symbol = web3.utils.hexToString(tokenSymbol) - decimals = tokenDecimals - } catch (e) { - // this is a particular treatment for the DCD token, as it seems to lack of symbol and decimal methods - if (tx.tokenAddress && tx.tokenAddress.toLowerCase() === '0xe0b7927c4af23765cb51314a0e0521a9645f0e2a') { - symbol = 'DCD' - decimals = 9 - } - // if it's not DCD, then we fall to the default values - } - } + web3Batch.execute() + + return Promise.all(whenTxsValues) +} + +const batchRequestIncomingTxsData = (txs: IncomingTxServiceModel[]) => { + const web3Batch = new web3.BatchRequest() + + const whenTxsValues = txs.map((tx) => { + const methods = ['symbol', 'decimals', { method: 'getTransaction', args: [tx.transactionHash], type: 'eth' }] + + return generateBatchRequests({ + abi: ALTERNATIVE_TOKEN_ABI, + address: tx.tokenAddress, + batch: web3Batch, + context: tx, + methods, + }) + }) + + web3Batch.execute() + + return Promise.all(whenTxsValues).then((txsValues) => + txsValues.map(([tx, symbol, decimals, { gas, gasPrice }]) => [ + tx, + symbol === null ? 'ETH' : symbol, + decimals === null ? '18' : decimals, + bn(gas).div(gasPrice).toFixed(), + ]), + ) +} + +export const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]: [ + IncomingTxServiceModel, + string, + string, + string, +]) => { + // this is a particular treatment for the DCD token, as it seems to lack of symbol and decimal methods + if (tx.tokenAddress && tx.tokenAddress.toLowerCase() === '0xe0b7927c4af23765cb51314a0e0521a9645f0e2a') { + symbol = 'DCD' + decimals = '9' } const { transactionHash, ...incomingTx } = tx @@ -278,7 +302,7 @@ export type SafeTransactionsType = { let etagSafeTransactions = null let etagCachedSafeIncommingTransactions = null -export const loadSafeTransactions = async (safeAddress: string): Promise => { +export const loadSafeTransactions = async (safeAddress: string, getState: GetState): Promise => { let transactions: TxServiceModel[] = addMockSafeCreationTx(safeAddress) try { @@ -310,9 +334,14 @@ export const loadSafeTransactions = async (safeAddress: string): Promise> = await Promise.all( - transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx)), + txsWithData.map(([tx: TxServiceModel, decimals, symbol, name, code]) => + buildTransactionFrom(safeAddress, tx, knownTokens, decimals, symbol, name, code), + ), ) const groupedTxs = List(txsRecord).groupBy((tx) => (tx.get('cancellationTx') ? 'cancel' : 'outgoing')) @@ -352,14 +381,15 @@ export const loadSafeIncomingTransactions = async (safeAddress: string) => { } } - const incomingTxsRecord = await Promise.all(incomingTransactions.map(buildIncomingTransactionFrom)) + const incomingTxsWithData = await batchRequestIncomingTxsData(incomingTransactions) + const incomingTxsRecord = incomingTxsWithData.map(buildIncomingTransactionFrom) return Map().set(safeAddress, List(incomingTxsRecord)) } -export default (safeAddress: string) => async (dispatch: ReduxDispatch) => { +export default (safeAddress: string) => async (dispatch: ReduxDispatch, getState: GetState) => { web3 = await getWeb3() - const transactions: SafeTransactionsType | undefined = await loadSafeTransactions(safeAddress) + const transactions: SafeTransactionsType | undefined = await loadSafeTransactions(safeAddress, getState) if (transactions) { const { cancel, outgoing } = transactions diff --git a/src/routes/safe/store/selectors/index.js b/src/routes/safe/store/selectors/index.js index ff6c677d..b6821955 100644 --- a/src/routes/safe/store/selectors/index.js +++ b/src/routes/safe/store/selectors/index.js @@ -1,7 +1,7 @@ // @flow import { List, Map, Set } from 'immutable' import { type Match, matchPath } from 'react-router-dom' -import { type OutputSelector, createSelector, createStructuredSelector } from 'reselect' +import { type OutputSelector, createSelector } from 'reselect' import { getWeb3 } from '~/logic/wallets/getWeb3' import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from '~/routes/routes' @@ -25,10 +25,6 @@ export type RouterProps = { match: Match, } -export type SafeProps = { - safeAddress: string, -} - type TransactionProps = { transaction: Transaction, } @@ -70,6 +66,17 @@ const incomingTransactionsSelector = (state: GlobalState): IncomingTransactionsS const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction +export const safeParamAddressFromStateSelector = (state: GlobalState): 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 null +} + export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => { const urlAdd = props.match.params[SAFE_PARAM_ADDRESS] return urlAdd ? getWeb3().utils.toChecksumAddress(urlAdd) : '' @@ -79,7 +86,7 @@ type TxSelectorType = OutputSelector export const safeTransactionsSelector: TxSelectorType = createSelector( transactionsSelector, - safeParamAddressSelector, + safeParamAddressFromStateSelector, (transactions: TransactionsState, address: string): List => { if (!transactions) { return List([]) @@ -105,7 +112,7 @@ export const addressBookQueryParamsSelector = (state: GlobalState): string => { export const safeCancellationTransactionsSelector: TxSelectorType = createSelector( cancellationTransactionsSelector, - safeParamAddressSelector, + safeParamAddressFromStateSelector, (cancellationTransactions: TransactionsState, address: string): List => { if (!cancellationTransactions) { return List([]) @@ -119,22 +126,11 @@ export const safeCancellationTransactionsSelector: TxSelectorType = createSelect }, ) -export const safeParamAddressFromStateSelector = (state: GlobalState): 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 null -} - type IncomingTxSelectorType = OutputSelector> export const safeIncomingTransactionsSelector: IncomingTxSelectorType = createSelector( incomingTransactionsSelector, - safeParamAddressSelector, + safeParamAddressFromStateSelector, (incomingTransactions: IncomingTransactionsState, address: string): List => { if (!incomingTransactions) { return List([]) @@ -233,12 +229,6 @@ export const safeBlacklistedAssetsSelector: OutputSelector): List => - safes.get(safeAddress).get('activeTokens') - -export const safeBlacklistedTokensSelectorBySafe = (safeAddress: string, safes: Map): List => - safes.get(safeAddress).get('blacklistedTokens') - export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: Map): List => safes.get(safeAddress).get('activeAssets') @@ -256,6 +246,63 @@ export const safeBalancesSelector: OutputSelector> = createSelector( + safeSelector, + (safe: Safe) => { + return safe ? safe.name : undefined + }, +) + +export const safeEthBalanceSelector: OutputSelector> = createSelector( + safeSelector, + (safe: Safe) => { + return safe ? safe.ethBalance : undefined + }, +) + +export const safeNeedsUpdateSelector: OutputSelector> = createSelector( + safeSelector, + (safe: Safe) => { + return safe ? safe.needsUpdate : undefined + }, +) + +export const safeCurrentVersionSelector: OutputSelector> = createSelector( + safeSelector, + (safe: Safe) => { + return safe ? safe.currentVersion : undefined + }, +) + +export const safeThresholdSelector: OutputSelector> = createSelector( + safeSelector, + (safe: Safe) => { + return safe ? safe.threshold : undefined + }, +) + +export const safeNonceSelector: OutputSelector> = createSelector( + safeSelector, + (safe: Safe) => { + return safe ? safe.nonce : undefined + }, +) + +export const safeOwnersSelector: OutputSelector> = createSelector( + safeSelector, + (safe: Safe) => { + return safe ? safe.owners : undefined + }, +) + +export const safeFeaturesEnabledSelector: OutputSelector< + GlobalState, + RouterProps, + Map, +> = createSelector(safeSelector, (safe: Safe) => { + return safe ? safe.featuresEnabled : undefined +}) + export const getActiveTokensAddressesForAllSafes: OutputSelector> = createSelector( safesListSelector, (safes: List) => { @@ -285,9 +332,3 @@ export const getBlacklistedTokensAddressesForAllSafes: OutputSelector({ - safe: safeSelector, - tokens: safeActiveTokensSelector, - blacklistedTokens: safeBlacklistedTokensSelector, -}) diff --git a/src/test/builder/safe.dom.utils.js b/src/test/builder/safe.dom.utils.js index bb2a8aab..1d8d8542 100644 --- a/src/test/builder/safe.dom.utils.js +++ b/src/test/builder/safe.dom.utils.js @@ -13,6 +13,7 @@ import { history, type GlobalState } from '~/store' import AppRoutes from '~/routes' import { SAFELIST_ADDRESS } from '~/routes/routes' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' +import { wrapInSuspense } from '~/utils/wrapInSuspense' export const EXPAND_BALANCE_INDEX = 0 export const EXPAND_OWNERS_INDEX = 1 @@ -89,9 +90,7 @@ const renderApp = (store: Store) => ({ - }> - - + {wrapInSuspense(,
)} , diff --git a/src/test/safe.dom.funds.threshold>1.test.js b/src/test/safe.dom.funds.threshold>1.test.js index 4c507fa0..bf860399 100644 --- a/src/test/safe.dom.funds.threshold>1.test.js +++ b/src/test/safe.dom.funds.threshold>1.test.js @@ -9,7 +9,7 @@ import { sleep } from '~/utils/timer' import '@testing-library/jest-dom/extend-expect' import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances' import { fillAndSubmitSendFundsForm } from './utils/transactions' -import { TRANSACTIONS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout' +import { TRANSACTIONS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout/index' import { TRANSACTION_ROW_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable' import { useTestAccountAt, resetTestAccount } from './utils/accounts' import { CONFIRM_TX_BTN_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/ButtonRow' diff --git a/src/test/safe.dom.settings.name.test.js b/src/test/safe.dom.settings.name.test.js index 49822695..5f33ae16 100644 --- a/src/test/safe.dom.settings.name.test.js +++ b/src/test/safe.dom.settings.name.test.js @@ -5,7 +5,7 @@ import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { renderSafeView } from '~/test/builder/safe.dom.utils' import { sleep } from '~/utils/timer' import '@testing-library/jest-dom/extend-expect' -import { SETTINGS_TAB_BTN_TEST_ID, SAFE_VIEW_NAME_HEADING_TEST_ID } from '~/routes/safe/components/Layout' +import { SETTINGS_TAB_BTN_TEST_ID, SAFE_VIEW_NAME_HEADING_TEST_ID } from '~/routes/safe/components/Layout/index' import { SAFE_NAME_INPUT_TEST_ID, SAFE_NAME_SUBMIT_BTN_TEST_ID } from '~/routes/safe/components/Settings/SafeDetails' describe('DOM > Feature > Settings - Name', () => { diff --git a/src/test/safe.dom.settings.owners.test.js b/src/test/safe.dom.settings.owners.test.js index 3350fd96..baefba87 100644 --- a/src/test/safe.dom.settings.owners.test.js +++ b/src/test/safe.dom.settings.owners.test.js @@ -10,7 +10,7 @@ import { checkRegisteredTxRemoveOwner, checkRegisteredTxReplaceOwner, } from './utils/transactions' -import { SETTINGS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout' +import { SETTINGS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout/index' import { OWNERS_SETTINGS_TAB_TEST_ID } from '~/routes/safe/components/Settings' import { RENAME_OWNER_BTN_TEST_ID, diff --git a/src/test/utils/transactions/transactionList.helper.js b/src/test/utils/transactions/transactionList.helper.js index bd933765..73157767 100644 --- a/src/test/utils/transactions/transactionList.helper.js +++ b/src/test/utils/transactions/transactionList.helper.js @@ -2,7 +2,7 @@ import { fireEvent } from '@testing-library/react' import { sleep } from '~/utils/timer' import { shortVersionOf } from '~/logic/wallets/ethAddresses' -import { TRANSACTIONS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout' +import { TRANSACTIONS_TAB_BTN_TEST_ID } from '~/routes/safe/components/Layout/index' import { TRANSACTION_ROW_TEST_ID } from '~/routes/safe/components/Transactions/TxsTable' import { TRANSACTIONS_DESC_ADD_OWNER_TEST_ID, diff --git a/src/utils/constants.js b/src/utils/constants.js index f8c61f29..bc82518c 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -13,3 +13,4 @@ export const LATEST_SAFE_VERSION = process.env.REACT_APP_LATEST_SAFE_VERSION || export const APP_VERSION = process.env.REACT_APP_APP_VERSION || 'not-defined' export const OPENSEA_API_KEY = process.env.REACT_APP_OPENSEA_API_KEY || '' export const COLLECTIBLES_SOURCE = process.env.REACT_APP_COLLECTIBLES_SOURCE || 'OpenSea' +export const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000 diff --git a/src/utils/googleAnalytics.js b/src/utils/googleAnalytics.js index 48ae64c7..b4cce767 100644 --- a/src/utils/googleAnalytics.js +++ b/src/utils/googleAnalytics.js @@ -1,12 +1,11 @@ // @flow -import React, { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import GoogleAnalytics from 'react-ga' import { getGoogleAnalyticsTrackingID } from '~/config' import { COOKIES_KEY } from '~/logic/cookies/model/cookie' import type { CookiesProps } from '~/logic/cookies/model/cookie' import { loadFromCookie } from '~/logic/cookies/utils' -import type { RouterProps } from '~/routes/safe/store/selectors' let analyticsLoaded = false export const loadGoogleAnalytics = () => { @@ -25,22 +24,22 @@ export const loadGoogleAnalytics = () => { } } -export const withTracker = (WrappedComponent, options = {}) => { - const [useAnalytics, setUseAnalytics] = useState(false) +export const useAnalytics = () => { + const [analyticsAllowed, setAnalyticsAllowed] = useState(false) useEffect(() => { async function fetchCookiesFromStorage() { const cookiesState: CookiesProps = await loadFromCookie(COOKIES_KEY) if (cookiesState) { const { acceptedAnalytics } = cookiesState - setUseAnalytics(acceptedAnalytics) + setAnalyticsAllowed(acceptedAnalytics) } } fetchCookiesFromStorage() }, []) - const trackPage = (page) => { - if (!useAnalytics || !analyticsLoaded) { + const trackPage = useCallback((page, options = {}) => { + if (!analyticsAllowed || !analyticsLoaded) { return } GoogleAnalytics.set({ @@ -48,17 +47,7 @@ export const withTracker = (WrappedComponent, options = {}) => { ...options, }) GoogleAnalytics.pageview(page) - } + }, []) - const HOC = (props: RouterProps) => { - // eslint-disable-next-line react/prop-types - const { location } = props - useEffect(() => { - const page = location.pathname + location.search - trackPage(page) - }, [location.pathname]) - return - } - - return HOC + return { trackPage } } diff --git a/src/utils/wrapInSuspense.js b/src/utils/wrapInSuspense.js new file mode 100644 index 00000000..2d7a8198 --- /dev/null +++ b/src/utils/wrapInSuspense.js @@ -0,0 +1,3 @@ +// @flow +import React from 'react' +export const wrapInSuspense = (component, fallback) => {component}