pull from dev

This commit is contained in:
Mikhail Mikheev 2020-04-24 17:33:26 +04:00
commit c45ae160ff
84 changed files with 1725 additions and 2228 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

@ -56,7 +56,7 @@ export const getTxServiceUriFrom = (safeAddress: string) =>
`safes/${safeAddress}/transactions/`
export const getIncomingTxServiceUriTo = (safeAddress: string) =>
`safes/${safeAddress}/incoming-transactions/`
`safes/${safeAddress}/incoming-transfers/`
export const getRelayUrl = () => getConfig()[RELAY_API_URL]

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,25 +34,3 @@ export const getAddressBookListSelector: Selector<GlobalState, {}, List<AddressB
return result
},
)
export const nameFromAddressBookSelector = createSelector(
addressBookMapSelector,
safeParamAddressFromStateSelector,
(addressBook, address): string => {
const adbkEntry = addressBook.find((entry) => entry.address === address)
return adbkEntry ? adbkEntry.name : 'UNKNOWN'
},
)
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)
dispatch(addNftAssets(collectibles.nftAssets))
dispatch(addNftTokens(collectibles.nftTokens))
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))
dispatch(setCurrencySelected(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,17 +43,19 @@ const sendTransactions = (
)
.encodeABI()
return createTransaction({
safeAddress,
to: multiSendAddress,
valueInWei: 0,
txData: encodeMultiSendCalldata,
notifiedTransaction: 'STANDARD_TX',
enqueueSnackbar,
closeSnackbar,
operation: DELEGATE_CALL,
// navigateToTransactionsTab: false,
origin,
})
return dispatch(
createTransaction({
safeAddress,
to: multiSendAddress,
valueInWei: 0,
txData: encodeMultiSendCalldata,
notifiedTransaction: 'STANDARD_TX',
enqueueSnackbar,
closeSnackbar,
operation: DELEGATE_CALL,
// navigateToTransactionsTab: false,
origin,
}),
)
}
export default sendTransactions

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,53 +77,55 @@ const styles = () => ({
type Props = {
onClose: () => void,
classes: Object,
safeName: string,
safeAddress: string,
}
const Receive = ({ classes, onClose, safeAddress, safeName }: Props) => (
<>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin size="xl" weight="bolder">
Receive funds
</Paragraph>
<IconButton disableRipple onClick={onClose}>
<Close className={classes.close} />
</IconButton>
</Row>
<Hairline />
<Paragraph className={classes.annotation} noMargin size="lg">
This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send
ETH and ERC-20 tokens to this address!
</Paragraph>
<Col layout="column" middle="xs">
<Paragraph className={classes.safeName} noMargin size="lg" weight="bold">
{safeName}
</Paragraph>
<Block className={classes.qrContainer}>
<QRCode size={135} value={safeAddress} />
</Block>
<Block className={classes.addressContainer} justify="center">
<Identicon address={safeAddress} diameter={32} />
<Paragraph
className={classes.address}
onClick={() => {
copyToClipboard(safeAddress)
}}
>
{safeAddress}
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">
Receive funds
</Paragraph>
<CopyBtn content={safeAddress} />
<EtherscanBtn type="address" value={safeAddress} />
</Block>
</Col>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button color="primary" minWidth={130} onClick={onClose} variant="contained">
Done
</Button>
</Row>
</>
)
<IconButton disableRipple onClick={onClose}>
<Close className={classes.close} />
</IconButton>
</Row>
<Hairline />
<Paragraph className={classes.annotation} noMargin size="lg">
This is the address of your Safe. Deposit funds by scanning the QR code or copying the address below. Only send
ETH and ERC-20 tokens to this address!
</Paragraph>
<Col layout="column" middle="xs">
<Paragraph className={classes.safeName} noMargin size="lg" weight="bold">
{safeName}
</Paragraph>
<Block className={classes.qrContainer}>
<QRCode size={135} value={safeAddress} />
</Block>
<Block className={classes.addressContainer} justify="center">
<Identicon address={safeAddress} diameter={32} />
<Paragraph
className={classes.address}
onClick={() => {
copyToClipboard(safeAddress)
}}
>
{safeAddress}
</Paragraph>
<CopyBtn content={safeAddress} />
<EtherscanBtn type="address" value={safeAddress} />
</Block>
</Col>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button color="primary" minWidth={130} onClick={onClose} variant="contained">
Done
</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 `${balance} ${currencySelected}`
return true
}
return token.address === tokenAddress
})
if (!currencyValue) {
return ''
}
return null
const { balanceInBaseCurrency } = currencyValue
const balance = BigNumber(balanceInBaseCurrency).times(currencyRate).toFixed(2)
return `${balance} ${currencySelected}`
}
// eslint-disable-next-line max-len
export const getBalanceData = (
activeTokens: List<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,71 +38,52 @@ 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 = {
erc721Enabled: false,
subMenuOptions: [],
showToken: false,
showManageCollectibleModal: false,
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
showCoins: true,
showCollectibles: false,
showReceive: false,
}
props.fetchTokens()
}
const INITIAL_STATE: State = {
erc721Enabled: false,
subMenuOptions: [],
showToken: false,
showManageCollectibleModal: false,
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
showCoins: true,
showCollectibles: false,
showReceive: false,
}
static isCoinsLocation = /\/balances\/?$/
static isCollectiblesLocation = /\/balances\/collectibles$/
export const COINS_LOCATION_REGEX = /\/balances\/?$/
export const COLLECTIBLES_LOCATION_REGEX = /\/balances\/collectibles$/
componentDidMount(): void {
const { activateAssetsByBalance, activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props
fetchCurrencyValues(safeAddress)
activateTokensByBalance(safeAddress)
activateAssetsByBalance(safeAddress)
const Balances = (props: Props) => {
const [state, setState] = useState(INITIAL_STATE)
const showCollectibles = Balances.isCollectiblesLocation.test(history.location.pathname)
const showCoins = Balances.isCoinsLocation.test(history.location.pathname)
const address = useSelector(safeParamAddressFromStateSelector)
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
useFetchTokens()
useEffect(() => {
const showCollectibles = COLLECTIBLES_LOCATION_REGEX.test(history.location.pathname)
const showCoins = COINS_LOCATION_REGEX.test(history.location.pathname)
const subMenuOptions = [{ enabled: showCoins, legend: 'Coins', url: `${SAFELIST_ADDRESS}/${address}/balances` }]
if (!showCollectibles && !showCoins) {
history.replace(`${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances`)
history.replace(`${SAFELIST_ADDRESS}/${address}/balances`)
}
const subMenuOptions = [
{ enabled: showCoins, legend: 'Coins', url: `${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances` },
]
const erc721Enabled = this.props.featuresEnabled.includes('ERC721')
const erc721Enabled = featuresEnabled && featuresEnabled.includes('ERC721')
if (erc721Enabled) {
subMenuOptions.push({
enabled: showCollectibles,
legend: 'Collectibles',
url: `${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances/collectibles`,
url: `${SAFELIST_ADDRESS}/${address}/balances/collectibles`,
})
} else {
if (showCollectibles) {
@ -109,124 +91,129 @@ class Balances extends React.Component<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 {
erc721Enabled,
sendFunds,
showCoins,
showCollectibles,
showManageCollectibleModal,
showReceive,
showToken,
subMenuOptions,
} = this.state
const { activeTokens, classes, createTransaction, ethBalance, safeAddress, safeName } = this.props
const {
assetDivider,
assetTab,
assetTabActive,
assetTabs,
controls,
manageTokensButton,
receiveModal,
tokenControls,
} = props.classes
const {
erc721Enabled,
sendFunds,
showCoins,
showCollectibles,
showManageCollectibleModal,
showReceive,
showToken,
subMenuOptions,
} = state
return (
<>
<Row align="center" className={classes.controls}>
<Col className={classes.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} />}
<Link
className={enabled ? classes.assetTabActive : classes.assetTab}
data-testid={`${legend.toLowerCase()}'-assets-btn'`}
size="md"
to={url}
weight={enabled ? 'bold' : 'regular'}
>
{legend}
</Link>
</React.Fragment>
))}
</Col>
<Col className={classes.tokenControls} end="sm" sm={6} xs={12}>
{showCoins && <DropdownCurrency />}
<ButtonLink
className={classes.manageTokensButton}
onClick={erc721Enabled && showCollectibles ? this.onShow('ManageCollectibleModal') : this.onShow('Token')}
size="lg"
testId="manage-tokens-btn"
>
Manage List
</ButtonLink>
<Modal
description={
erc721Enabled ? 'Enable and disables assets to be listed' : 'Enable and disable tokens to be listed'
}
handleClose={showManageCollectibleModal ? this.onHide('ManageCollectibleModal') : this.onHide('Token')}
open={showToken || showManageCollectibleModal}
title="Manage List"
>
<Tokens
modalScreen={showManageCollectibleModal ? 'assetsList' : 'tokenList'}
onClose={showManageCollectibleModal ? this.onHide('ManageCollectibleModal') : this.onHide('Token')}
safeAddress={safeAddress}
/>
</Modal>
</Col>
</Row>
{showCoins && <Coins showReceiveFunds={this.onShow('Receive')} showSendFunds={this.showSendFunds} />}
{erc721Enabled && showCollectibles && <Collectibles />}
<SendModal
activeScreenType="sendFunds"
createTransaction={createTransaction}
ethBalance={ethBalance}
isOpen={sendFunds.isOpen}
onClose={this.hideSendFunds}
safeAddress={safeAddress}
safeName={safeName}
selectedToken={sendFunds.selectedToken}
tokens={activeTokens}
/>
<Modal
description="Receive Tokens Form"
handleClose={this.onHide('Receive')}
open={showReceive}
paperClassName={classes.receiveModal}
title="Receive Tokens"
>
<Receive onClose={this.onHide('Receive')} safeAddress={safeAddress} safeName={safeName} />
</Modal>
</>
)
}
return (
<>
<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={assetDivider} />}
<Link
className={enabled ? assetTabActive : assetTab}
data-testid={`${legend.toLowerCase()}'-assets-btn'`}
size="md"
to={url}
weight={enabled ? 'bold' : 'regular'}
>
{legend}
</Link>
</React.Fragment>
))}
</Col>
<Col className={tokenControls} end="sm" sm={6} xs={12}>
{showCoins && <DropdownCurrency />}
<ButtonLink
className={manageTokensButton}
onClick={erc721Enabled && showCollectibles ? () => onShow('ManageCollectibleModal') : () => onShow('Token')}
size="lg"
testId="manage-tokens-btn"
>
Manage List
</ButtonLink>
<Modal
description={
erc721Enabled ? 'Enable and disables assets to be listed' : 'Enable and disable tokens to be listed'
}
handleClose={showManageCollectibleModal ? () => onHide('ManageCollectibleModal') : () => onHide('Token')}
open={showToken || showManageCollectibleModal}
title="Manage List"
>
<Tokens
modalScreen={showManageCollectibleModal ? 'assetsList' : 'tokenList'}
onClose={showManageCollectibleModal ? () => onHide('ManageCollectibleModal') : () => onHide('Token')}
safeAddress={address}
/>
</Modal>
</Col>
</Row>
{showCoins && wrapInSuspense(<Coins showReceiveFunds={() => onShow('Receive')} showSendFunds={showSendFunds} />)}
{erc721Enabled && showCollectibles && wrapInSuspense(<Collectibles />)}
<SendModal
activeScreenType="sendFunds"
isOpen={sendFunds.isOpen}
onClose={hideSendFunds}
selectedToken={sendFunds.selectedToken}
/>
<Modal
description="Receive Tokens Form"
handleClose={() => onHide('Receive')}
open={showReceive}
paperClassName={receiveModal}
title="Receive Tokens"
>
<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,43 +42,34 @@ 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({
safeAddress,
to: safeAddress,
valueInWei: 0,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
})
const txHash = await dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
}),
)
if (txHash) {
addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress })
dispatch(addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress }))
}
}
const AddOwner = ({
addSafeOwner,
classes,
closeSnackbar,
createTransaction,
enqueueSnackbar,
isOpen,
onClose,
owners,
safeAddress,
safeName,
threshold,
}: Props) => {
const dispatch = useDispatch()
const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose }: Props) => {
const [activeScreen, setActiveScreen] = useState<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,39 +63,30 @@ export const sendRemoveOwner = async (
.removeOwner(prevAddress, ownerAddressToRemove, values.threshold)
.encodeABI()
const txHash = await createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
})
const txHash = await dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
}),
)
if (txHash && safe.threshold === 1) {
removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove })
if (txHash && threshold === 1) {
dispatch(removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove }))
}
}
const RemoveOwner = ({
classes,
closeSnackbar,
createTransaction,
enqueueSnackbar,
isOpen,
onClose,
ownerAddress,
ownerName,
owners,
removeSafeOwner,
safe,
safeAddress,
safeName,
threshold,
}: Props) => {
const RemoveOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, ownerAddress, ownerName }: Props) => {
const [activeScreen, setActiveScreen] = useState<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,45 +54,36 @@ export const sendReplaceOwner = async (
.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress)
.encodeABI()
const txHash = await createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
})
if (txHash && safe.threshold === 1) {
replaceSafeOwner({
const txHash = await dispatch(
createTransaction({
safeAddress,
oldOwnerAddress: ownerAddressToRemove,
ownerAddress: values.ownerAddress,
ownerName: values.ownerName,
})
to: safeAddress,
valueInWei: 0,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
}),
)
if (txHash && threshold === 1) {
dispatch(
replaceSafeOwner({
safeAddress,
oldOwnerAddress: ownerAddressToRemove,
ownerAddress: values.ownerAddress,
ownerName: values.ownerName,
}),
)
}
}
const ReplaceOwner = ({
classes,
closeSnackbar,
createTransaction,
enqueueSnackbar,
isOpen,
onClose,
ownerAddress,
ownerName,
owners,
replaceSafeOwner,
safe,
safeAddress,
safeName,
threshold,
}: Props) => {
const dispatch = useDispatch()
const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, ownerAddress, ownerName }: Props) => {
const [activeScreen, setActiveScreen] = useState<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,79 +35,81 @@ type Props = Actions & {
onClose: () => void,
classes: Object,
isOpen: boolean,
safeAddress: string,
etherScanLink: string,
safeName: string,
}
const RemoveSafeComponent = ({ classes, etherScanLink, isOpen, onClose, removeSafe, safeAddress, safeName }: Props) => (
<Modal
description="Remove the selected Safe"
handleClose={onClose}
open={isOpen}
paperClassName={classes.modal}
title="Remove Safe"
>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin weight="bolder">
Remove Safe
</Paragraph>
<IconButton disableRipple onClick={onClose}>
<Close className={classes.close} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.container}>
<Row className={classes.owner}>
<Col align="center" xs={1}>
<Identicon address={safeAddress} diameter={32} />
</Col>
<Col xs={11}>
<Block className={classNames(classes.name, classes.userName)}>
<Paragraph noMargin size="lg" weight="bolder">
{safeName}
</Paragraph>
<Block className={classes.user} justify="center">
<Paragraph color="disabled" noMargin size="md">
{safeAddress}
</Paragraph>
<Link className={classes.open} target="_blank" to={etherScanLink}>
<OpenInNew style={openIconStyle} />
</Link>
</Block>
</Block>
</Col>
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}
open={isOpen}
paperClassName={classes.modal}
title="Remove Safe"
>
<Row align="center" className={classes.heading} grow>
<Paragraph className={classes.manage} noMargin weight="bolder">
Remove Safe
</Paragraph>
<IconButton disableRipple onClick={onClose}>
<Close className={classes.close} />
</IconButton>
</Row>
<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.
</Paragraph>
<Block className={classes.container}>
<Row className={classes.owner}>
<Col align="center" xs={1}>
<Identicon address={safeAddress} diameter={32} />
</Col>
<Col xs={11}>
<Block className={classNames(classes.name, classes.userName)}>
<Paragraph noMargin size="lg" weight="bolder">
{safeName}
</Paragraph>
<Block className={classes.user} justify="center">
<Paragraph color="disabled" noMargin size="md">
{safeAddress}
</Paragraph>
<Link className={classes.open} target="_blank" to={etherScanLink}>
<OpenInNew style={openIconStyle} />
</Link>
</Block>
</Block>
</Col>
</Row>
<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.
</Paragraph>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={onClose}>
Cancel
</Button>
<Button
className={classes.buttonRemove}
minWidth={140}
onClick={() => {
dispatch(removeSafe(safeAddress))
onClose()
history.push(SAFELIST_ADDRESS)
}}
type="submit"
variant="contained"
>
Remove
</Button>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={onClose}>
Cancel
</Button>
<Button
className={classes.buttonRemove}
minWidth={140}
onClick={() => {
removeSafe(safeAddress)
onClose()
history.push(SAFELIST_ADDRESS)
}}
type="submit"
variant="contained"
>
Remove
</Button>
</Row>
</Modal>
)
</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,15 +46,17 @@ const ThresholdSettings = ({
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI()
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
})
dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
}),
)
}
return (

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,189 +25,101 @@ import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import Span from '~/components/layout/Span'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { type Owner } from '~/routes/safe/store/models/owner'
import type { Safe } from '~/routes/safe/store/models/safe'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { safeNeedsUpdate } from '~/logic/safe/utils/safeVersion'
import { grantedSelector } from '~/routes/safe/container/selector'
import { safeOwnersSelector } from '~/routes/safe/store/selectors'
export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab'
type State = {
showRemoveSafe: boolean,
menuOptionIndex: number,
}
type Props = Actions & {
addSafeOwner: Function,
addressBook: AddressBook,
type Props = {
classes: Object,
createTransaction: Function,
editSafeOwner: Function,
etherScanLink: string,
granted: boolean,
network: string,
owners: List<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 (
<>
<Row className={classes.message}>
<ButtonLink className={classes.removeSafeBtn} color="error" onClick={this.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}
/>
</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)}
return !owners ? (
<Loader />
) : (
<>
<Row className={classes.message}>
<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 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={handleChange(1)}>
<SafeDetailsIcon />
<Badge
badgeContent=" "
color="error"
invisible={!needsUpdate || !granted}
style={{ paddingRight: '10px' }}
variant="dot"
>
<SafeDetailsIcon />
<Badge
badgeContent=" "
color="error"
invisible={!safe.needsUpdate || !granted}
style={{ paddingRight: '10px' }}
variant="dot"
>
Safe details
</Badge>
</Row>
<Hairline className={classes.hairline} />
<Row
className={cn(classes.menuOption, menuOptionIndex === 2 && classes.active)}
onClick={this.handleChange(2)}
testId={OWNERS_SETTINGS_TAB_TEST_ID}
>
<OwnersIcon />
Owners
<Paragraph className={classes.counter} size="xs">
{owners.size}
</Paragraph>
</Row>
<Hairline className={classes.hairline} />
<Row
className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)}
onClick={this.handleChange(3)}
>
<RequiredConfirmationsIcon />
Policies
</Row>
<Hairline className={classes.hairline} />
</Block>
</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}
/>
)}
</Block>
</Col>
</Block>
</>
)
}
Safe details
</Badge>
</Row>
<Hairline className={classes.hairline} />
<Row
className={cn(classes.menuOption, menuOptionIndex === 2 && classes.active)}
onClick={handleChange(2)}
testId={OWNERS_SETTINGS_TAB_TEST_ID}
>
<OwnersIcon />
Owners
<Paragraph className={classes.counter} size="xs">
{owners.size}
</Paragraph>
</Row>
<Hairline className={classes.hairline} />
<Row className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)} onClick={handleChange(3)}>
<RequiredConfirmationsIcon />
Policies
</Row>
<Hairline className={classes.hairline} />
</Block>
</Col>
<Col className={classes.contents} layout="column">
<Block className={classes.container}>
{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,15 +116,17 @@ const ApproveTxModal = ({
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
const approveTx = () => {
processTransaction({
safeAddress,
tx,
userAddress,
notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
enqueueSnackbar,
closeSnackbar,
approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted,
})
dispatch(
processTransaction({
safeAddress,
tx,
userAddress,
notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
enqueueSnackbar,
closeSnackbar,
approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted,
}),
)
onClose()
}

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,16 +59,18 @@ const RejectTxModal = ({
}, [])
const sendReplacementTransaction = () => {
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
enqueueSnackbar,
closeSnackbar,
txNonce: tx.nonce,
origin: tx.origin,
})
dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
enqueueSnackbar,
closeSnackbar,
txNonce: tx.nonce,
origin: tx.origin,
}),
)
onClose()
}

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

