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 <fernando.greco@gmail.com>
Co-authored-by: Mikhail Mikheev <mmvsha73@gmail.com>
This commit is contained in:
Agustin Pane 2020-04-23 11:18:06 -03:00 committed by GitHub
parent c0422bd7d1
commit 9d784922f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1715 additions and 2204 deletions

View File

@ -30,7 +30,7 @@ const useStyles = makeStyles({
padding: '27px 15px',
position: 'fixed',
width: '100%',
zIndex: '5',
zIndex: '15',
},
content: {
maxWidth: '100%',

View File

@ -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 = () => (
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<ConnectedRouter history={history}>
<PageFrame>
<Suspense fallback={<Loader />}>
<AppRoutes />
</Suspense>
</PageFrame>
<PageFrame>{wrapInSuspense(<AppRoutes />, <Loader />)}</PageFrame>
</ConnectedRouter>
</MuiThemeProvider>
</Provider>

View File

@ -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<GlobalState, {}, List<AddressB
return result
},
)
export const getNameFromAddressBook = (userAddress: string): string | null => {
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
}

View File

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

View File

@ -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<GlobalState>, getState
const source = getConfiguredSource()
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network)
batch(() => {
dispatch(addNftAssets(collectibles.nftAssets))
dispatch(addNftTokens(collectibles.nftTokens))
})
}
export default fetchCollectibles

View File

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

View File

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

View File

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

View File

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

View File

@ -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<typeof AVAILABLE_CURRENCIES>) => async (
dispatch: ReduxDispatch<GlobalState>,
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

View File

@ -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<GlobalState>) => {
export const fetchCurrencyValues = () => async (dispatch: ReduxDispatch<GlobalState>) => {
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))
batch(() => {
dispatch(setCurrencySelected(currencyValueSelected))
dispatch(fetchCurrencySelectedValue(currencyValueSelected))
})
} catch (err) {
console.error('Error fetching tokens price list', err)
}

View File

@ -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<string, *>(
SET_CURRENCY_RATE,
(currencyRate: string): CurrencyValuesProps => ({ currencyRate }),
)

View File

@ -9,5 +9,5 @@ export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
// eslint-disable-next-line max-len
export const setCurrencySelected = createAction<string, *>(
SET_CURRENT_CURRENCY,
(currencyValueSelected: AVAILABLE_CURRENCIES): CurrencyValuesProps => ({ currencyValueSelected }),
(currencyValueSelected: $Keys<typeof AVAILABLE_CURRENCIES>): CurrencyValuesProps => ({ currencyValueSelected }),
)

View File

