* Refactor Balances to functional component Removes Balances props in Layout.jsx * Replaces selectors that were using safeParamAddressSelector with safeParamAddressFromStateSelector to avoid the bug of non-loaded safe when called Also exports extendedTransactionsSelector to let the components use it * Refactors Transactions.jsx, replaces transactions.tsx with txsTable.jsx Removes all unused props from transactions.jsx Makes all childs of txsTable.jsx fetch the props they need instead of sharing all of them even if they are not used * Adds new selectors: safeNameSelector, safeNeedsUpdateSelector, safeCurrentVersionSelector * Removes unused props from layout to settings.jsx Removed props from the settings.jsx childs, now they fetch the values they need directly from the store * Removes unused redux connect * Adds safeEthBalanceSelector * Removes all the props from layout to settings * Refactor root and layout, extracts checkForUpdate and componentDidMount to components Extracts header and tabs from Layout.jsx * Makes routes.jsx use selectors instead of connect to unify the code * Loads tabs components using react suspense * Fixs warning when trying to update root component within loadStore.jsx Replaces safe with safeAddress selector in layout.jsx to reduce the unnecessary rendering Fixs setState in container.jsx * Fixs checkForUpdates fetchTokenBalances Adds FetchTokens.jsx in balances Adds safeFeaturesEnabledSelector * Fixs load addressbook action * Replaces safe with owners in manage owners * Improves buildSafe promises calls Improves the loadStorage calls * Fixs error "Uncaught (in promise) TypeError: Cannot read property 'includes' of undefined" * Extracts LoadStore from outside the router component to avoid re-loading the store Adds react lazy for Coins and Collectible tabs * Reduce the polling rate for fetching transactions * Removes unused actions.js Removes unused selector props * Fixes owners column selector usage * Fixes processTransaction usage * Optimize how the transaction list within the transactions tab are loaded * Fix lint error * Fix edit addressbook entry * Fixs show send coins modal * feature: batchRequest for incoming transactions * Fixs race condition on loadStore Fixs check for updates address null validation * Adds ensureOnceAsync for getGnosisSafeInstanceAt Adds batch request for safe params * Removes unnecessary checkAndUpdateSafe from checkForUpdates, now the safe gets updated when a transaction arrives * Fixs ensureOnce/memoization * refactor: make a generic generateBatchRequest method Create a generic method to easily build web3 batch requests * refactor: use `generateBatchRequest` in `getTokensBalances` * Removes .toJS in edit entry * Removes web3 from sendTransactions Removes pascal case FetchTokens * Replaces /layout import * Replaces CheckForUpdates component with useCheckForUpdates hook * Makes FetchTokens a hook * Removes getSafeParamsBatch, now the safes gets the data using the generic generateBatchRequests * Replaces INITIAL_STATUS with INITIAL_STATE * Replaces regex Adds spaces before return * Adds wrapInSuspense * Runs prettier * Makes checkAndUpdateSafe use generateBatchRequests * Makes checkAndUpdateSafe use generateBatchRequests * Fixs check for updates with null address * Fixs transaction details getNameFromAddressBook * Fixes zIndex between cookies banner and transaction details * fix: cast returned values to number Original code was expecting a BN instance, now it's a plain string. * Fix replace owner name * Fix race condition with adbk load * Fixes replaces owner * Fixs apps * Moved hooks into own folder * Moved LoadStore to routes.js Refactors LoadStore as Hook Move LoadStore to hooks folder * Moves useLoadStore to safes/index * Revert loadStore place change * Fixes bug with fetchEtherBalance that causes updateSafe to be called * remove useLoadStore, add useAnalytics hook * remove React.memo from SafeView * Reverts removing useLoadStore in order to maintain the consistency of the code * rewrite useLoadStore in a more hook-y way, rename it to useLoadSafe * Removes unnecessary setSafeLoaded * Removes unnecessary safeLoaded * fix: Coins values and balances The app was retrieving ERC20 tokens information from 3 different endpoints. - One from `balance/` to have the list updated - another from `balance/usd` to have the values in USD - the last one from the blockchain, to update the balances This was all simplified to `balance/usd`. Also, added a `currencyRate` to be updated when the currency is modified. The value calculation happens on a component level, so when the `balanceUsd` value is modified, the value is properly reflected on the screen. Refactored `activateTokensByBalance` to `fetchSafeTokens`, as this was doing quite more than just _activating_ and also added the `currencyList` calculation in it, so everything is updated when `balance/usd` endpoint is requested. * fix: Balance screen Collectibles weren't loading when clicking on the link Also, refactored setState usage, to properly update current state * fix: featuresEnabled undefined * fix: add/activate newly received tokens * fix: NaN Values in Coins for a newly loaded Safe Was failing to set a default value for the `currencyRate` * fix: Settings fails to load if `owners` is not loaded into store Added a `Loader` until the required Safe's data is loaded into store. * fix: prefetch txs data When building the Txs list, we requested data for every tx what was translated into several RPC calls. Now by _batchRequesting_ all the information on beforehand, Safe's loading is a bit faster. * fix: prevent requesting safe, when there's no safe available in the store * enhancement: fetch tokens when loading safe By doing this, when loading a safe in the balance screen we will have tokens immediately loaded into the coins list * fix: load collectibles when switch to collectibles screen Collectibles weren't loaded when clicking menu link for a newly loaded safe. Now every switch to the collectible's screen will trigger a fetch for collectibles. * fix: fetch only if safe is ready Co-authored-by: fernandomg <fernando.greco@gmail.com> Co-authored-by: Mikhail Mikheev <mmvsha73@gmail.com>
This commit is contained in:
parent
c0422bd7d1
commit
9d784922f2
|
@ -30,7 +30,7 @@ const useStyles = makeStyles({
|
|||
padding: '27px 15px',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
zIndex: '5',
|
||||
zIndex: '15',
|
||||
},
|
||||
content: {
|
||||
maxWidth: '100%',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable import/named */
|
||||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Selector, createSelector } from 'reselect'
|
||||
|
||||
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
|
||||
|
@ -35,15 +34,3 @@ export const getAddressBookListSelector: Selector<GlobalState, {}, List<AddressB
|
|||
return result
|
||||
},
|
||||
)
|
||||
|
||||
export const getNameFromAddressBook = (userAddress: string): string | null => {
|
||||
if (!userAddress) {
|
||||
return null
|
||||
}
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const result = addressBook.filter((addressBookItem) => addressBookItem.address === userAddress)
|
||||
if (result.size > 0) {
|
||||
return result.get(0).name
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -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>) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }),
|
||||
)
|
|
@ -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 }),
|
||||
)
|
||||
|
|
|
@ -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[],
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -23,7 +23,7 @@ export const ALTERNATIVE_TOKEN_ABI = [
|
|||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
|
@ -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)
|
|
@ -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',
|
|
@ -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
|
|
@ -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))
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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)
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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'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'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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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])
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
// @flow
|
||||
import loadAddressBookFromStorage from '~/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
||||
import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
|
||||
import fetchCurrencyValues from '~/logic/currencyValues/store/actions/fetchCurrencyValues'
|
||||
import addViewedSafe from '~/logic/currentSession/store/actions/addViewedSafe'
|
||||
import activateAssetsByBalance from '~/logic/tokens/store/actions/activateAssetsByBalance'
|
||||
import activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance'
|
||||
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
import fetchEtherBalance from '~/routes/safe/store/actions/fetchEtherBalance'
|
||||
import fetchLatestMasterContractVersion from '~/routes/safe/store/actions/fetchLatestMasterContractVersion'
|
||||
import fetchSafe, { checkAndUpdateSafe } from '~/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
import processTransaction from '~/routes/safe/store/actions/processTransaction'
|
||||
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||
|
||||
export type Actions = {
|
||||
fetchSafe: typeof fetchSafe,
|
||||
fetchTokenBalances: typeof fetchTokenBalances,
|
||||
createTransaction: typeof createTransaction,
|
||||
fetchTransactions: typeof fetchTransactions,
|
||||
updateSafe: typeof updateSafe,
|
||||
fetchCollectibles: typeof fetchCollectibles,
|
||||
fetchTokens: typeof fetchTokens,
|
||||
processTransaction: typeof processTransaction,
|
||||
fetchEtherBalance: typeof fetchEtherBalance,
|
||||
fetchLatestMasterContractVersion: typeof fetchLatestMasterContractVersion,
|
||||
activateTokensByBalance: typeof activateTokensByBalance,
|
||||
activateAssetsByBalance: typeof activateAssetsByBalance,
|
||||
checkAndUpdateSafe: typeof checkAndUpdateSafe,
|
||||
fetchCurrencyValues: typeof fetchCurrencyValues,
|
||||
loadAddressBook: typeof loadAddressBookFromStorage,
|
||||
updateAddressBookEntry: typeof updateAddressBookEntry,
|
||||
addViewedSafe: typeof addViewedSafe,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchSafe,
|
||||
fetchTokenBalances,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchCollectibles,
|
||||
fetchTokens,
|
||||
fetchTransactions,
|
||||
activateTokensByBalance,
|
||||
activateAssetsByBalance,
|
||||
updateSafe,
|
||||
fetchEtherBalance,
|
||||
fetchLatestMasterContractVersion,
|
||||
fetchCurrencyValues,
|
||||
checkAndUpdateSafe,
|
||||
loadAddressBook: loadAddressBookFromStorage,
|
||||
updateAddressBookEntry,
|
||||
addViewedSafe,
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
export const wrapInSuspense = (component, fallback) => <React.Suspense fallback={fallback}>{component}</React.Suspense>
|
Loading…
Reference in New Issue