From c0422bd7d1423281a7b1f49a6ba2ebc34775f590 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 23 Apr 2020 15:43:54 +0200 Subject: [PATCH 1/2] Rename incoming-transactions url to incoming-transfers --- src/config/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/index.js b/src/config/index.js index 24729b2e..f8b1e7c5 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -56,7 +56,7 @@ export const getTxServiceUriFrom = (safeAddress: string) => `safes/${safeAddress}/transactions/` export const getIncomingTxServiceUriTo = (safeAddress: string) => - `safes/${safeAddress}/incoming-transactions/` + `safes/${safeAddress}/incoming-transfers/` export const getRelayUrl = () => getConfig()[RELAY_API_URL] From 9d784922f205a632da3a3f177e669ee962db9fe5 Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Thu, 23 Apr 2020 11:18:06 -0300 Subject: [PATCH 2/2] Feature #587 - App performance improvement (#738) * Refactor Balances to functional component Removes Balances props in Layout.jsx * Replaces selectors that were using safeParamAddressSelector with safeParamAddressFromStateSelector to avoid the bug of non-loaded safe when called Also exports extendedTransactionsSelector to let the components use it * Refactors Transactions.jsx, replaces transactions.tsx with txsTable.jsx Removes all unused props from transactions.jsx Makes all childs of txsTable.jsx fetch the props they need instead of sharing all of them even if they are not used * Adds new selectors: safeNameSelector, safeNeedsUpdateSelector, safeCurrentVersionSelector * Removes unused props from layout to settings.jsx Removed props from the settings.jsx childs, now they fetch the values they need directly from the store * Removes unused redux connect * Adds safeEthBalanceSelector * Removes all the props from layout to settings * Refactor root and layout, extracts checkForUpdate and componentDidMount to components Extracts header and tabs from Layout.jsx * Makes routes.jsx use selectors instead of connect to unify the code * Loads tabs components using react suspense * Fixs warning when trying to update root component within loadStore.jsx Replaces safe with safeAddress selector in layout.jsx to reduce the unnecessary rendering Fixs setState in container.jsx * Fixs checkForUpdates fetchTokenBalances Adds FetchTokens.jsx in balances Adds safeFeaturesEnabledSelector * Fixs load addressbook action * Replaces safe with owners in manage owners * Improves buildSafe promises calls Improves the loadStorage calls * Fixs error "Uncaught (in promise) TypeError: Cannot read property 'includes' of undefined" * Extracts LoadStore from outside the router component to avoid re-loading the store Adds react lazy for Coins and Collectible tabs * Reduce the polling rate for fetching transactions * Removes unused actions.js Removes unused selector props * Fixes owners column selector usage * Fixes processTransaction usage * Optimize how the transaction list within the transactions tab are loaded * Fix lint error * Fix edit addressbook entry * Fixs show send coins modal * feature: batchRequest for incoming transactions * Fixs race condition on loadStore Fixs check for updates address null validation * Adds ensureOnceAsync for getGnosisSafeInstanceAt Adds batch request for safe params * Removes unnecessary checkAndUpdateSafe from checkForUpdates, now the safe gets updated when a transaction arrives * Fixs ensureOnce/memoization * refactor: make a generic generateBatchRequest method Create a generic method to easily build web3 batch requests * refactor: use `generateBatchRequest` in `getTokensBalances` * Removes .toJS in edit entry * Removes web3 from sendTransactions Removes pascal case FetchTokens * Replaces /layout import * Replaces CheckForUpdates component with useCheckForUpdates hook * Makes FetchTokens a hook * Removes getSafeParamsBatch, now the safes gets the data using the generic generateBatchRequests * Replaces INITIAL_STATUS with INITIAL_STATE * Replaces regex Adds spaces before return * Adds wrapInSuspense * Runs prettier * Makes checkAndUpdateSafe use generateBatchRequests * Makes checkAndUpdateSafe use generateBatchRequests * Fixs check for updates with null address * Fixs transaction details getNameFromAddressBook * Fixes zIndex between cookies banner and transaction details * fix: cast returned values to number Original code was expecting a BN instance, now it's a plain string. * Fix replace owner name * Fix race condition with adbk load * Fixes replaces owner * Fixs apps * Moved hooks into own folder * Moved LoadStore to routes.js Refactors LoadStore as Hook Move LoadStore to hooks folder * Moves useLoadStore to safes/index * Revert loadStore place change * Fixes bug with fetchEtherBalance that causes updateSafe to be called * remove useLoadStore, add useAnalytics hook * remove React.memo from SafeView * Reverts removing useLoadStore in order to maintain the consistency of the code * rewrite useLoadStore in a more hook-y way, rename it to useLoadSafe * Removes unnecessary setSafeLoaded * Removes unnecessary safeLoaded * fix: Coins values and balances The app was retrieving ERC20 tokens information from 3 different endpoints. - One from `balance/` to have the list updated - another from `balance/usd` to have the values in USD - the last one from the blockchain, to update the balances This was all simplified to `balance/usd`. Also, added a `currencyRate` to be updated when the currency is modified. The value calculation happens on a component level, so when the `balanceUsd` value is modified, the value is properly reflected on the screen. Refactored `activateTokensByBalance` to `fetchSafeTokens`, as this was doing quite more than just _activating_ and also added the `currencyList` calculation in it, so everything is updated when `balance/usd` endpoint is requested. * fix: Balance screen Collectibles weren't loading when clicking on the link Also, refactored setState usage, to properly update current state * fix: featuresEnabled undefined * fix: add/activate newly received tokens * fix: NaN Values in Coins for a newly loaded Safe Was failing to set a default value for the `currencyRate` * fix: Settings fails to load if `owners` is not loaded into store Added a `Loader` until the required Safe's data is loaded into store. * fix: prefetch txs data When building the Txs list, we requested data for every tx what was translated into several RPC calls. Now by _batchRequesting_ all the information on beforehand, Safe's loading is a bit faster. * fix: prevent requesting safe, when there's no safe available in the store * enhancement: fetch tokens when loading safe By doing this, when loading a safe in the balance screen we will have tokens immediately loaded into the coins list * fix: load collectibles when switch to collectibles screen Collectibles weren't loaded when clicking menu link for a newly loaded safe. Now every switch to the collectible's screen will trigger a fetch for collectibles. * fix: fetch only if safe is ready Co-authored-by: fernandomg Co-authored-by: Mikhail Mikheev --- src/components/CookiesBanner/index.jsx | 2 +- src/components/Root/index.js | 9 +- .../addressBook/store/selectors/index.js | 13 - src/logic/addressBook/utils/index.js | 2 +- .../store/actions/fetchCollectibles.js | 7 +- src/logic/contracts/generateBatchRequests.js | 58 +++ src/logic/contracts/methodIds.js | 4 +- src/logic/contracts/safeContracts.js | 7 +- .../api/fetchTokenCurrenciesBalances.js | 6 +- .../actions/fetchCurrencySelectedValue.js | 28 +- .../store/actions/fetchCurrencyValues.js | 41 +- .../store/actions/setCurrencyRate.js | 12 + .../store/actions/setCurrencySelected.js | 2 +- .../store/model/currencyValues.js | 5 +- .../store/reducer/currencyValues.js | 17 +- .../currencyValues/store/selectors/index.js | 3 + .../store/actions/activateAssetsByBalance.js | 7 +- .../store/actions/activateTokensByBalance.js | 61 --- .../tokens/store/actions/fetchSafeTokens.js | 100 +++++ src/logic/tokens/store/actions/fetchTokens.js | 16 +- src/logic/tokens/utils/alternativeAbi.js | 2 +- src/routes/index.js | 34 +- .../safe/components/AddressBook/index.jsx | 8 +- src/routes/safe/components/Apps/index.jsx | 52 +-- .../safe/components/Apps/sendTransactions.js | 32 +- .../safe/components/Balances/Coins/index.jsx | 14 +- .../components/Balances/Receive/index.jsx | 94 +++-- .../safe/components/Balances/dataFetcher.js | 37 +- src/routes/safe/components/Balances/index.jsx | 289 ++++++------- src/routes/safe/components/Layout.jsx | 399 ------------------ .../safe/components/Layout/Header/index.jsx | 82 ++++ .../components/{ => Layout/Header}/style.js | 31 +- .../Layout/Tabs/SettingsTab/index.jsx | 30 ++ .../safe/components/Layout/Tabs/index.jsx | 135 ++++++ .../safe/components/Layout/Tabs/style.js | 21 + src/routes/safe/components/Layout/index.jsx | 126 ++++++ src/routes/safe/components/Layout/style.js | 24 ++ .../ManageOwners/AddOwnerModal/index.jsx | 77 ++-- .../AddOwnerModal/screens/OwnerForm/index.jsx | 8 +- .../AddOwnerModal/screens/Review/index.jsx | 12 +- .../screens/ThresholdForm/index.jsx | 10 +- .../ManageOwners/EditOwnerModal/index.jsx | 20 +- .../ManageOwners/RemoveOwnerModal/index.jsx | 82 ++-- .../RemoveOwnerModal/screens/Review/index.jsx | 23 +- .../screens/ThresholdForm/index.jsx | 10 +- .../ManageOwners/ReplaceOwnerModal/index.jsx | 105 ++--- .../screens/OwnerForm/index.jsx | 8 +- .../screens/Review/index.jsx | 33 +- .../Settings/ManageOwners/index.jsx | 68 +-- .../Settings/RemoveSafeModal/index.jsx | 149 +++---- .../components/Settings/SafeDetails/index.jsx | 34 +- .../Settings/ThresholdSettings/index.jsx | 51 ++- .../safe/components/Settings/actions.js | 19 - src/routes/safe/components/Settings/index.jsx | 254 ++++------- .../ExpandedTx/ApproveTxModal/index.jsx | 37 +- .../ExpandedTx/OwnersColumn/index.jsx | 15 +- .../TxsTable/ExpandedTx/OwnersColumn/style.js | 4 +- .../ExpandedTx/RejectTxModal/index.jsx | 41 +- .../TxsTable/ExpandedTx/index.jsx | 50 +-- .../Transactions/TxsTable/index.jsx | 41 +- .../safe/components/Transactions/index.jsx | 52 --- .../container/Hooks/useCheckForUpdates.jsx | 32 ++ .../safe/container/Hooks/useFetchTokens.jsx | 30 ++ .../safe/container/Hooks/useLoadSafe.jsx | 30 ++ src/routes/safe/container/actions.js | 57 --- src/routes/safe/container/index.jsx | 224 +++------- src/routes/safe/container/selector.js | 49 +-- .../safe/store/actions/fetchEtherBalance.js | 13 +- src/routes/safe/store/actions/fetchSafe.js | 50 ++- .../safe/store/actions/fetchTokenBalances.js | 102 ----- .../safe/store/actions/fetchTransactions.js | 172 ++++---- src/routes/safe/store/selectors/index.js | 103 +++-- src/test/builder/safe.dom.utils.js | 5 +- src/test/safe.dom.funds.threshold>1.test.js | 2 +- src/test/safe.dom.settings.name.test.js | 2 +- src/test/safe.dom.settings.owners.test.js | 2 +- .../transactions/transactionList.helper.js | 2 +- src/utils/constants.js | 1 + src/utils/googleAnalytics.js | 27 +- src/utils/wrapInSuspense.js | 3 + 80 files changed, 1715 insertions(+), 2204 deletions(-) create mode 100644 src/logic/contracts/generateBatchRequests.js create mode 100644 src/logic/currencyValues/store/actions/setCurrencyRate.js delete mode 100644 src/logic/tokens/store/actions/activateTokensByBalance.js create mode 100644 src/logic/tokens/store/actions/fetchSafeTokens.js delete mode 100644 src/routes/safe/components/Layout.jsx create mode 100644 src/routes/safe/components/Layout/Header/index.jsx rename src/routes/safe/components/{ => Layout/Header}/style.js (73%) create mode 100644 src/routes/safe/components/Layout/Tabs/SettingsTab/index.jsx create mode 100644 src/routes/safe/components/Layout/Tabs/index.jsx create mode 100644 src/routes/safe/components/Layout/Tabs/style.js create mode 100644 src/routes/safe/components/Layout/index.jsx create mode 100644 src/routes/safe/components/Layout/style.js delete mode 100644 src/routes/safe/components/Settings/actions.js delete mode 100644 src/routes/safe/components/Transactions/index.jsx create mode 100644 src/routes/safe/container/Hooks/useCheckForUpdates.jsx create mode 100644 src/routes/safe/container/Hooks/useFetchTokens.jsx create mode 100644 src/routes/safe/container/Hooks/useLoadSafe.jsx delete mode 100644 src/routes/safe/container/actions.js delete mode 100644 src/routes/safe/store/actions/fetchTokenBalances.js create mode 100644 src/utils/wrapInSuspense.js 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}