@ -39,7 +39,7 @@ export const AVAILABLE_CURRENCIES = {
}
export type BalanceCurrencyType = {
currencyName: AVAILABLE_CURRENCIES,
currencyName: $Keys<typeof AVAILABLE_CURRENCIES>,
tokenAddress: string,
balanceInBaseCurrency: string,
balanceInSelectedCurrency: string,
@ -53,7 +53,8 @@ export const makeBalanceCurrency = Record({
})
export type CurrencyValuesProps = {
currencyValueSelected: AVAILABLE_CURRENCIES,
currencyValueSelected: $Keys<typeof AVAILABLE_CURRENCIES>,
currencyRate: string,
currencyValuesList: BalanceCurrencyType[],
}

View File

@ -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<State, *>(
{
[SET_CURRENCY_RATE]: (state: State, action: ActionType<Function>): State => {
const { currencyRate } = action.payload
return state.set('currencyRate', currencyRate)
},
[SET_CURRENCY_BALANCES]: (state: State, action: ActionType<Function>): 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<Function>): State => {
const { currencyValueSelected } = action.payload
const newState = state.set('currencyValueSelected', currencyValueSelected)
return newState
return state.set('currencyValueSelected', currencyValueSelected)
},
},
Map(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ export const ALTERNATIVE_TOKEN_ABI = [
outputs: [
{
name: '',
type: 'bytes32',
type: 'string',
},
],
payable: false,

View File

@ -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<boolean>(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 (
<Switch>
<Route
@ -46,7 +53,6 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => {
return <Loader />
}
setInitialLoad(false)
if (defaultSafe) {
return <Redirect to={`${SAFELIST_ADDRESS}/${defaultSafe}/balances`} />
}
@ -54,17 +60,13 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => {
return <Redirect to={WELCOME_ADDRESS} />
}}
/>
<Route component={withTracker(Welcome)} exact path={WELCOME_ADDRESS} />
<Route component={withTracker(Open)} exact path={OPEN_ADDRESS} />
<Route component={withTracker(Safe)} path={SAFE_ADDRESS} />
<Route component={withTracker(Load)} exact path={LOAD_ADDRESS} />
<Route component={Welcome} exact path={WELCOME_ADDRESS} />
<Route component={Open} exact path={OPEN_ADDRESS} />
<Route component={Safe} path={SAFE_ADDRESS} />
<Route component={Load} exact path={LOAD_ADDRESS} />
<Redirect to="/" />
</Switch>
)
}
// $FlowFixMe
export default connect<Object, Object, ?Function, ?Object>(
(state) => ({ defaultSafe: defaultSafeSelector(state) }),
null,
)(withRouter(Routes))
export default withRouter(Routes)

View File

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

View File

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

View File

@ -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<any>,
enqueueSnackbar: Function,
closeSnackbar: Function,
origin: string,
) => {
const web3 = getWeb3()
const multiSend = new web3.eth.Contract(multiSendAbi, multiSendAddress)
const encodeMultiSendCalldata = multiSend.methods
@ -41,7 +43,8 @@ const sendTransactions = (
)
.encodeABI()
return createTransaction({
return dispatch(
createTransaction({
safeAddress,
to: multiSendAddress,
valueInWei: 0,
@ -52,6 +55,7 @@ const sendTransactions = (
operation: DELEGATE_CALL,
// navigateToTransactionsTab: false,
origin,
})
}),
)
}
export default sendTransactions

View File

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

View File

@ -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,11 +77,12 @@ const styles = () => ({
type Props = {
onClose: () => void,
classes: Object,
safeName: string,
safeAddress: string,
}
const Receive = ({ classes, onClose, safeAddress, safeName }: Props) => (
const Receive = ({ classes, onClose }: Props) => {
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
return (
<>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin size="xl" weight="bolder">
@ -122,6 +125,7 @@ const Receive = ({ classes, onClose, safeAddress, safeName }: Props) => (
</Button>
</Row>
</>
)
)
}
export default withStyles(styles)(Receive)

View File

@ -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<BalanceData>
// eslint-disable-next-line max-len
const getTokenPriceInCurrency = (
token: Token,
currencySelected: typeof AVAILABLE_CURRENCIES,
currencySelected: $Keys<typeof AVAILABLE_CURRENCIES>,
currencyValues: List<BalanceCurrencyType>,
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 true
}
return token.address === tokenAddress
})
if (!currencyValue) {
return ''
}
const { balanceInBaseCurrency } = currencyValue
const balance = BigNumber(balanceInBaseCurrency).times(currencyRate).toFixed(2)
return `${balance} ${currencySelected}`
}
}
return null
}
// eslint-disable-next-line max-len
export const getBalanceData = (
activeTokens: List<Token>,
currencySelected: string,
currencySelected: $Keys<typeof AVAILABLE_CURRENCIES>,
currencyValues: List<BalanceCurrencyType>,
currencyRate: string,
): List<BalanceRow> => {
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

View File

@ -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,30 +38,12 @@ type State = {
}
type Props = {
activateTokensByBalance: Function,
activateAssetsByBalance: Function,
activeTokens: List<Token>,
blacklistedTokens: List<Token>,
classes: Object,
createTransaction: Function,
currencySelected: string,
currencyValues: BalanceCurrencyType[],
ethBalance: string,
featuresEnabled: string[],
fetchCurrencyValues: Function,
fetchTokens: Function,
granted: boolean,
safeAddress: string,
safeName: string,
tokens: List<Token>,
}
type Action = 'Token' | 'Send' | 'Receive' | 'ManageCollectibleModal'
class Balances extends React.Component<Props, State> {
constructor(props) {
super(props)
this.state = {
const INITIAL_STATE: State = {
erc721Enabled: false,
subMenuOptions: [],
showToken: false,
@ -72,36 +55,35 @@ class Balances extends React.Component<Props, State> {
showCoins: true,
showCollectibles: false,
showReceive: false,
}
props.fetchTokens()
}
}
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,41 +91,53 @@ class Balances extends React.Component<Props, State> {
}
}
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 {
assetDivider,
assetTab,
assetTabActive,
assetTabs,
controls,
manageTokensButton,
receiveModal,
tokenControls,
} = props.classes
const {
erc721Enabled,
sendFunds,
@ -153,19 +147,18 @@ class Balances extends React.Component<Props, State> {
showReceive,
showToken,
subMenuOptions,
} = this.state
const { activeTokens, classes, createTransaction, ethBalance, safeAddress, safeName } = this.props
} = state
return (
<>
<Row align="center" className={classes.controls}>
<Col className={classes.assetTabs} sm={6} start="sm" xs={12}>
<Row align="center" className={controls}>
<Col className={assetTabs} sm={6} start="sm" xs={12}>
{subMenuOptions.length > 1 &&
subMenuOptions.map(({ enabled, legend, url }, index) => (
<React.Fragment key={`legend-${index}`}>
{index > 0 && <Divider className={classes.assetDivider} />}
{index > 0 && <Divider className={assetDivider} />}
<Link
className={enabled ? classes.assetTabActive : classes.assetTab}
className={enabled ? assetTabActive : assetTab}
data-testid={`${legend.toLowerCase()}'-assets-btn'`}
size="md"
to={url}
@ -176,11 +169,11 @@ class Balances extends React.Component<Props, State> {
</React.Fragment>
))}
</Col>
<Col className={classes.tokenControls} end="sm" sm={6} xs={12}>
<Col className={tokenControls} end="sm" sm={6} xs={12}>
{showCoins && <DropdownCurrency />}
<ButtonLink
className={classes.manageTokensButton}
onClick={erc721Enabled && showCollectibles ? this.onShow('ManageCollectibleModal') : this.onShow('Token')}
className={manageTokensButton}
onClick={erc721Enabled && showCollectibles ? () => onShow('ManageCollectibleModal') : () => onShow('Token')}
size="lg"
testId="manage-tokens-btn"
>
@ -190,43 +183,37 @@ class Balances extends React.Component<Props, State> {
description={
erc721Enabled ? 'Enable and disables assets to be listed' : 'Enable and disable tokens to be listed'
}
handleClose={showManageCollectibleModal ? this.onHide('ManageCollectibleModal') : this.onHide('Token')}
handleClose={showManageCollectibleModal ? () => onHide('ManageCollectibleModal') : () => onHide('Token')}
open={showToken || showManageCollectibleModal}
title="Manage List"
>
<Tokens
modalScreen={showManageCollectibleModal ? 'assetsList' : 'tokenList'}
onClose={showManageCollectibleModal ? this.onHide('ManageCollectibleModal') : this.onHide('Token')}
safeAddress={safeAddress}
onClose={showManageCollectibleModal ? () => onHide('ManageCollectibleModal') : () => onHide('Token')}
safeAddress={address}
/>
</Modal>
</Col>
</Row>
{showCoins && <Coins showReceiveFunds={this.onShow('Receive')} showSendFunds={this.showSendFunds} />}
{erc721Enabled && showCollectibles && <Collectibles />}
{showCoins && wrapInSuspense(<Coins showReceiveFunds={() => onShow('Receive')} showSendFunds={showSendFunds} />)}
{erc721Enabled && showCollectibles && wrapInSuspense(<Collectibles />)}
<SendModal
activeScreenType="sendFunds"
createTransaction={createTransaction}
ethBalance={ethBalance}
isOpen={sendFunds.isOpen}
onClose={this.hideSendFunds}
safeAddress={safeAddress}
safeName={safeName}
onClose={hideSendFunds}
selectedToken={sendFunds.selectedToken}
tokens={activeTokens}
/>
<Modal
description="Receive Tokens Form"
handleClose={this.onHide('Receive')}
handleClose={() => onHide('Receive')}
open={showReceive}
paperClassName={classes.receiveModal}
paperClassName={receiveModal}
title="Receive Tokens"
>
<Receive onClose={this.onHide('Receive')} safeAddress={safeAddress} safeName={safeName} />
<Receive onClose={() => onHide('Receive')} />
</Modal>
</>
)
}
}
export default withStyles(styles)(Balances)

View File

@ -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 <NoSafe provider={provider} text="Safe not found" />
}
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 = (
<>
<AddressBookIcon />
Address Book
</>
)
const labelApps = (
<>
<AppsIcon />
Apps
</>
)
const labelSettings = (
<>
<SettingsIcon />
<Badge
badgeContent=""
color="error"
invisible={!safe.needsUpdate || !granted}
style={{ paddingRight: '10px' }}
variant="dot"
>
Settings
</Badge>
</>
)
const labelBalances = (
<>
<BalancesIcon />
Assets
</>
)
const labelTransactions = (
<>
<TransactionsIcon />
Transactions
</>
)
const renderAppsTab = () => (
<React.Suspense>
<Apps
closeModal={closeGenericModal}
createTransaction={createTransaction}
ethBalance={ethBalance}
granted={granted}
network={network}
openModal={openGenericModal}
safeAddress={address}
safeName={name}
web3={web3Instance}
/>
</React.Suspense>
)
const tabsValue = () => {
const balanceLocation = `${match.url}/balances`
const isInBalance = new RegExp(`^${balanceLocation}.*$`)
const { pathname } = location
if (isInBalance.test(pathname)) {
return balanceLocation
}
return pathname
}
return (
<>
<Block className={classes.container} margin="xl">
<Row className={classes.userInfo}>
<Identicon address={address} diameter={50} />
<Block className={classes.name}>
<Row>
<Heading className={classes.nameText} color="primary" tag="h2" testId={SAFE_VIEW_NAME_HEADING_TEST_ID}>
{name}
</Heading>
{!granted && <Block className={classes.readonly}>Read Only</Block>}
</Row>
<Block className={classes.user} justify="center">
<Paragraph className={classes.address} color="disabled" noMargin size="md">
{address}
</Paragraph>
<CopyBtn content={address} />
<EtherscanBtn type="address" value={address} />
</Block>
</Block>
</Row>
<Block className={classes.balance}>
<Button
className={classes.send}
color="primary"
disabled={!granted}
onClick={() => showSendFunds('Ether')}
size="small"
variant="contained"
>
<CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
Send
</Button>
<Button
className={classes.receive}
color="primary"
onClick={onShow('Receive')}
size="small"
variant="contained"
>
<CallReceived alt="Receive Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
Receive
</Button>
</Block>
</Block>
<Tabs
indicatorColor="secondary"
onChange={handleCallToRouter}
textColor="secondary"
value={tabsValue()}
variant="scrollable"
>
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={BALANCES_TAB_BTN_TEST_ID}
label={labelBalances}
value={`${match.url}/balances`}
/>
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={TRANSACTIONS_TAB_BTN_TEST_ID}
label={labelTransactions}
value={`${match.url}/transactions`}
/>
{process.env.REACT_APP_ENV !== 'production' && (
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={TRANSACTIONS_TAB_BTN_TEST_ID}
label={labelApps}
value={`${match.url}/apps`}
/>
)}
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={ADDRESS_BOOK_TAB_BTN_TEST_ID}
label={labelAddressBook}
value={`${match.url}/address-book`}
/>
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={SETTINGS_TAB_BTN_TEST_ID}
label={labelSettings}
value={`${match.url}/settings`}
/>
</Tabs>
<Hairline color={border} style={{ marginTop: '-2px' }} />
<Switch>
<Route
exact
path={`${match.path}/balances/:assetType?`}
render={() => (
<Balances
activateAssetsByBalance={activateAssetsByBalance}
activateTokensByBalance={activateTokensByBalance}
activeTokens={activeTokens}
blacklistedTokens={blacklistedTokens}
currencySelected={currencySelected}
currencyValues={currencyValues}
featuresEnabled={featuresEnabled}
fetchCurrencyValues={fetchCurrencyValues}
fetchTokens={fetchTokens}
granted={granted}
safeAddress={address}
safeName={name}
tokens={tokens}
/>
)}
/>
<Route
exact
path={`${match.path}/transactions`}
render={() => (
<Transactions
cancellationTransactions={cancellationTransactions}
createTransaction={createTransaction}
currentNetwork={network}
granted={granted}
nonce={safe.nonce}
owners={safe.owners}
processTransaction={processTransaction}
safeAddress={address}
threshold={safe.threshold}
transactions={transactions}
userAddress={userAddress}
/>
)}
/>
<Route exact path={`${match.path}/apps`} render={renderAppsTab} />
<Route
exact
path={`${match.path}/settings`}
render={() => (
<Settings
addressBook={addressBook}
createTransaction={createTransaction}
etherScanLink={etherScanLink}
granted={granted}
network={network}
owners={safe.owners}
safe={safe}
safeAddress={address}
safeName={name}
threshold={safe.threshold}
updateAddressBookEntry={updateAddressBookEntry}
updateSafe={updateSafe}
userAddress={userAddress}
/>
)}
/>
<Route exact path={`${match.path}/address-book`} render={() => <AddressBookTable />} />
<Redirect to={`${match.path}/balances`} />
</Switch>
<SendModal
activeScreenType="chooseTxType"
isOpen={sendFunds.isOpen}
onClose={hideSendFunds}
selectedToken={sendFunds.selectedToken}
/>
<Modal
description="Receive Tokens Form"
handleClose={onHide('Receive')}
open={showReceive}
paperClassName={classes.receiveModal}
title="Receive Tokens"
>
<Receive onClose={onHide('Receive')} safeAddress={address} safeName={name} />
</Modal>
{modal.isOpen && <GenericModal {...modal} onClose={closeGenericModal} />}
</>
)
}
export default withStyles(styles)(withRouter(Layout))