@ -31,26 +31,30 @@ const typeToLabel = {
}
const TxType = ({ origin, txType }: { txType: TransactionType, origin: string | null }) => {
const isThirdPartyApp = txType === 'third-party-app'
const [loading, setLoading] = useState(true)
const [appInfo, setAppInfo] = useState()
const [forceCustom, setForceCustom] = useState(false)
useEffect(() => {
const getAppInfo = async () => {
const parsedOrigin = getAppInfoFromOrigin(origin)
if (!parsedOrigin) {
setForceCustom(true)
return
}
const appInfo = await getAppInfoFromUrl(parsedOrigin.url)
setAppInfo(appInfo)
setLoading(false)
}
if (!isThirdPartyApp) {
if (!origin) {
return
}
getAppInfo()
}, [txType])
if (!isThirdPartyApp) {
if (forceCustom || !origin) {
return <IconText iconUrl={typeToIcon[txType]} text={typeToLabel[txType]} />
}

View File

@ -75,7 +75,7 @@ const getTransactionTableData = (tx: Transaction, cancelTx: ?Transaction): Trans
} else if (tx.cancellationTx) {
txType = 'cancellation'
} else if (tx.customTx) {
txType = tx.origin ? 'third-party-app' : 'custom'
txType = 'custom'
} else if (tx.creationTx) {
txType = 'creation'
} else if (tx.upgradeTx) {

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 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 createTransaction from '~/routes/safe/store/actions/transactions/createTransaction'
import fetchTransactions from '~/routes/safe/store/actions/transactions/fetchTransactions'
import processTransaction from '~/routes/safe/store/actions/transactions/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 INITIAL_STATE = {
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
showReceive: false,
}
const SafeView = () => {
const [state, setState] = useState(INITIAL_STATE)
const safeAddress = useSelector(safeParamAddressFromStateSelector)
useLoadSafe(safeAddress)
useCheckForUpdates()
const onShow = (action: Action) => () => {
setState((prevState) => ({
...prevState,
[`show${action}`]: true,
}))
}
const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
class SafeView extends React.Component<Props, State> {
state = {
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
showReceive: false,
const onHide = (action: Action) => () => {
setState((prevState) => ({
...prevState,
[`show${action}`]: false,
}))
}
intervalId: IntervalID
longIntervalId: IntervalID
componentDidMount() {
const {
activeTokens,
addViewedSafe,
fetchCollectibles,
fetchCurrencyValues,
fetchLatestMasterContractVersion,
fetchSafe,
fetchTokenBalances,
fetchTokens,
fetchTransactions,
loadAddressBook,
safeUrl,
} = this.props
fetchLatestMasterContractVersion()
.then(() => fetchSafe(safeUrl))
.then(() => {
// The safe needs to be loaded before fetching the transactions
fetchTransactions(safeUrl)
addViewedSafe(safeUrl)
fetchCollectibles()
})
fetchTokenBalances(safeUrl, activeTokens)
// fetch tokens there to get symbols for tokens in TXs list
fetchTokens()
fetchCurrencyValues(safeUrl)
loadAddressBook()
this.intervalId = setInterval(() => {
this.checkForUpdates()
}, TIMEOUT)
this.longIntervalId = setInterval(() => {
fetchCollectibles()
}, TIMEOUT * 3)
}
componentDidUpdate(prevProps) {
const { activeTokens, fetchTransactions, safeUrl } = this.props
const oldActiveTokensSize = prevProps.activeTokens.size
if (oldActiveTokensSize > 0 && activeTokens.size > oldActiveTokensSize) {
this.checkForUpdates()
}
if (safeUrl !== prevProps.safeUrl) {
fetchTransactions(safeUrl)
}
}
componentWillUnmount() {
clearInterval(this.intervalId)
clearInterval(this.longIntervalId)
}
onShow = (action: Action) => () => {
this.setState(() => ({ [`show${action}`]: true }))
}
onHide = (action: Action) => () => {
this.setState(() => ({ [`show${action}`]: false }))
}
showSendFunds = (token: Token) => {
this.setState({
const showSendFunds = (token: Token) => {
setState((prevState) => ({
...prevState,
sendFunds: {
isOpen: true,
selectedToken: token,
},
})
}))
}
hideSendFunds = () => {
this.setState({
const hideSendFunds = () => {
setState((prevState) => ({
...prevState,
sendFunds: {
isOpen: false,
selectedToken: undefined,
},
})
}))
}
const { sendFunds, showReceive } = state
checkForUpdates() {
const {
activeTokens,
checkAndUpdateSafe,
fetchEtherBalance,
fetchTokenBalances,
fetchTransactions,
safe,
safeUrl,
} = this.props
checkAndUpdateSafe(safeUrl)
fetchTokenBalances(safeUrl, activeTokens)
fetchEtherBalance(safe)
fetchTransactions(safeUrl)
}
render() {
const { sendFunds, showReceive } = this.state
const {
activateAssetsByBalance,
activateTokensByBalance,
activeTokens,
addressBook,
blacklistedTokens,
cancellationTransactions,
createTransaction,
currencySelected,
currencyValues,
fetchCurrencyValues,
fetchTokens,
granted,
network,
processTransaction,
provider,
safe,
tokens,
transactions,
updateAddressBookEntry,
updateSafe,
userAddress,
} = this.props
return (
<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}
sendFunds={sendFunds}
showReceive={showReceive}
showSendFunds={this.showSendFunds}
tokens={tokens}
transactions={transactions}
updateAddressBookEntry={updateAddressBookEntry}
updateSafe={updateSafe}
userAddress={userAddress}
/>
</Page>
)
}
return (
<Page>
<Layout
hideSendFunds={hideSendFunds}
onHide={onHide}
onShow={onShow}
sendFunds={sendFunds}
showReceive={showReceive}
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,31 +130,27 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod
}
}
let symbol = 'ETH'
let decimals = 18
let symbol = txTokenSymbol || 'ETH'
let decimals = txTokenDecimals || 18
let decodedParams
if (isSendTokenTx) {
if (isSendTokenTx && (txTokenSymbol === null || txTokenDecimals === null)) {
try {
const tokenInstance = await getTokenInfos(tx.to)
symbol = tokenInstance.symbol
decimals = tokenInstance.decimals
} catch (err) {
try {
const alternativeTokenInstance = new web3.eth.Contract(ALTERNATIVE_TOKEN_ABI, tx.to)
const [tokenSymbol, tokenDecimals] = await Promise.all([
alternativeTokenInstance.methods.symbol().call(),
alternativeTokenInstance.methods.decimals().call(),
])
const [tokenSymbol, tokenDecimals] = await Promise.all(
generateBatchRequests({
abi: ALTERNATIVE_TOKEN_ABI,
address: tx.to,
methods: ['symbol', 'decimals'],
}),
)
symbol = web3.utils.toAscii(tokenSymbol)
decimals = tokenDecimals
} catch (e) {
// some contracts may implement the same methods as in ERC20 standard
// we may falsely treat them as tokens, so in case we get any errors when getting token info
// we fallback to displaying custom transaction
isSendTokenTx = false
customTx = true
}
symbol = tokenSymbol
decimals = tokenDecimals
} catch (e) {
// some contracts may implement the same methods as in ERC20 standard
// we may falsely treat them as tokens, so in case we get any errors when getting token info
// we fallback to displaying custom transaction
isSendTokenTx = false
customTx = true
}
const params = web3.eth.abi.decodeParameters(['address', 'uint256'], tx.data.slice(10))
@ -161,9 +159,9 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod
value: params[1],
}
} else if (modifySettingsTx && tx.data) {
decodedParams = await decodeParamsFromSafeMethod(tx.data)
decodedParams = decodeParamsFromSafeMethod(tx.data)
} else if (customTx && tx.data) {
decodedParams = await decodeParamsFromSafeMethod(tx.data)
decodedParams = decodeParamsFromSafeMethod(tx.data)
}
return makeTransaction({
@ -227,36 +225,62 @@ const addMockSafeCreationTx = (safeAddress): Array<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()),
)
symbol = web3.utils.hexToString(tokenSymbol)
decimals = tokenDecimals
} catch (e) {
// this is a particular treatment for the DCD token, as it seems to lack of symbol and decimal methods
if (tx.tokenAddress && tx.tokenAddress.toLowerCase() === '0xe0b7927c4af23765cb51314a0e0521a9645f0e2a') {
symbol = 'DCD'
decimals = 9
}
// if it's not DCD, then we fall to the default values
}
}
web3Batch.execute()
return Promise.all(whenTxsValues)
}
const batchRequestIncomingTxsData = (txs: IncomingTxServiceModel[]) => {
const web3Batch = new web3.BatchRequest()
const whenTxsValues = txs.map((tx) => {
const methods = ['symbol', 'decimals', { method: 'getTransaction', args: [tx.transactionHash], type: 'eth' }]
return generateBatchRequests({
abi: ALTERNATIVE_TOKEN_ABI,
address: tx.tokenAddress,
batch: web3Batch,
context: tx,
methods,
})
})
web3Batch.execute()
return Promise.all(whenTxsValues).then((txsValues) =>
txsValues.map(([tx, symbol, decimals, { gas, gasPrice }]) => [
tx,
symbol === null ? 'ETH' : symbol,
decimals === null ? '18' : decimals,
bn(gas).div(gasPrice).toFixed(),
]),
)
}
export const buildIncomingTransactionFrom = ([tx, symbol, decimals, fee]: [
IncomingTxServiceModel,
string,
string,
string,
]) => {
// this is a particular treatment for the DCD token, as it seems to lack of symbol and decimal methods
if (tx.tokenAddress && tx.tokenAddress.toLowerCase() === '0xe0b7927c4af23765cb51314a0e0521a9645f0e2a') {
symbol = 'DCD'
decimals = '9'
}
const { transactionHash, ...incomingTx } = tx
@ -278,7 +302,7 @@ export type SafeTransactionsType = {
let etagSafeTransactions = null
let etagCachedSafeIncommingTransactions = null
export const loadSafeTransactions = async (safeAddress: string): Promise<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

@ -7,14 +7,7 @@ import { type Confirmation } from '~/routes/safe/store/models/confirmation'
export const OUTGOING_TX_TYPE = 'outgoing'
export type TransactionType =
| 'incoming'
| 'outgoing'
| 'settings'
| 'custom'
| 'creation'
| 'cancellation'
| 'third-party-app'
export type TransactionType = 'incoming' | 'outgoing' | 'settings' | 'custom' | 'creation' | 'cancellation'
export type TransactionStatus =
| 'awaiting_your_confirmation'
@ -24,7 +17,6 @@ export type TransactionStatus =
| 'cancelled'
| 'awaiting_execution'
| 'pending'
| 'third-party-app'
export type TransactionProps = {
nonce: ?number,

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>