pull from dev
This commit is contained in:
commit
c45ae160ff
|
@ -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>
|
||||
|
|
|
@ -56,7 +56,7 @@ export const getTxServiceUriFrom = (safeAddress: string) =>
|
|||
`safes/${safeAddress}/transactions/`
|
||||
|
||||
export const getIncomingTxServiceUriTo = (safeAddress: string) =>
|
||||
`safes/${safeAddress}/incoming-transactions/`
|
||||
`safes/${safeAddress}/incoming-transfers/`
|
||||
|
||||
export const getRelayUrl = () => getConfig()[RELAY_API_URL]
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable import/named */
|
||||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Selector, createSelector } from 'reselect'
|
||||
|
||||
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
|
||||
|
@ -35,25 +34,3 @@ export const getAddressBookListSelector: Selector<GlobalState, {}, List<AddressB
|
|||
return result
|
||||
},
|
||||
)
|
||||
|
||||
export const nameFromAddressBookSelector = createSelector(
|
||||
addressBookMapSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
(addressBook, address): string => {
|
||||
const adbkEntry = addressBook.find((entry) => entry.address === address)
|
||||
|
||||
return adbkEntry ? adbkEntry.name : 'UNKNOWN'
|
||||
},
|
||||
)
|
||||
|
||||
export const getNameFromAddressBook = (userAddress: string): string | null => {
|
||||
if (!userAddress) {
|
||||
return null
|
||||
}
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
const result = addressBook.filter((addressBookItem) => addressBookItem.address === userAddress)
|
||||
if (result.size > 0) {
|
||||
return result.get(0).name
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -31,26 +31,30 @@ const typeToLabel = {
|
|||
}
|
||||
|
||||
const TxType = ({ origin, txType }: { txType: TransactionType, origin: string | null }) => {
|
||||
const isThirdPartyApp = txType === 'third-party-app'
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [appInfo, setAppInfo] = useState()
|
||||
const [forceCustom, setForceCustom] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const getAppInfo = async () => {
|
||||
const parsedOrigin = getAppInfoFromOrigin(origin)
|
||||
if (!parsedOrigin) {
|
||||
setForceCustom(true)
|
||||
return
|
||||
}
|
||||
const appInfo = await getAppInfoFromUrl(parsedOrigin.url)
|
||||
setAppInfo(appInfo)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
if (!isThirdPartyApp) {
|
||||
if (!origin) {
|
||||
return
|
||||
}
|
||||
|
||||
getAppInfo()
|
||||
}, [txType])
|
||||
|
||||
if (!isThirdPartyApp) {
|
||||
if (forceCustom || !origin) {
|
||||
return <IconText iconUrl={typeToIcon[txType]} text={typeToLabel[txType]} />
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ const getTransactionTableData = (tx: Transaction, cancelTx: ?Transaction): Trans
|
|||
} else if (tx.cancellationTx) {
|
||||
txType = 'cancellation'
|
||||
} else if (tx.customTx) {
|
||||
txType = tx.origin ? 'third-party-app' : 'custom'
|
||||
txType = 'custom'
|
||||
} else if (tx.creationTx) {
|
||||
txType = 'creation'
|
||||
} else if (tx.upgradeTx) {
|
||||
|
|
|
@ -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 fetchEtherBalance from '~/routes/safe/store/actions/fetchEtherBalance'
|
||||
import fetchLatestMasterContractVersion from '~/routes/safe/store/actions/fetchLatestMasterContractVersion'
|
||||
import fetchSafe, { checkAndUpdateSafe } from '~/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||
import createTransaction from '~/routes/safe/store/actions/transactions/createTransaction'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/transactions/fetchTransactions'
|
||||
import processTransaction from '~/routes/safe/store/actions/transactions/processTransaction'
|
||||
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||
|
||||
export type Actions = {
|
||||
fetchSafe: typeof fetchSafe,
|
||||
fetchTokenBalances: typeof fetchTokenBalances,
|
||||
createTransaction: typeof createTransaction,
|
||||
fetchTransactions: typeof fetchTransactions,
|
||||
updateSafe: typeof updateSafe,
|
||||
fetchCollectibles: typeof fetchCollectibles,
|
||||
fetchTokens: typeof fetchTokens,
|
||||
processTransaction: typeof processTransaction,
|
||||
fetchEtherBalance: typeof fetchEtherBalance,
|
||||
fetchLatestMasterContractVersion: typeof fetchLatestMasterContractVersion,
|
||||
activateTokensByBalance: typeof activateTokensByBalance,
|
||||
activateAssetsByBalance: typeof activateAssetsByBalance,
|
||||
checkAndUpdateSafe: typeof checkAndUpdateSafe,
|
||||
fetchCurrencyValues: typeof fetchCurrencyValues,
|
||||
loadAddressBook: typeof loadAddressBookFromStorage,
|
||||
updateAddressBookEntry: typeof updateAddressBookEntry,
|
||||
addViewedSafe: typeof addViewedSafe,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchSafe,
|
||||
fetchTokenBalances,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchCollectibles,
|
||||
fetchTokens,
|
||||
fetchTransactions,
|
||||
activateTokensByBalance,
|
||||
activateAssetsByBalance,
|
||||
updateSafe,
|
||||
fetchEtherBalance,
|
||||
fetchLatestMasterContractVersion,
|
||||
fetchCurrencyValues,
|
||||
checkAndUpdateSafe,
|
||||
loadAddressBook: loadAddressBookFromStorage,
|
||||
updateAddressBookEntry,
|
||||
addViewedSafe,
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -7,14 +7,7 @@ import { type Confirmation } from '~/routes/safe/store/models/confirmation'
|
|||
|
||||
export const OUTGOING_TX_TYPE = 'outgoing'
|
||||
|
||||
export type TransactionType =
|
||||
| 'incoming'
|
||||
| 'outgoing'
|
||||
| 'settings'
|
||||
| 'custom'
|
||||
| 'creation'
|
||||
| 'cancellation'
|
||||
| 'third-party-app'
|
||||
export type TransactionType = 'incoming' | 'outgoing' | 'settings' | 'custom' | 'creation' | 'cancellation'
|
||||
|
||||
export type TransactionStatus =
|
||||
| 'awaiting_your_confirmation'
|
||||
|
@ -24,7 +17,6 @@ export type TransactionStatus =
|
|||
| 'cancelled'
|
||||
| 'awaiting_execution'
|
||||
| 'pending'
|
||||
| 'third-party-app'
|
||||
|
||||
export type TransactionProps = {
|
||||
nonce: ?number,
|
||||
|
|
|
@ -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