View File

@ -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 (
<Block className={classes.container} margin="xl">
<Row className={classes.userInfo}>
<Identicon address={address} diameter={50} />
<Block className={classes.name}>
<Row>
<Heading className={classes.nameText} color="primary" tag="h2" testId={SAFE_VIEW_NAME_HEADING_TEST_ID}>
{name}
</Heading>
{!granted && <Block className={classes.readonly}>Read Only</Block>}
</Row>
<Block className={classes.user} justify="center">
<Paragraph className={classes.address} color="disabled" noMargin size="md">
{address}
</Paragraph>
<CopyBtn content={address} />
<EtherscanBtn type="address" value={address} />
</Block>
</Block>
</Row>
<Block className={classes.balance}>
<Button
className={classes.send}
color="primary"
disabled={!granted}
onClick={() => showSendFunds('Ether')}
size="small"
variant="contained"
>
<CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
Send
</Button>
<Button
className={classes.receive}
color="primary"
onClick={onShow('Receive')}
size="small"
variant="contained"
>
<CallReceived alt="Receive Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
Receive
</Button>
</Block>
</Block>
)
}
export default withStyles(styles)(LayoutHeader)

View File

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

View File

@ -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 (
<>
<SettingsIcon />
<Badge
badgeContent=""
color="error"
invisible={!needsUpdate || !granted}
style={{ paddingRight: '10px' }}
variant="dot"
>
Settings
</Badge>
</>
)
}
export default SettingsTab

View File

@ -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 = (
<>
<BalancesIcon />
Assets
</>
)
const labelAddressBook = (
<>
<AddressBookIcon />
Address Book
</>
)
const labelApps = (
<>
<AppsIcon />
Apps
</>
)
const labelTransactions = (
<>
<TransactionsIcon />
Transactions
</>
)
return (
<Tabs
indicatorColor="secondary"
onChange={handleCallToRouter}
textColor="secondary"
value={tabsValue(match)}
variant="scrollable"
>
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={BALANCES_TAB_BTN_TEST_ID}
label={labelBalances}
value={`${match.url}/balances`}
/>
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={TRANSACTIONS_TAB_BTN_TEST_ID}
label={labelTransactions}
value={`${match.url}/transactions`}
/>
{process.env.REACT_APP_ENV !== 'production' && (
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={TRANSACTIONS_TAB_BTN_TEST_ID}
label={labelApps}
value={`${match.url}/apps`}
/>
)}
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={ADDRESS_BOOK_TAB_BTN_TEST_ID}
label={labelAddressBook}
value={`${match.url}/address-book`}
/>
<Tab
classes={{
selected: classes.tabWrapperSelected,
wrapper: classes.tabWrapper,
}}
data-testid={SETTINGS_TAB_BTN_TEST_ID}
label={<SettingsTab />}
value={`${match.url}/settings`}
/>
</Tabs>
)
}
export default withStyles(styles)(withRouter(TabsComponent))

View File

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

View File

@ -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 <NoSafe provider={provider} text="Safe not found" />
}
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 (
<>
<LayoutHeader onShow={onShow} showSendFunds={showSendFunds} />
<TabsComponent />
<Hairline color={border} style={{ marginTop: '-2px' }} />
<Switch>
<Route exact path={`${match.path}/balances/:assetType?`} render={() => wrapInSuspense(<Balances />, null)} />
<Route exact path={`${match.path}/transactions`} render={() => wrapInSuspense(<TxsTable />, null)} />
{process.env.REACT_APP_ENV !== 'production' && (
<Route
exact
path={`${match.path}/apps`}
render={() => wrapInSuspense(<Apps closeModal={closeGenericModal} openModal={openGenericModal} />, null)}
/>
)}
<Route exact path={`${match.path}/settings`} render={() => wrapInSuspense(<Settings />, null)} />
<Route exact path={`${match.path}/address-book`} render={() => wrapInSuspense(<AddressBookTable />, null)} />
<Redirect to={`${match.path}/balances`} />
</Switch>
<SendModal
activeScreenType="chooseTxType"
isOpen={sendFunds.isOpen}
onClose={hideSendFunds}
selectedToken={sendFunds.selectedToken}
/>
<Modal
description="Receive Tokens Form"
handleClose={onHide('Receive')}
open={showReceive}
paperClassName={classes.receiveModal}
title="Receive Tokens"
>
<Receive onClose={onHide('Receive')} />
</Modal>
{modal.isOpen && <GenericModal {...modal} onClose={closeGenericModal} />}
</>
)
}
export default withRouter(Layout)

View File

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

View File

@ -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<Owner>,
threshold: number,
addSafeOwner: Function,
createTransaction: Function,
enqueueSnackbar: Function,
closeSnackbar: Function,
}
@ -45,13 +42,13 @@ export const sendAddOwner = async (
ownersOld: List<Owner>,
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({
const txHash = await dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
@ -59,29 +56,20 @@ export const sendAddOwner = async (
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<ActiveScreen>('selectOwner')
const [values, setValues] = useState<Object>({})
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' && <OwnerForm onClose={onClose} onSubmit={ownerSubmitted} owners={owners} />}
{activeScreen === 'selectOwner' && <OwnerForm onClose={onClose} onSubmit={ownerSubmitted} />}
{activeScreen === 'selectThreshold' && (
<ThresholdForm
onClickBack={onClickBack}
onClose={onClose}
onSubmit={thresholdSubmitted}
owners={owners}
threshold={threshold}
/>
<ThresholdForm onClickBack={onClickBack} onClose={onClose} onSubmit={thresholdSubmitted} />
)}
{activeScreen === 'reviewAddOwner' && (
<ReviewAddOwner
onClickBack={onClickBack}
onClose={onClose}
onSubmit={onAddOwner}
owners={owners}
safeAddress={safeAddress}
safeName={safeName}
values={values}
/>
<ReviewAddOwner onClickBack={onClickBack} onClose={onClose} onSubmit={onAddOwner} values={values} />
)}
</>
</Modal>

View File

@ -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<Owner>,
}
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 (

View File

@ -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<Owner>,
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<string>('< 0.001')
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
const owners = useSelector(safeOwnersSelector)
useEffect(() => {
let isCurrent = true
const estimateGas = async () => {

View File

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

View File

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

View File

@ -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<Owner>,
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<Owner>,
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,7 +63,8 @@ export const sendRemoveOwner = async (
.removeOwner(prevAddress, ownerAddressToRemove, values.threshold)
.encodeABI()
const txHash = await createTransaction({
const txHash = await dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
@ -72,31 +72,21 @@ export const sendRemoveOwner = async (
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<ActiveScreen>('checkOwner')
const [values, setValues] = useState<Object>({})
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 = ({
<CheckOwner onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
)}
{activeScreen === 'selectThreshold' && (
<ThresholdForm
onClickBack={onClickBack}
onClose={onClose}
onSubmit={thresholdSubmitted}
owners={owners}
threshold={threshold}
/>
<ThresholdForm onClickBack={onClickBack} onClose={onClose} onSubmit={thresholdSubmitted} />
)}
{activeScreen === 'reviewRemoveOwner' && (
<ReviewRemoveOwner
@ -168,9 +151,6 @@ const RemoveOwner = ({
onSubmit={onRemoveOwner}
ownerAddress={ownerAddress}
ownerName={ownerName}
owners={owners}
safeAddress={safeAddress}
safeName={safeName}
values={values}
/>
)}

View File

@ -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<Owner>,
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<string>('< 0.001')
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
const owners = useSelector(safeOwnersSelector)
useEffect(() => {
let isCurrent = true

View File

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

View File

@ -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<Owner>,
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,7 +54,8 @@ export const sendReplaceOwner = async (
.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress)
.encodeABI()
const txHash = await createTransaction({
const txHash = await dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
@ -70,37 +63,27 @@ export const sendReplaceOwner = async (
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
})
}),
)
if (txHash && safe.threshold === 1) {
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<ActiveScreen>('checkOwner')
const [values, setValues] = useState<Object>({})
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' && (
<OwnerForm
onClose={onClose}
onSubmit={ownerSubmitted}
ownerAddress={ownerAddress}
ownerName={ownerName}
owners={owners}
/>
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
)}
{activeScreen === 'reviewReplaceOwner' && (
<ReviewReplaceOwner
@ -170,10 +137,6 @@ const ReplaceOwner = ({
onSubmit={onReplaceOwner}
ownerAddress={ownerAddress}
ownerName={ownerName}
owners={owners}
safeAddress={safeAddress}
safeName={safeName}
threshold={threshold}
values={values}
/>
)}

View File

@ -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<Owner>,
}
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 (

View File

@ -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<Owner>,
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<string>('< 0.001')
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
const owners = useSelector(safeOwnersSelector)
const threshold = useSelector(safeThresholdSelector)
useEffect(() => {
let isCurrent = true

View File

@ -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<Owner>,
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<Props, State> {
}
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<Props, State> {
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<Props, State> {
</Row>
</>
)}
<AddOwnerModal
addSafeOwner={addSafeOwner}
createTransaction={createTransaction}
isOpen={showAddOwner}
onClose={this.onHide('AddOwner')}
owners={owners}
safeAddress={safeAddress}
safeName={safeName}
threshold={threshold}
userAddress={userAddress}
/>
<AddOwnerModal isOpen={showAddOwner} onClose={this.onHide('AddOwner')} />
<RemoveOwnerModal
createTransaction={createTransaction}
isOpen={showRemoveOwner}
onClose={this.onHide('RemoveOwner')}
ownerAddress={selectedOwnerAddress}
ownerName={selectedOwnerName}
owners={owners}
removeSafeOwner={removeSafeOwner}
safe={safe}
safeAddress={safeAddress}
safeName={safeName}
threshold={threshold}
userAddress={userAddress}
/>
<ReplaceOwnerModal
createTransaction={createTransaction}
isOpen={showReplaceOwner}
network={network}
onClose={this.onHide('ReplaceOwner')}
ownerAddress={selectedOwnerAddress}
ownerName={selectedOwnerName}
owners={owners}
replaceSafeOwner={replaceSafeOwner}
safe={safe}
safeAddress={safeAddress}
safeName={safeName}
threshold={threshold}
userAddress={userAddress}
/>
<EditOwnerModal
editSafeOwner={editSafeOwner}
isOpen={showEditOwner}
onClose={this.onHide('EditOwner')}
ownerAddress={selectedOwnerAddress}
owners={owners}
safeAddress={safeAddress}
selectedOwnerName={selectedOwnerName}
updateAddressBookEntry={updateAddressBookEntry}
/>
</>
)

View File

@ -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,12 +35,15 @@ 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) => (
const RemoveSafeComponent = ({ classes, isOpen, onClose }: Props) => {
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector)
const dispatch = useDispatch()
const etherScanLink = getEtherScanLink('address', safeAddress)
return (
<Modal
description="Remove the selected Safe"
handleClose={onClose}
@ -78,8 +84,8 @@ const RemoveSafeComponent = ({ classes, etherScanLink, isOpen, onClose, removeSa
<Hairline />
<Row className={classes.description}>
<Paragraph noMargin>
Removing a Safe only removes it from your interface. <b>It does not delete the Safe</b>. You can always add it
back using the Safe&apos;s address.
Removing a Safe only removes it from your interface. <b>It does not delete the Safe</b>. You can always add
it back using the Safe&apos;s address.
</Paragraph>
</Row>
</Block>
@ -92,7 +98,7 @@ const RemoveSafeComponent = ({ classes, etherScanLink, isOpen, onClose, removeSa
className={classes.buttonRemove}
minWidth={140}
onClick={() => {
removeSafe(safeAddress)
dispatch(removeSafe(safeAddress))
onClose()
history.push(SAFELIST_ADDRESS)
}}
@ -103,8 +109,7 @@ const RemoveSafeComponent = ({ classes, etherScanLink, isOpen, onClose, removeSa
</Button>
</Row>
</Modal>
)
)
}
const RemoveSafeModal = withStyles(styles)(RemoveSafeComponent)
export default connect(undefined, actions)(RemoveSafeModal)
export const RemoveSafeModal = withStyles(styles)(RemoveSafeComponent)

View File

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

View File

@ -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<Owner>,
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,6 +46,7 @@ const ThresholdSettings = ({
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI()
dispatch(
createTransaction({
safeAddress,
to: safeAddress,
@ -57,7 +55,8 @@ const ThresholdSettings = ({
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
})
}),
)
}
return (

View File

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

View File

@ -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,112 +25,65 @@ 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<Owner>,
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<Props, State> {
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 (
return !owners ? (
<Loader />
) : (
<>
<Row className={classes.message}>
<ButtonLink className={classes.removeSafeBtn} color="error" onClick={this.onShow('RemoveSafe')} size="lg">
<ButtonLink className={classes.removeSafeBtn} color="error" onClick={onShow('RemoveSafe')} size="lg">
<Span className={classes.links}>Remove Safe</Span>
<Img alt="Trash Icon" className={classes.removeSafeIcon} src={RemoveSafeIcon} />
</ButtonLink>
<RemoveSafeModal
etherScanLink={etherScanLink}
isOpen={showRemoveSafe}
onClose={this.onHide('RemoveSafe')}
safeAddress={safeAddress}
safeName={safeName}
/>
<RemoveSafeModal isOpen={showRemoveSafe} onClose={onHide('RemoveSafe')} />
</Row>
<Block className={classes.root}>
<Col className={classes.menuWrapper} layout="column">
<Block className={classes.menu}>
<Row
className={cn(classes.menuOption, menuOptionIndex === 1 && classes.active)}
onClick={this.handleChange(1)}
>
<Row className={cn(classes.menuOption, menuOptionIndex === 1 && classes.active)} onClick={handleChange(1)}>
<SafeDetailsIcon />
<Badge
badgeContent=" "
color="error"
invisible={!safe.needsUpdate || !granted}
invisible={!needsUpdate || !granted}
style={{ paddingRight: '10px' }}
variant="dot"
>
@ -140,7 +93,7 @@ class Settings extends React.Component<Props, State> {
<Hairline className={classes.hairline} />
<Row
className={cn(classes.menuOption, menuOptionIndex === 2 && classes.active)}
onClick={this.handleChange(2)}
onClick={handleChange(2)}
testId={OWNERS_SETTINGS_TAB_TEST_ID}
>
<OwnersIcon />
@ -150,10 +103,7 @@ class Settings extends React.Component<Props, State> {
</Paragraph>
</Row>
<Hairline className={classes.hairline} />
<Row
className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)}
onClick={this.handleChange(3)}
>
<Row className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)} onClick={handleChange(3)}>
<RequiredConfirmationsIcon />
Policies
</Row>
@ -162,52 +112,14 @@ class Settings extends React.Component<Props, State> {
</Col>
<Col className={classes.contents} layout="column">
<Block className={classes.container}>
{menuOptionIndex === 1 && (
<SafeDetails
createTransaction={createTransaction}
safeAddress={safeAddress}
safeCurrentVersion={safe.currentVersion}
safeName={safeName}
safeNeedsUpdate={safe.needsUpdate}
updateSafe={updateSafe}
/>
)}
{menuOptionIndex === 2 && (
<ManageOwners
addressBook={addressBook}
addSafeOwner={addSafeOwner}
createTransaction={createTransaction}
editSafeOwner={editSafeOwner}
granted={granted}
network={network}
owners={owners}
removeSafeOwner={removeSafeOwner}
replaceSafeOwner={replaceSafeOwner}
safe={safe}
safeAddress={safeAddress}
safeName={safeName}
threshold={threshold}
updateAddressBookEntry={updateAddressBookEntry}
userAddress={userAddress}
/>
)}
{menuOptionIndex === 3 && (
<ThresholdSettings
createTransaction={createTransaction}
granted={granted}
owners={owners}
safeAddress={safeAddress}
threshold={threshold}
/>
)}
{menuOptionIndex === 1 && <SafeDetails />}
{menuOptionIndex === 2 && <ManageOwners addressBook={addressBook} granted={granted} owners={owners} />}
{menuOptionIndex === 3 && <ThresholdSettings />}
</Block>
</Col>
</Block>
</>
)
}
}
const settingsComponent = withStyles(styles)(Settings)
export default connect(undefined, actions)(settingsComponent)
export default withStyles(styles)(Settings)

View File

@ -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<boolean>(canExecute)
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx)
@ -117,6 +116,7 @@ const ApproveTxModal = ({
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
const approveTx = () => {
dispatch(
processTransaction({
safeAddress,
tx,
@ -125,7 +125,8 @@ const ApproveTxModal = ({
enqueueSnackbar,
closeSnackbar,
approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted,
})
}),
)
onClose()
}

View File

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

View File

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

View File

@ -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<string>('< 0.001')
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
useEffect(() => {
let isCurrent = true
const estimateGasCosts = async () => {
@ -66,6 +59,7 @@ const RejectTxModal = ({
}, [])
const sendReplacementTransaction = () => {
dispatch(
createTransaction({
safeAddress,
to: safeAddress,
@ -75,7 +69,8 @@ const RejectTxModal = ({
closeSnackbar,
txNonce: tx.nonce,
origin: tx.origin,
})
}),
)
onClose()
}

View File

@ -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<Owner>,
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<OpenModal>(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}
/>
)}
</Row>
@ -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' && (
<RejectTxModal
createTransaction={createTransaction}
isOpen
onClose={closeModal}
safeAddress={safeAddress}
tx={tx}
/>
)}
{openModal === 'rejectTx' && <RejectTxModal isOpen onClose={closeModal} tx={tx} />}
{openModal === 'executeRejectTx' && (
<ApproveTxModal
canExecute={canExecuteCancel}
isCancelTx
isOpen
onClose={closeModal}
processTransaction={processTransaction}
safeAddress={safeAddress}
threshold={threshold}
thresholdReached={cancelThresholdReached}
tx={cancelTx}
userAddress={userAddress}
/>
)}
</>

View File

@ -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<Transaction | IncomingTransaction>,
cancellationTransactions: List<Transaction>,
threshold: number,
owners: List<Owner>,
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<string | null>(null)
const cancellationTransactions = useSelector(safeCancellationTransactionsSelector)
const transactions = useSelector(extendedTransactionsSelector)
const handleTxExpand = (safeTxHash) => {
setExpandedTx((prevTx) => (prevTx === safeTxHash ? null : safeTxHash))
@ -156,18 +135,10 @@ const TxsTable = ({
<Collapse
cancelTx={row[TX_TABLE_RAW_CANCEL_TX_ID]}
component={ExpandedTxComponent}
createTransaction={createTransaction}
granted={granted}
in={expandedTx === row.tx.safeTxHash}
nonce={nonce}
owners={owners}
processTransaction={processTransaction}
safeAddress={safeAddress}
threshold={threshold}
timeout="auto"
tx={row[TX_TABLE_RAW_TX_ID]}
unmountOnExit
userAddress={userAddress}
/>
</TableCell>
</TableRow>

View File

@ -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<Transaction | IncomingTransaction>,
cancellationTransactions: List<Transaction>,
owners: List<Owner>,
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) => (
<TxsTable
cancellationTransactions={cancellationTransactions}
createTransaction={createTransaction}
currentNetwork={currentNetwork}
granted={granted}
nonce={nonce}
owners={owners}
processTransaction={processTransaction}
safeAddress={safeAddress}
threshold={threshold}
transactions={transactions}
userAddress={userAddress}
/>
)
export default Transactions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
class SafeView extends React.Component<Props, State> {
state = {
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,
}))
}
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)
const onHide = (action: Action) => () => {
setState((prevState) => ({
...prevState,
[`show${action}`]: false,
}))
}
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,
},
})
}))
}
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
const { sendFunds, showReceive } = state
return (
<Page>
<Layout
activateAssetsByBalance={activateAssetsByBalance}
activateTokensByBalance={activateTokensByBalance}
activeTokens={activeTokens}
addressBook={addressBook}
blacklistedTokens={blacklistedTokens}
cancellationTransactions={cancellationTransactions}
createTransaction={createTransaction}
currencySelected={currencySelected}
currencyValues={currencyValues}
fetchCurrencyValues={fetchCurrencyValues}
fetchTokens={fetchTokens}
granted={granted}
hideSendFunds={this.hideSendFunds}
network={network}
onHide={this.onHide}
onShow={this.onShow}
processTransaction={processTransaction}
provider={provider}
safe={safe}
hideSendFunds={hideSendFunds}
onHide={onHide}
onShow={onShow}
sendFunds={sendFunds}
showReceive={showReceive}
showSendFunds={this.showSendFunds}
tokens={tokens}
transactions={transactions}
updateAddressBookEntry={updateAddressBookEntry}
updateSafe={updateSafe}
userAddress={userAddress}
showSendFunds={showSendFunds}
/>
</Page>
)
}
}
export default connect<Object, Object, ?Function, ?Object>(selector, actions)(SafeView)
export default SafeView

View File

@ -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<Token>,
activeTokens: List<Token>,
blacklistedTokens: List<Token>,
userAddress: string,
network: string,
safeUrl: string,
currencySelected: string,
currencyValues: BalanceCurrencyType[],
transactions: List<Transaction | IncomingTransaction>,
cancellationTransactions: List<Transaction>,
addressBook: AddressBook,
}
const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): TransactionStatus => {
let txStatus
if (tx.executionTxHash) {
@ -112,7 +88,7 @@ export const extendedSafeTokensSelector: Selector<GlobalState, RouterProps, List
},
)
const extendedTransactionsSelector: Selector<
export const extendedTransactionsSelector: Selector<
GlobalState,
RouterProps,
List<Transaction | IncomingTransaction>,
@ -143,20 +119,3 @@ const extendedTransactionsSelector: Selector<
return List([...extendedTransactions, ...incomingTransactions])
},
)
export default createStructuredSelector<Object, *>({
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,
})

View File

@ -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<GlobalState>) => {
const fetchEtherBalance = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>, 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

View File

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

View File

@ -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<Token>, 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<Token>) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
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

View File

@ -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<Transaction> => {
export const buildTransactionFrom = async (
safeAddress: string,
tx: TxServiceModel,
knownTokens,
txTokenDecimals,
txTokenSymbol,
txTokenName,
code,
): Promise<Transaction> => {
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,23 +130,20 @@ 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)
symbol = tokenSymbol
decimals = tokenDecimals
} catch (e) {
// some contracts may implement the same methods as in ERC20 standard
@ -153,7 +152,6 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod
isSendTokenTx = false
customTx = true
}
}
const params = web3.eth.abi.decodeParameters(['address', 'uint256'], tx.data.slice(10))
decodedParams = {
@ -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<TxServiceModel> => [
},
]
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()),
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(),
]),
)
symbol = web3.utils.hexToString(tokenSymbol)
decimals = tokenDecimals
} catch (e) {
}
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
}
// if it's not DCD, then we fall to the default values
}
}
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<SafeTransactionsType> => {
export const loadSafeTransactions = async (safeAddress: string, getState: GetState): Promise<SafeTransactionsType> => {
let transactions: TxServiceModel[] = addMockSafeCreationTx(safeAddress)
try {
@ -310,9 +334,14 @@ export const loadSafeTransactions = async (safeAddress: string): Promise<SafeTra
}
}
const state = getState()
const knownTokens = state[TOKEN_REDUCER_ID]
const txsWithData = await batchRequestTxsData(transactions)
// In case that the etags don't match, we parse the new transactions and save them to the cache
const txsRecord: Array<RecordInstance<TransactionProps>> = 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<GlobalState>) => {
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>, 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

View File

@ -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<GlobalState, RouterProps, List<Transaction>
export const safeTransactionsSelector: TxSelectorType = createSelector(
transactionsSelector,
safeParamAddressSelector,
safeParamAddressFromStateSelector,
(transactions: TransactionsState, address: string): List<Transaction> => {
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<Transaction> => {
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<GlobalState, RouterProps, List<IncomingTransaction>>
export const safeIncomingTransactionsSelector: IncomingTxSelectorType = createSelector(
incomingTransactionsSelector,
safeParamAddressSelector,
safeParamAddressFromStateSelector,
(incomingTransactions: IncomingTransactionsState, address: string): List<IncomingTransaction> => {
if (!incomingTransactions) {
return List([])
@ -233,12 +229,6 @@ export const safeBlacklistedAssetsSelector: OutputSelector<GlobalState, RouterPr
},
)
export const safeActiveTokensSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
safes.get(safeAddress).get('activeTokens')
export const safeBlacklistedTokensSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
safes.get(safeAddress).get('blacklistedTokens')
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
safes.get(safeAddress).get('activeAssets')
@ -256,6 +246,63 @@ export const safeBalancesSelector: OutputSelector<GlobalState, RouterProps, Map<
},
)
export const safeNameSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
safeSelector,
(safe: Safe) => {
return safe ? safe.name : undefined
},
)
export const safeEthBalanceSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
safeSelector,
(safe: Safe) => {
return safe ? safe.ethBalance : undefined
},
)
export const safeNeedsUpdateSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
safeSelector,
(safe: Safe) => {
return safe ? safe.needsUpdate : undefined
},
)
export const safeCurrentVersionSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
safeSelector,
(safe: Safe) => {
return safe ? safe.currentVersion : undefined
},
)
export const safeThresholdSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
safeSelector,
(safe: Safe) => {
return safe ? safe.threshold : undefined
},
)
export const safeNonceSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
safeSelector,
(safe: Safe) => {
return safe ? safe.nonce : undefined
},
)
export const safeOwnersSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
safeSelector,
(safe: Safe) => {
return safe ? safe.owners : undefined
},
)
export const safeFeaturesEnabledSelector: OutputSelector<
GlobalState,
RouterProps,
Map<string, string>,
> = createSelector(safeSelector, (safe: Safe) => {
return safe ? safe.featuresEnabled : undefined
})
export const getActiveTokensAddressesForAllSafes: OutputSelector<GlobalState, any, Set<string>> = createSelector(
safesListSelector,
(safes: List<Safe>) => {
@ -285,9 +332,3 @@ export const getBlacklistedTokensAddressesForAllSafes: OutputSelector<GlobalStat
return addresses
},
)
export default createStructuredSelector<Object, *>({
safe: safeSelector,
tokens: safeActiveTokensSelector,
blacklistedTokens: safeBlacklistedTokensSelector,
})

View File

@ -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) => ({
<Provider store={store}>
<ConnectedRouter history={history}>
<PageFrame>
<React.Suspense fallback={<div />}>
<AppRoutes />
</React.Suspense>
{wrapInSuspense(<AppRoutes />, <div />)}
</PageFrame>
</ConnectedRouter>
</Provider>,

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

@ -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 <WrappedComponent {...props} />
}
return HOC
return { trackPage }
}

View File

@ -0,0 +1,3 @@
// @flow
import React from 'react'
export const wrapInSuspense = (component, fallback) => <React.Suspense fallback={fallback}>{component}</React.Suspense>