Merge pull request #1074 from gnosis/feature/#934-advanced-settings

(Feature) Advanced Settings - Safe Details
This commit is contained in:
Mikhail Mikheev 2020-07-18 17:39:58 +04:00 committed by GitHub
commit 57248c6985
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 998 additions and 227 deletions

10
.gitignore vendored
View File

@ -1,12 +1,12 @@
node_modules/
./build
/node_modules
/build
.DS_Store
yarn-error.log
.env*
.idea/
/.idea
dist
electron-builder.yml
.yalc/
/.yalc
yalc.lock
# testing
/coverage/
/coverage

View File

@ -160,7 +160,7 @@
},
"dependencies": {
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "^0.1.3",
"@gnosis.pm/safe-react-components": "^0.2.0",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid": "5.19.0",
"@material-ui/core": "4.11.0",
@ -227,6 +227,7 @@
"@types/node": "14.0.14",
"@types/react": "^16.9.32",
"@types/react-dom": "^16.9.6",
"@types/react-redux": "^7.1.9",
"@types/styled-components": "^5.1.0",
"@typescript-eslint/eslint-plugin": "3.5.0",
"@typescript-eslint/parser": "3.5.0",

View File

@ -42,7 +42,7 @@ interface Props {
ethBalance?: string
}
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props) => {
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactElement => {
return (
<Wrapper>
<div className="icon-section">

View File

@ -10,6 +10,7 @@ import Col from 'src/components/layout/Col'
import Paragraph from 'src/components/layout/Paragraph'
import { safesCountSelector } from 'src/routes/safe/store/selectors'
import { border, md, screenSm, sm, xs } from 'src/theme/variables'
import { AppReduxState } from 'src/store'
export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN'
@ -59,8 +60,4 @@ const SafeListHeader = ({ safesCount }) => {
)
}
export default connect(
// $FlowFixMe
(state) => ({ safesCount: safesCountSelector(state) }),
null,
)(SafeListHeader)
export default connect((state: AppReduxState) => ({ safesCount: safesCountSelector(state) }), null)(SafeListHeader)

View File

@ -18,6 +18,7 @@ import { WELCOME_ADDRESS } from 'src/routes/routes'
import setDefaultSafe from 'src/routes/safe/store/actions/setDefaultSafe'
import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
import { AppReduxState } from 'src/store'
const { useEffect, useMemo, useState } = React
@ -123,8 +124,7 @@ const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeActi
}
export default connect(
// $FlowFixMe
(state) => ({
(state: AppReduxState) => ({
safes: sortedSafeListSelector(state),
defaultSafe: defaultSafeSelector(state),
currentSafe: safeParamAddressFromStateSelector(state),

View File

@ -4,7 +4,11 @@ import TableRow from '@material-ui/core/TableRow'
import TableSortLabel from '@material-ui/core/TableSortLabel'
import * as React from 'react'
export const cellWidth = (width) => {
interface CellWidth {
maxWidth: string
}
export const cellWidth = (width: string | number): CellWidth | undefined => {
if (!width) {
return undefined
}

11
src/components/Table/types.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
export interface TableColumn {
align?: 'inherit' | 'left' | 'center' | 'right' | 'justify'
custom: boolean
disablePadding: boolean
id: string
label: string
order: boolean
static?: boolean
style?: any
width?: number
}

View File

@ -1,3 +1,5 @@
import { List } from 'immutable'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
@ -100,7 +102,7 @@ export const minMaxLength = (minLen, maxLen) => (value) =>
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
export const uniqueAddress = (addresses: string[]) =>
export const uniqueAddress = (addresses: string[] | List<string>) =>
simpleMemoize((value: string[]) => {
const addressAlreadyExists = addresses.some((address) => sameAddress(value, address))
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined

View File

@ -19,6 +19,7 @@ import Img from 'src/components/layout/Img'
import { getNetwork } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
import { networkSelector } from 'src/logic/wallets/store/selectors'
import { AppReduxState } from 'src/store'
const notificationStyles = {
success: {
@ -75,7 +76,7 @@ const PageFrame = ({ children, classes, currentNetwork }) => {
export default withStyles(notificationStyles)(
connect(
(state) => ({
(state: AppReduxState) => ({
currentNetwork: networkSelector(state),
}),
null,

View File

@ -57,14 +57,14 @@ export interface Collectibles {
}
class OpenSea {
_rateLimit = async () => {}
_rateLimit = async (): Promise<void> => {}
_endpointsUrls = {
[ETHEREUM_NETWORK.MAINNET]: 'https://api.opensea.io/api/v1',
[ETHEREUM_NETWORK.RINKEBY]: 'https://rinkeby-api.opensea.io/api/v1',
}
_fetch = async (url) => {
_fetch = async (url: string): Promise<Response> => {
// eslint-disable-next-line no-underscore-dangle
return fetch(url, {
headers: { 'X-API-KEY': OPENSEA_API_KEY || '' },
@ -76,7 +76,7 @@ class OpenSea {
* @param {object} options
* @param {number} options.rps - requests per second
*/
constructor(options) {
constructor(options: { rps: number }) {
// eslint-disable-next-line no-underscore-dangle
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
}

View File

@ -1,6 +1,5 @@
import { List } from 'immutable'
import { createSelector } from 'reselect'
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
import { AppReduxState } from 'src/store'
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
@ -9,22 +8,24 @@ import { safeActiveAssetsSelector } from 'src/routes/safe/store/selectors'
export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
export const nftAssetsListSelector = createSelector(nftAssetsSelector, (assets) => {
return assets ? List(Object.entries(assets).map((item) => item[1])) : List([])
export const nftAssetsListSelector = createSelector(nftAssetsSelector, (assets): NFTAsset[] => {
return assets ? Object.values(assets) : []
})
export const activeNftAssetsListSelector = createSelector(
nftAssetsListSelector,
safeActiveAssetsSelector,
(assets, activeAssetsList) => {
return assets.filter((asset: any) => activeAssetsList.has(asset.address))
(assets, activeAssetsList): NFTAsset[] => {
return assets.filter(({ address }) => activeAssetsList.has(address))
},
)
export const safeActiveSelectorMap = createSelector(activeNftAssetsListSelector, (activeAssets) => {
const assetsMap = {}
activeAssets.forEach((asset: any) => {
assetsMap[asset.address] = asset
})
return assetsMap
})
export const safeActiveSelectorMap = createSelector(
activeNftAssetsListSelector,
(activeAssets): NFTAssets => {
return activeAssets.reduce((acc, asset) => {
acc[asset.address] = asset
return acc
}, {})
},
)

View File

@ -43,6 +43,8 @@ export const SAFE_METHODS_NAMES = {
CHANGE_THRESHOLD: 'changeThreshold',
REMOVE_OWNER: 'removeOwner',
SWAP_OWNER: 'swapOwner',
ENABLE_MODULE: 'enableModule',
DISABLE_MODULE: 'disableModule',
}
const METHOD_TO_ID = {
@ -50,9 +52,11 @@ const METHOD_TO_ID = {
'0x0d582f13': SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD,
'0xf8dc5dd9': SAFE_METHODS_NAMES.REMOVE_OWNER,
'0x694e80c3': SAFE_METHODS_NAMES.CHANGE_THRESHOLD,
'0x610b5925': SAFE_METHODS_NAMES.ENABLE_MODULE,
'0xe009cfde': SAFE_METHODS_NAMES.DISABLE_MODULE,
}
type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom'
type DecodedValues = Array<{
@ -127,6 +131,29 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
}
}
// enableModule
case '0x610b5925': {
const decodedParameters = web3.eth.abi.decodeParameters(['address'], params)
return {
method: METHOD_TO_ID[methodId],
parameters: [
{ name: 'module', type: '', value: decodedParameters[0] },
],
}
}
// disableModule
case '0xe009cfde': {
const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address'], params)
return {
method: METHOD_TO_ID[methodId],
parameters: [
{ name: 'prevModule', type: '', value: decodedParameters[0] },
{ name: 'module', type: '', value: decodedParameters[1] },
],
}
}
default:
return null
}

View File

@ -4,10 +4,11 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import { ensureOnce } from 'src/utils/singleton'
import { simpleMemoize } from 'src/components/forms/validator'
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3'
import { getNetworkIdFrom, getWeb3 } from 'src/logic/wallets/getWeb3'
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { isProxyCode } from 'src/logic/contracts/historicProxyCode'
import Web3 from 'web3'
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
export const MULTI_SEND_ADDRESS = '0xB522a9f781924eD250A11C54105E51840B138AdD'
@ -19,7 +20,7 @@ export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB0
let proxyFactoryMaster
let safeMaster
const createGnosisSafeContract = (web3) => {
const createGnosisSafeContract = (web3: Web3): any => {
const gnosisSafe = contract(GnosisSafeSol)
gnosisSafe.setProvider(web3.currentProvider)
@ -72,7 +73,7 @@ export const getSafeMasterContract = async () => {
export const getSafeDeploymentTransaction = (safeAccounts, numConfirmations, userAccount) => {
const gnosisSafeData = safeMaster.contract.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI()
.encodeABI()
return proxyFactoryMaster.methods.createProxy(safeMaster.address, gnosisSafeData)
}
@ -94,11 +95,10 @@ export const estimateGasForDeployingSafe = async (
return gas * parseInt(gasPrice, 10)
}
export const getGnosisSafeInstanceAt = simpleMemoize(async (safeAddress) => {
export const getGnosisSafeInstanceAt = simpleMemoize(async (safeAddress): Promise<any> => {
const web3 = getWeb3()
const GnosisSafe = await getGnosisSafeContract(web3)
const gnosisSafe = await GnosisSafe.at(safeAddress)
return gnosisSafe
return GnosisSafe.at(safeAddress)
})
const cleanByteCodeMetadata = (bytecode) => {

View File

@ -10,7 +10,7 @@ export const FEATURES = [
{ name: 'ERC1155', validVersion: '>=1.1.1' },
]
export const safeNeedsUpdate = (currentVersion, latestVersion) => {
export const safeNeedsUpdate = (currentVersion: string, latestVersion: string): boolean => {
if (!currentVersion || !latestVersion) {
return false
}
@ -21,9 +21,10 @@ export const safeNeedsUpdate = (currentVersion, latestVersion) => {
return latest ? semverLessThan(current, latest) : false
}
export const getCurrentSafeVersion = (gnosisSafeInstance) => gnosisSafeInstance.VERSION()
export const getCurrentSafeVersion = (gnosisSafeInstance: { VERSION: () => Promise<string> }): Promise<string> =>
gnosisSafeInstance.VERSION()
export const enabledFeatures = (version) =>
export const enabledFeatures = (version: string): Array<string> =>
FEATURES.reduce((acc, feature) => {
if (semverSatisfies(version, feature.validVersion)) {
acc.push(feature.name)
@ -31,7 +32,16 @@ export const enabledFeatures = (version) =>
return acc
}, [])
export const checkIfSafeNeedsUpdate = async (gnosisSafeInstance, lastSafeVersion) => {
interface SafeVersionInfo {
current: string
latest: string
needUpdate: boolean
}
export const checkIfSafeNeedsUpdate = async (
gnosisSafeInstance: { VERSION: () => Promise<string> },
lastSafeVersion: string,
): Promise<SafeVersionInfo> => {
if (!gnosisSafeInstance || !lastSafeVersion) {
return null
}
@ -43,7 +53,7 @@ export const checkIfSafeNeedsUpdate = async (gnosisSafeInstance, lastSafeVersion
return { current, latest, needUpdate }
}
export const getCurrentMasterContractLastVersion = async () => {
export const getCurrentMasterContractLastVersion = async (): Promise<string> => {
const safeMaster = await getSafeMasterContract()
let safeMasterVersion
try {
@ -56,7 +66,7 @@ export const getCurrentMasterContractLastVersion = async () => {
return safeMasterVersion
}
export const getSafeVersionInfo = async (safeAddress) => {
export const getSafeVersionInfo = async (safeAddress: string): Promise<SafeVersionInfo> => {
try {
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
const lastSafeVersion = await getCurrentMasterContractLastVersion()

View File

@ -15,7 +15,7 @@ export const upgradeSafeToLatestVersion = async (safeAddress, createTransaction)
createTransaction({
safeAddress,
to: MULTI_SEND_ADDRESS,
valueInWei: 0,
valueInWei: '0',
txData: encodeMultiSendCallData,
notifiedTransaction: 'STANDARD_TX',
enqueueSnackbar: () => {},

View File

@ -25,7 +25,7 @@ const fetchSafeTokens = (safeAddress: string) => async (
): Promise<void> => {
try {
const state = getState()
const safe = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress])
const safe = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
const currentTokens = state[TOKEN_REDUCER_ID]
if (!safe) {

View File

@ -3,24 +3,24 @@ import { handleActions } from 'redux-actions'
import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken'
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens'
import { makeToken } from 'src/logic/tokens/store/model/token'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
export const TOKEN_REDUCER_ID = 'tokens'
export type TokenState = Map<string, Token>
export default handleActions(
{
[ADD_TOKENS]: (state, action) => {
[ADD_TOKENS]: (state: TokenState, action) => {
const { tokens } = action.payload
const newState = state.withMutations((map) => {
tokens.forEach((token) => {
return state.withMutations((map) => {
tokens.forEach((token: Token) => {
map.set(token.address, token)
})
})
return newState
},
[ADD_TOKEN]: (state, action) => {
[ADD_TOKEN]: (state: TokenState, action) => {
const { token } = action.payload
const { address: tokenAddress } = token

View File

@ -1,8 +1,9 @@
import { createSelector } from 'reselect'
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
import { AppReduxState } from 'src/store'
export const tokensSelector = (state) => state[TOKEN_REDUCER_ID]
export const tokensSelector = (state: AppReduxState): TokenState => state[TOKEN_REDUCER_ID]
export const tokenListSelector = createSelector(tokensSelector, (tokens) => tokens.toList())

View File

@ -15,7 +15,7 @@ import { Map } from 'immutable'
export const ETH_ADDRESS = '0x000'
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
export const getEthAsToken = (balance: string): Token => {
export const getEthAsToken = (balance: string | number): Token => {
return makeToken({
address: ETH_ADDRESS,
name: 'Ether',

View File

@ -9,15 +9,17 @@ import { provider as Provider } from 'web3-core'
import { ProviderProps } from './store/model/provider'
export const ETHEREUM_NETWORK = {
MAINNET: 'MAINNET',
MORDEN: 'MORDEN',
ROPSTEN: 'ROPSTEN',
RINKEBY: 'RINKEBY',
GOERLI: 'GOERLI',
KOVAN: 'KOVAN',
UNKNOWN: 'UNKNOWN',
MAINNET: 'MAINNET' as const,
MORDEN: 'MORDEN' as const,
ROPSTEN: 'ROPSTEN' as const,
RINKEBY: 'RINKEBY' as const,
GOERLI: 'GOERLI' as const,
KOVAN: 'KOVAN' as const,
UNKNOWN: 'UNKNOWN' as const,
}
export type EthereumNetworks = typeof ETHEREUM_NETWORK[keyof typeof ETHEREUM_NETWORK]
export const WALLET_PROVIDER = {
SAFE: 'SAFE',
METAMASK: 'METAMASK',
@ -37,17 +39,11 @@ export const WALLET_PROVIDER = {
}
export const ETHEREUM_NETWORK_IDS = {
// $FlowFixMe
1: ETHEREUM_NETWORK.MAINNET,
// $FlowFixMe
2: ETHEREUM_NETWORK.MORDEN,
// $FlowFixMe
3: ETHEREUM_NETWORK.ROPSTEN,
// $FlowFixMe
4: ETHEREUM_NETWORK.RINKEBY,
// $FlowFixMe
5: ETHEREUM_NETWORK.GOERLI,
// $FlowFixMe
42: ETHEREUM_NETWORK.KOVAN,
}

View File

@ -2,14 +2,17 @@ import { handleActions } from 'redux-actions'
import { ADD_PROVIDER } from 'src/logic/wallets/store/actions/addProvider'
import { REMOVE_PROVIDER } from 'src/logic/wallets/store/actions/removeProvider'
import { makeProvider } from 'src/logic/wallets/store/model/provider'
import { makeProvider, ProviderRecord, ProviderProps } from 'src/logic/wallets/store/model/provider'
export const PROVIDER_REDUCER_ID = 'providers'
export type ProviderState = ProviderRecord
export default handleActions(
{
[ADD_PROVIDER]: (state, { payload }) => makeProvider(payload),
[REMOVE_PROVIDER]: () => makeProvider(),
[ADD_PROVIDER]: (state: ProviderState, { payload }: { payload: ProviderProps }): ProviderState =>
makeProvider(payload),
[REMOVE_PROVIDER]: (): ProviderState => makeProvider(),
},
makeProvider(),
)

View File

@ -1,28 +1,33 @@
import { createSelector } from 'reselect'
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS } from 'src/logic/wallets/getWeb3'
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, EthereumNetworks } from 'src/logic/wallets/getWeb3'
import { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider'
import { AppReduxState } from 'src/store'
export const providerSelector = (state) => state[PROVIDER_REDUCER_ID]
export const providerSelector = (state: AppReduxState): ProviderState => state[PROVIDER_REDUCER_ID]
export const userAccountSelector = createSelector(providerSelector, (provider): string => {
export const userAccountSelector = createSelector(providerSelector, (provider: ProviderState): string => {
const account = provider.get('account')
return account || ''
})
export const providerNameSelector = createSelector(providerSelector, (provider): string => {
export const providerNameSelector = createSelector(providerSelector, (provider: ProviderState): string | undefined => {
const name = provider.get('name')
return name ? name.toLowerCase() : undefined
})
export const networkSelector = createSelector(providerSelector, (provider): string => {
const networkId = provider.get('network')
export const networkSelector = createSelector(
providerSelector,
(provider: ProviderState): EthereumNetworks => {
const networkId = provider.get('network')
return ETHEREUM_NETWORK_IDS[networkId] || ETHEREUM_NETWORK.UNKNOWN
},
)
return ETHEREUM_NETWORK_IDS[networkId] || ETHEREUM_NETWORK.UNKNOWN
})
export const loadedSelector = createSelector(providerSelector, (provider: ProviderState): boolean =>
provider.get('loaded'),
)
export const loadedSelector = createSelector(providerSelector, (provider) => provider.get('loaded'))
export const availableSelector = createSelector(providerSelector, (provider) => provider.get('available'))
export const availableSelector = createSelector(providerSelector, (provider: ProviderState): boolean =>
provider.get('available'),
)

View File

@ -1,13 +1,13 @@
//
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import { userAccountSelector } from '../selectors'
import { ProviderFactory } from './builder/index.builder'
import { AppReduxState } from 'src/store'
const providerReducerTests = () => {
describe('Provider Name Selector[userAccountSelector]', () => {
it('should return empty when no provider is loaded', () => {
// GIVEN
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider }
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider } as AppReduxState
// WHEN
const providerName = userAccountSelector(reduxStore)
@ -18,7 +18,7 @@ const providerReducerTests = () => {
it('should return empty when Metamask is loaded but not available', () => {
// GIVEN
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded }
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded } as AppReduxState
// WHEN
const providerName = userAccountSelector(reduxStore)
@ -29,7 +29,7 @@ const providerReducerTests = () => {
it('should return account when Metamask is loaded and available', () => {
// GIVEN
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable }
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable } as AppReduxState
// WHEN
const providerName = userAccountSelector(reduxStore)

View File

@ -1,13 +1,13 @@
//
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import { providerNameSelector } from '../selectors'
import { ProviderFactory } from './builder/index.builder'
import { AppReduxState } from 'src/store'
const providerReducerTests = () => {
describe('Provider Name Selector[providerNameSelector]', () => {
it('should return undefined when no provider is loaded', () => {
// GIVEN
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider }
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider } as AppReduxState
// WHEN
const providerName = providerNameSelector(reduxStore)
@ -18,7 +18,7 @@ const providerReducerTests = () => {
it('should return metamask when Metamask is loaded but not available', () => {
// GIVEN
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded }
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded } as AppReduxState
// WHEN
const providerName = providerNameSelector(reduxStore)
@ -29,7 +29,7 @@ const providerReducerTests = () => {
it('should return METAMASK when Metamask is loaded and available', () => {
// GIVEN
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable }
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable } as AppReduxState
// WHEN
const providerName = providerNameSelector(reduxStore)

View File

@ -37,7 +37,7 @@ const sendTransactions = (dispatch, safeAddress, txs, enqueueSnackbar, closeSnac
createTransaction({
safeAddress,
to: multiSendAddress,
valueInWei: 0,
valueInWei: '0',
txData: encodeMultiSendCallData,
notifiedTransaction: 'STANDARD_TX',
enqueueSnackbar,

View File

@ -90,7 +90,7 @@ const Collectibles = () => {
return (
<Card className={classes.cardOuter}>
<div className={classes.cardInner}>
{activeAssetsList.size ? (
{activeAssetsList.length ? (
activeAssetsList.map((nftAsset) => {
return (
<React.Fragment key={nftAsset.slug}>

View File

@ -45,7 +45,7 @@ const typePlaceholder = (text: string, type: string): string => {
return `${text} E.g.: ["first value", "second value", "third value"]`
}
const ArrayTypeInput = ({ name, text, type }: { name: string; text: string; type: string }): JSX.Element => (
const ArrayTypeInput = ({ name, text, type }: { name: string; text: string; type: string }): React.ReactElement => (
<TextareaField name={name} placeholder={typePlaceholder(text, type)} text={text} type="text" validate={validator} />
)

View File

@ -15,7 +15,7 @@ type Props = {
placeholder: string
}
const InputComponent = ({ type, keyValue, placeholder }: Props): JSX.Element => {
const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement => {
if (!type) {
return null
}

View File

@ -7,7 +7,7 @@ import InputComponent from './InputComponent'
import { generateFormFieldKey } from '../utils'
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
const RenderInputParams = (): JSX.Element => {
const RenderInputParams = (): React.ReactElement => {
const {
meta: { valid: validABI },
} = useField('abi', { subscription: { valid: true, value: true } })

View File

@ -130,12 +130,12 @@ const AssetsList = (props) => {
</Row>
<Hairline />
</Block>
{!nftAssetsList.size && (
{!nftAssetsList.length && (
<Block className={classes.progressContainer} justify="center">
<CircularProgress />
</Block>
)}
{nftAssetsList.size > 0 && (
{nftAssetsList.length > 0 && (
<MuiList className={classes.list}>
<FixedSizeList
height={413}

View File

@ -4,6 +4,7 @@ import { List } from 'immutable'
import { FIXED, buildOrderFieldFrom } from 'src/components/Table/sorting'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers'
import { TableColumn } from 'src/components/Table/types'
export const BALANCE_TABLE_ASSET_ID = 'asset'
export const BALANCE_TABLE_BALANCE_ID = 'balance'
@ -52,8 +53,8 @@ export const getBalanceData = (activeTokens, currencySelected, currencyValues, c
return rows
}
export const generateColumns = () => {
const assetColumn = {
export const generateColumns = (): List<TableColumn> => {
const assetColumn: TableColumn = {
id: BALANCE_TABLE_ASSET_ID,
order: true,
disablePadding: false,
@ -62,7 +63,7 @@ export const generateColumns = () => {
width: 250,
}
const balanceColumn = {
const balanceColumn: TableColumn = {
id: BALANCE_TABLE_BALANCE_ID,
align: 'right',
order: true,
@ -71,7 +72,7 @@ export const generateColumns = () => {
custom: false,
}
const actions = {
const actions: TableColumn = {
id: 'actions',
order: false,
disablePadding: false,
@ -80,7 +81,7 @@ export const generateColumns = () => {
static: true,
}
const value = {
const value: TableColumn = {
id: BALANCE_TABLE_VALUE_ID,
order: false,
label: 'Value',

View File

@ -0,0 +1,125 @@
import { Button, Text } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles'
import TableContainer from '@material-ui/core/TableContainer'
import styled from 'styled-components'
import cn from 'classnames'
import React from 'react'
import { useSelector } from 'react-redux'
import { generateColumns, ModuleAddressColumn, MODULES_TABLE_ADDRESS_ID } from './dataFetcher'
import RemoveModuleModal from './RemoveModuleModal'
import { styles } from './style'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { ModulePair } from 'src/routes/safe/store/models/safe'
import Table from 'src/components/Table'
import { TableCell, TableRow } from 'src/components/layout/Table'
import Block from 'src/components/layout/Block'
import Identicon from 'src/components/Identicon'
import Row from 'src/components/layout/Row'
const REMOVE_MODULE_BTN_TEST_ID = 'remove-module-btn'
const MODULES_ROW_TEST_ID = 'owners-row'
const useStyles = makeStyles(styles)
const AddressText = styled(Text)`
margin-left: 12px;
`
const TableActionButton = styled(Button)`
background-color: transparent;
&:hover {
background-color: transparent;
}
`
interface ModulesTableProps {
moduleData: ModuleAddressColumn | null
}
const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement => {
const classes = useStyles()
const columns = generateColumns()
const autoColumns = columns.filter(({ custom }) => !custom)
const granted = useSelector(grantedSelector)
const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false)
const hideRemoveModuleModal = () => setViewRemoveModuleModal(false)
const [selectedModule, setSelectedModule] = React.useState(null)
const triggerRemoveSelectedModule = (module: ModulePair): void => {
setSelectedModule(module)
setViewRemoveModuleModal(true)
}
return (
<>
<TableContainer>
<Table
columns={columns}
data={moduleData}
defaultFixed
defaultOrderBy={MODULES_TABLE_ADDRESS_ID}
disablePagination
label="Modules"
noBorder
size={moduleData.length}
>
{(sortedData) =>
sortedData.map((row, index) => (
<TableRow
className={cn(classes.hide, index >= 3 && index === sortedData.size - 1 && classes.noBorderBottom)}
data-testid={MODULES_ROW_TEST_ID}
key={index}
tabIndex={-1}
>
{autoColumns.map((column, index) => {
const columnId = column.id
const rowElement = row[columnId]
return (
<React.Fragment key={`${columnId}-${index}`}>
<TableCell align={column.align} component="td" key={columnId}>
{columnId === MODULES_TABLE_ADDRESS_ID ? (
<Block justify="left">
<Identicon address={rowElement[0]} diameter={32} />
<AddressText size="lg">{rowElement[0]}</AddressText>
</Block>
) : (
rowElement
)}
</TableCell>
<TableCell component="td">
<Row align="end" className={classes.actions}>
{granted && (
<TableActionButton
size="md"
iconType="delete"
color="error"
variant="outlined"
onClick={() => triggerRemoveSelectedModule(rowElement)}
data-testid={REMOVE_MODULE_BTN_TEST_ID}
>
{null}
</TableActionButton>
)}
</Row>
</TableCell>
</React.Fragment>
)
})}
</TableRow>
))
}
</Table>
</TableContainer>
{viewRemoveModuleModal && <RemoveModuleModal onClose={hideRemoveModuleModal} selectedModule={selectedModule} />}
</>
)
}
export default ModulesTable

View File

@ -0,0 +1,144 @@
import { Button } from '@gnosis.pm/safe-react-components'
import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import OpenInNew from '@material-ui/icons/OpenInNew'
import cn from 'classnames'
import { useSnackbar } from 'notistack'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import { styles } from './style'
import { ModulePair } from 'src/routes/safe/store/models/safe'
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import Modal from 'src/components/Modal'
import Row from 'src/components/layout/Row'
import Paragraph from 'src/components/layout/Paragraph'
import Hairline from 'src/components/layout/Hairline'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import Identicon from 'src/components/Identicon'
import Link from 'src/components/layout/Link'
import { getEtherScanLink } from 'src/logic/wallets/getWeb3'
import { md, secondary } from 'src/theme/variables'
const useStyles = makeStyles(styles)
const FooterWrapper = styled.div`
display: flex;
justify-content: space-around;
`
const openIconStyle = {
height: md,
color: secondary,
}
interface RemoveModuleModal {
onClose: () => void
selectedModule: ModulePair
}
const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): React.ReactElement => {
const classes = useStyles()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const dispatch = useDispatch()
const removeSelectedModule = async (): Promise<void> => {
try {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const [module, prevModule] = selectedModule
const txData = safeInstance.contract.methods.disableModule(prevModule, module).encodeABI()
dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: '0',
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,
closeSnackbar,
}),
)
} catch (e) {
console.error(`failed to remove the module ${selectedModule}`, e.message)
}
}
return (
<>
<Modal
description="Remove the selected Module"
handleClose={onClose}
paperClassName={classes.modal}
title="Remove Module"
open
>
<Row align="center" className={classes.modalHeading} grow>
<Paragraph className={classes.modalManage} noMargin weight="bolder">
Remove Module
</Paragraph>
<IconButton disableRipple onClick={onClose}>
<Close className={classes.modalClose} />
</IconButton>
</Row>
<Hairline />
<Block className={classes.modalContainer}>
<Row className={classes.modalOwner}>
<Col align="center" xs={1}>
<Identicon address={selectedModule[0]} diameter={32} />
</Col>
<Col xs={11}>
<Block className={cn(classes.modalName, classes.modalUserName)}>
<Paragraph noMargin size="lg" weight="bolder">
{selectedModule[0]}
</Paragraph>
<Block className={classes.modalUser} justify="center">
<Paragraph color="disabled" noMargin size="md">
{selectedModule[0]}
</Paragraph>
<Link
className={classes.modalOpen}
target="_blank"
to={getEtherScanLink('address', selectedModule[0])}
>
<OpenInNew style={openIconStyle} />
</Link>
</Block>
</Block>
</Col>
</Row>
<Hairline />
<Row className={classes.modalDescription}>
<Paragraph noMargin>
After removing this module, any feature or app that uses this module might no longer work. If this Safe
requires more then one signature, the module removal will have to be confirmed by other owners as well.
</Paragraph>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.modalButtonRow}>
<FooterWrapper>
<Button size="md" color="secondary" onClick={onClose}>
Cancel
</Button>
<Button color="error" size="md" variant="contained" onClick={removeSelectedModule}>
Remove
</Button>
</FooterWrapper>
</Row>
</Modal>
</>
)
}
export default RemoveModuleModal

View File

@ -0,0 +1,35 @@
import { List } from 'immutable'
import { TableColumn } from 'src/components/Table/types'
import { ModulePair } from 'src/routes/safe/store/models/safe'
export const MODULES_TABLE_ADDRESS_ID = 'address'
export const MODULES_TABLE_ACTIONS_ID = 'actions'
export type ModuleAddressColumn = { [MODULES_TABLE_ADDRESS_ID]: ModulePair }[]
export const getModuleData = (modulesList: ModulePair[] | null): ModuleAddressColumn | undefined => {
return modulesList?.map((modules) => ({
[MODULES_TABLE_ADDRESS_ID]: modules,
}))
}
export const generateColumns = (): List<TableColumn> => {
const addressColumn: TableColumn = {
align: 'left',
custom: false,
disablePadding: false,
id: MODULES_TABLE_ADDRESS_ID,
label: 'Address',
order: false,
}
const actionsColumn: TableColumn = {
custom: true,
disablePadding: false,
id: MODULES_TABLE_ACTIONS_ID,
label: '',
order: false,
}
return List([addressColumn, actionsColumn])
}

View File

@ -0,0 +1,93 @@
import { Loader, Text, theme, Title } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import { getModuleData } from './dataFetcher'
import { styles } from './style'
import ModulesTable from './ModulesTable'
import Block from 'src/components/layout/Block'
import { safeModulesSelector, safeNonceSelector } from 'src/routes/safe/store/selectors'
const useStyles = makeStyles(styles)
const InfoText = styled(Text)`
margin-top: 16px;
`
const Bold = styled.strong`
color: ${theme.colors.text};
`
const NoModuleLegend = (): React.ReactElement => (
<InfoText color="secondaryLight" size="xl">
No modules enabled
</InfoText>
)
const LoadingModules = (): React.ReactElement => {
const classes = useStyles()
return (
<Block className={classes.container}>
<Loader size="md" />
</Block>
)
}
const Advanced = (): React.ReactElement => {
const classes = useStyles()
const nonce = useSelector(safeNonceSelector)
const modules = useSelector(safeModulesSelector)
const moduleData = getModuleData(modules) ?? null
return (
<>
{/* Nonce */}
<Block className={classes.container}>
<Title size="xs" withoutMargin>
Safe Nonce
</Title>
<InfoText size="lg">
For security reasons, transactions made with the Safe need to be executed in order. The nonce shows you which
transaction was executed most recently. You can find the nonce for a transaction in the transaction details.
</InfoText>
<InfoText color="secondaryLight" size="xl">
Current Nonce: <Bold>{nonce}</Bold>
</InfoText>
</Block>
{/* Modules */}
<Block className={classes.container}>
<Title size="xs" withoutMargin>
Safe Modules
</Title>
<InfoText size="lg">
Modules allow you to customize the access-control logic of your Safe. Modules are potentially risky, so make
sure to only use modules from trusted sources. Learn more about modules{' '}
<a
href="https://docs.gnosis.io/safe/docs/contracts_architecture/#3-module-management"
rel="noopener noreferrer"
target="_blank"
>
here
</a>
.
</InfoText>
{moduleData === null ? (
<NoModuleLegend />
) : moduleData?.length === 0 ? (
<LoadingModules />
) : (
<ModulesTable moduleData={moduleData} />
)}
</Block>
</>
)
}
export default Advanced

View File

@ -0,0 +1,112 @@
import { createStyles } from '@material-ui/core'
import { background, border, error, fontColor, lg, md, secondaryText, sm, smallFontSize, xl } from 'src/theme/variables'
export const styles = createStyles({
title: {
padding: lg,
paddingBottom: 0,
},
hide: {
'&:hover': {
backgroundColor: '#fff3e2',
},
'&:hover $actions': {
visibility: 'initial',
},
},
actions: {
justifyContent: 'flex-end',
visibility: 'hidden',
minWidth: '100px',
},
noBorderBottom: {
'& > td': {
borderBottom: 'none',
},
},
annotation: {
paddingLeft: lg,
},
ownersText: {
color: secondaryText,
'& b': {
color: fontColor,
},
},
container: {
padding: lg,
},
buttonRow: {
padding: lg,
position: 'absolute',
left: 0,
bottom: 0,
boxSizing: 'border-box',
width: '100%',
justifyContent: 'flex-end',
borderTop: `2px solid ${border}`,
},
modifyBtn: {
height: xl,
fontSize: smallFontSize,
},
removeModuleIcon: {
marginLeft: lg,
cursor: 'pointer',
},
modalHeading: {
boxSizing: 'border-box',
justifyContent: 'space-between',
maxHeight: '75px',
padding: `${sm} ${lg}`,
},
modalContainer: {
minHeight: '369px',
},
modalManage: {
fontSize: lg,
},
modalClose: {
height: '35px',
width: '35px',
},
modalButtonRow: {
height: '84px',
justifyContent: 'center',
},
modalButtonRemove: {
color: '#fff',
backgroundColor: error,
height: '42px',
},
modalName: {
textOverflow: 'ellipsis',
overflow: 'hidden',
},
modalUserName: {
whiteSpace: 'nowrap',
},
modalOwner: {
backgroundColor: background,
padding: md,
alignItems: 'center',
},
modalUser: {
justifyContent: 'left',
},
modalDescription: {
padding: md,
},
modalOpen: {
paddingLeft: sm,
width: 'auto',
'&:hover': {
cursor: 'pointer',
},
},
modal: {
height: 'auto',
maxWidth: 'calc(100% - 30px)',
overflow: 'hidden',
},
})

View File

@ -34,7 +34,7 @@ export const sendAddOwner = async (values, safeAddress, ownersOld, enqueueSnackb
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
valueInWei: '0',
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,

View File

@ -53,7 +53,7 @@ export const sendRemoveOwner = async (
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
valueInWei: '0',
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,

View File

@ -47,7 +47,7 @@ export const sendReplaceOwner = async (
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
valueInWei: '0',
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,

View File

@ -1,4 +1,5 @@
import { List } from 'immutable'
import { TableColumn } from 'src/components/Table/types'
export const OWNERS_TABLE_NAME_ID = 'name'
export const OWNERS_TABLE_ADDRESS_ID = 'address'
@ -13,8 +14,8 @@ export const getOwnerData = (owners) => {
return rows
}
export const generateColumns = () => {
const nameColumn = {
export const generateColumns = (): List<TableColumn> => {
const nameColumn: TableColumn = {
id: OWNERS_TABLE_NAME_ID,
order: false,
disablePadding: false,
@ -24,7 +25,7 @@ export const generateColumns = () => {
align: 'left',
}
const addressColumn = {
const addressColumn: TableColumn = {
id: OWNERS_TABLE_ADDRESS_ID,
order: false,
disablePadding: false,
@ -33,7 +34,7 @@ export const generateColumns = () => {
align: 'left',
}
const actionsColumn = {
const actionsColumn: TableColumn = {
id: OWNERS_TABLE_ACTIONS_ID,
order: false,
disablePadding: false,

View File

@ -43,7 +43,7 @@ const ThresholdSettings = ({ classes, closeSnackbar, enqueueSnackbar }) => {
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
valueInWei: '0',
txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar,

View File

@ -1,17 +1,16 @@
import { IconText } from '@gnosis.pm/safe-react-components'
import Badge from '@material-ui/core/Badge'
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames'
import * as React from 'react'
import { useState } from 'react'
import { useSelector } from 'react-redux'
import Advanced from './Advanced'
import ManageOwners from './ManageOwners'
import { RemoveSafeModal } from './RemoveSafeModal'
import SafeDetails from './SafeDetails'
import ThresholdSettings from './ThresholdSettings'
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'
@ -25,9 +24,8 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import Span from 'src/components/layout/Span'
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
import { safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { safeOwnersSelector } from 'src/routes/safe/store/selectors'
import { safeNeedsUpdateSelector, safeOwnersSelector } from 'src/routes/safe/store/selectors'
export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab'
@ -36,10 +34,13 @@ const INITIAL_STATE = {
menuOptionIndex: 1,
}
const Settings = (props) => {
const useStyles = makeStyles(styles)
const Settings: React.FC = () => {
const classes = useStyles()
const [state, setState] = useState(INITIAL_STATE)
const owners = useSelector(safeOwnersSelector)
const needsUpdate = useSelector(safeNeedsUpdate)
const needsUpdate = useSelector(safeNeedsUpdateSelector)
const granted = useSelector(grantedSelector)
const addressBook = useSelector(getAddressBook)
@ -56,7 +57,6 @@ const Settings = (props) => {
}
const { menuOptionIndex, showRemoveSafe } = state
const { classes } = props
return !owners ? (
<Loader />
@ -73,7 +73,6 @@ const Settings = (props) => {
<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"
@ -81,7 +80,13 @@ const Settings = (props) => {
style={{ paddingRight: '10px' }}
variant="dot"
>
Safe details
<IconText
iconSize="sm"
textSize="xl"
iconType="info"
text="Safe Details"
color={menuOptionIndex === 1 ? 'primary' : 'secondary'}
/>
</Badge>
</Row>
<Hairline className={classes.hairline} />
@ -90,16 +95,36 @@ const Settings = (props) => {
onClick={handleChange(2)}
testId={OWNERS_SETTINGS_TAB_TEST_ID}
>
<OwnersIcon />
Owners
<IconText
iconSize="sm"
textSize="xl"
iconType="owners"
text="Owners"
color={menuOptionIndex === 2 ? 'primary' : 'secondary'}
/>
<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
<IconText
iconSize="sm"
textSize="xl"
iconType="requiredConfirmations"
text="Policies"
color={menuOptionIndex === 3 ? 'primary' : 'secondary'}
/>
</Row>
<Hairline className={classes.hairline} />
<Row className={cn(classes.menuOption, menuOptionIndex === 4 && classes.active)} onClick={handleChange(4)}>
<IconText
iconSize="sm"
textSize="xl"
iconType="settingsTool"
text="Advanced"
color={menuOptionIndex === 4 ? 'primary' : 'secondary'}
/>
</Row>
<Hairline className={classes.hairline} />
</Block>
@ -109,6 +134,7 @@ const Settings = (props) => {
{menuOptionIndex === 1 && <SafeDetails />}
{menuOptionIndex === 2 && <ManageOwners addressBook={addressBook} granted={granted} owners={owners} />}
{menuOptionIndex === 3 && <ThresholdSettings />}
{menuOptionIndex === 4 && <Advanced />}
</Block>
</Col>
</Block>
@ -116,4 +142,4 @@ const Settings = (props) => {
)
}
export default withStyles(styles as any)(Settings)
export default Settings

View File

@ -1,3 +1,5 @@
import { createStyles } from '@material-ui/core'
import {
background,
bolderFont,
@ -11,7 +13,7 @@ import {
xs,
} from 'src/theme/variables'
export const styles = () => ({
export const styles = createStyles({
root: {
backgroundColor: 'white',
borderRadius: sm,
@ -31,7 +33,7 @@ export const styles = () => ({
menuWrapper: {
display: 'flex',
flexDirection: 'row',
flexGrow: '0',
flexGrow: 0,
maxWidth: '100%',
[`@media (min-width: ${screenSm}px)`]: {
@ -43,7 +45,7 @@ export const styles = () => ({
borderBottom: `solid 2px ${border}`,
display: 'flex',
flexDirection: 'row',
flexGrow: '1',
flexGrow: 1,
height: '100%',
width: '100%',
@ -59,8 +61,8 @@ export const styles = () => ({
borderRight: `solid 1px ${border}`,
boxSizing: 'border-box',
cursor: 'pointer',
flexGrow: '1',
flexShrink: '1',
flexGrow: 1,
flexShrink: 1,
fontSize: '13px',
justifyContent: 'center',
lineHeight: '1.2',
@ -113,7 +115,7 @@ export const styles = () => ({
},
},
container: {
flexGrow: '1',
flexGrow: 1,
height: '100%',
position: 'relative',
},

View File

@ -53,7 +53,7 @@ const RejectTxModal = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClos
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: 0,
valueInWei: '0',
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
enqueueSnackbar,
closeSnackbar,

View File

@ -10,7 +10,7 @@ import Bold from 'src/components/layout/Bold'
import LinkWithRef from 'src/components/layout/Link'
import Paragraph from 'src/components/layout/Paragraph'
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
import { SAFE_METHODS_NAMES } from 'src/logic/contracts/methodIds'
import { SAFE_METHODS_NAMES, SafeMethods } from 'src/logic/contracts/methodIds'
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
@ -23,6 +23,8 @@ export const TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID = 'tx-description-change
export const TRANSACTIONS_DESC_SEND_TEST_ID = 'tx-description-send'
export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value'
export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data'
export const TRANSACTIONS_DESC_ADD_MODULE_TEST_ID = 'tx-description-add-module'
export const TRANSACTIONS_DESC_REMOVE_MODULE_TEST_ID = 'tx-description-remove-module'
export const TRANSACTIONS_DESC_NO_DATA = 'tx-description-no-data'
export const styles = () => ({
@ -43,7 +45,12 @@ export const styles = () => ({
},
})
const TransferDescription = ({ amount = '', recipient }) => {
interface TransferDescriptionProps {
amount: string
recipient: string
}
const TransferDescription = ({ amount = '', recipient }: TransferDescriptionProps): React.ReactElement => {
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
return (
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
@ -57,7 +64,11 @@ const TransferDescription = ({ amount = '', recipient }) => {
)
}
const RemovedOwner = ({ removedOwner }) => {
interface RemovedOwnerProps {
removedOwner: string
}
const RemovedOwner = ({ removedOwner }: RemovedOwnerProps): React.ReactElement => {
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, removedOwner))
return (
@ -72,7 +83,11 @@ const RemovedOwner = ({ removedOwner }) => {
)
}
const AddedOwner = ({ addedOwner }) => {
interface AddedOwnerProps {
addedOwner: string
}
const AddedOwner = ({ addedOwner }: AddedOwnerProps): React.ReactElement => {
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, addedOwner))
return (
@ -87,7 +102,11 @@ const AddedOwner = ({ addedOwner }) => {
)
}
const NewThreshold = ({ newThreshold }) => (
interface NewThresholdProps {
newThreshold: string
}
const NewThreshold = ({ newThreshold }: NewThresholdProps): React.ReactElement => (
<Block data-testid={TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID}>
<Bold>Change required confirmations:</Bold>
<Paragraph noMargin size="md">
@ -96,7 +115,43 @@ const NewThreshold = ({ newThreshold }) => (
</Block>
)
const SettingsDescription = ({ action, addedOwner, newThreshold, removedOwner }) => {
interface AddModuleProps {
module: string
}
const AddModule = ({ module }: AddModuleProps): React.ReactElement => (
<Block data-testid={TRANSACTIONS_DESC_ADD_MODULE_TEST_ID}>
<Bold>Add module:</Bold>
<EtherscanLink value={module} knownAddress={false} type="address" />
</Block>
)
interface RemoveModuleProps {
module: string
}
const RemoveModule = ({ module }: RemoveModuleProps): React.ReactElement => (
<Block data-testid={TRANSACTIONS_DESC_REMOVE_MODULE_TEST_ID}>
<Bold>Remove module:</Bold>
<EtherscanLink value={module} knownAddress={false} type="address" />
</Block>
)
interface SettingsDescriptionProps {
action: SafeMethods
addedOwner?: string
newThreshold?: string
removedOwner?: string
module?: string
}
const SettingsDescription = ({
action,
addedOwner,
newThreshold,
removedOwner,
module,
}: SettingsDescriptionProps): React.ReactElement => {
if (action === SAFE_METHODS_NAMES.REMOVE_OWNER && removedOwner && newThreshold) {
return (
<>
@ -128,6 +183,14 @@ const SettingsDescription = ({ action, addedOwner, newThreshold, removedOwner })
)
}
if (action === SAFE_METHODS_NAMES.ENABLE_MODULE && module) {
return <AddModule module={module} />
}
if (action === SAFE_METHODS_NAMES.DISABLE_MODULE && module) {
return <RemoveModule module={module} />
}
return (
<Block data-testid={TRANSACTIONS_DESC_NO_DATA}>
<Bold>No data available for current transaction</Bold>
@ -207,6 +270,7 @@ const TxDescription = ({ classes, tx }) => {
customTx,
data,
modifySettingsTx,
module,
newThreshold,
recipient,
removedOwner,
@ -221,6 +285,7 @@ const TxDescription = ({ classes, tx }) => {
addedOwner={addedOwner}
newThreshold={newThreshold}
removedOwner={removedOwner}
module={module}
/>
)}
{!upgradeTx && customTx && (

View File

@ -47,6 +47,14 @@ export const getTxData = (tx) => {
txData.action = SAFE_METHODS_NAMES.SWAP_OWNER
txData.removedOwner = oldOwner
txData.addedOwner = newOwner
} else if (tx.decodedParams[SAFE_METHODS_NAMES.ENABLE_MODULE]) {
const { module } = tx.decodedParams[SAFE_METHODS_NAMES.ENABLE_MODULE]
txData.action = SAFE_METHODS_NAMES.ENABLE_MODULE
txData.module = module
} else if (tx.decodedParams[SAFE_METHODS_NAMES.DISABLE_MODULE]) {
const { module } = tx.decodedParams[SAFE_METHODS_NAMES.DISABLE_MODULE]
txData.action = SAFE_METHODS_NAMES.DISABLE_MODULE
txData.module = module
}
} else if (tx.multiSendTx) {
txData.recipient = tx.recipient

View File

@ -8,9 +8,10 @@ import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAsse
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
import { Dispatch } from 'src/routes/safe/store/actions/types'
export const useFetchTokens = (safeAddress: string): void => {
const dispatch = useDispatch()
const dispatch = useDispatch<Dispatch>()
const location = useLocation()
useMemo(() => {

View File

@ -7,10 +7,11 @@ import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchLatestMasterContractVersion from 'src/routes/safe/store/actions/fetchLatestMasterContractVersion'
import fetchSafe from 'src/routes/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/routes/safe/store/actions/transactions/fetchTransactions'
import fetchSafeCreationTx from '../../store/actions/fetchSafeCreationTx'
import fetchSafeCreationTx from 'src/routes/safe/store/actions/fetchSafeCreationTx'
import { Dispatch } from 'src/routes/safe/store/actions/types'
export const useLoadSafe = (safeAddress) => {
const dispatch = useDispatch()
export const useLoadSafe = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>()
useEffect(() => {
const fetchData = () => {
@ -28,6 +29,7 @@ export const useLoadSafe = (safeAddress) => {
})
}
}
fetchData()
}, [dispatch, safeAddress])
}

View File

@ -17,7 +17,7 @@ const INITIAL_STATE = {
showReceive: false,
}
const SafeView = (): JSX.Element => {
const SafeView = (): React.ReactElement => {
const [state, setState] = useState(INITIAL_STATE)
const safeAddress = useSelector(safeParamAddressFromStateSelector)

View File

@ -1,4 +1,4 @@
import { Map } from 'immutable'
import { List, Map } from 'immutable'
import { createSelector } from 'reselect'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
@ -7,12 +7,13 @@ import { isUserOwner } from 'src/logic/wallets/ethAddresses'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { safeActiveTokensSelector, safeBalancesSelector, safeSelector } from 'src/routes/safe/store/selectors'
import { Token } from 'src/logic/tokens/store/model/token'
export const grantedSelector = createSelector(userAccountSelector, safeSelector, (userAccount, safe) =>
isUserOwner(safe, userAccount),
)
const safeEthAsTokenSelector = createSelector(safeSelector, (safe) => {
const safeEthAsTokenSelector = createSelector(safeSelector, (safe): Token | undefined => {
if (!safe) {
return undefined
}
@ -25,8 +26,8 @@ export const extendedSafeTokensSelector = createSelector(
safeBalancesSelector,
tokensSelector,
safeEthAsTokenSelector,
(safeTokens, balances, tokensList, ethAsToken) => {
const extendedTokens = Map().withMutations((map) => {
(safeTokens, balances, tokensList, ethAsToken): List<Token> => {
const extendedTokens = Map<string, Token>().withMutations((map) => {
safeTokens.forEach((tokenAddress) => {
const baseToken = tokensList.get(tokenAddress)
const tokenBalance = balances.get(tokenAddress)

View File

@ -0,0 +1,7 @@
import { createAction } from 'redux-actions'
export const ADD_SAFE_MODULES = 'ADD_SAFE_MODULES'
const addSafeModules = createAction(ADD_SAFE_MODULES)
export default addSafeModules

View File

@ -1,7 +1,9 @@
import { push } from 'connected-react-router'
import { List, Map } from 'immutable'
import { WithSnackbarProps } from 'notistack'
import { batch } from 'react-redux'
import semverSatisfies from 'semver/functions/satisfies'
import { ThunkAction } from 'redux-thunk'
import { onboardUser } from 'src/components/ConnectButton'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
@ -34,9 +36,17 @@ import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { makeConfirmation } from '../models/confirmation'
import fetchTransactions from './transactions/fetchTransactions'
import { safeTransactionsSelector } from 'src/routes/safe/store/selectors'
import { TransactionStatus, TxArgs } from 'src/routes/safe/store/models/types/transaction'
import { Transaction, TransactionStatus, TxArgs } from 'src/routes/safe/store/models/types/transaction'
import { AnyAction } from 'redux'
import { AppReduxState } from 'src/store'
import { Dispatch } from './types'
export const removeTxFromStore = (tx, safeAddress, dispatch, state) => {
export const removeTxFromStore = (
tx: Transaction,
safeAddress: string,
dispatch: Dispatch,
state: AppReduxState,
): void => {
if (tx.isCancellationTx) {
const newTxStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
const transactions = safeTransactionsSelector(state)
@ -53,7 +63,12 @@ export const removeTxFromStore = (tx, safeAddress, dispatch, state) => {
}
}
export const storeTx = async (tx, safeAddress, dispatch, state) => {
export const storeTx = async (
tx: Transaction,
safeAddress: string,
dispatch: Dispatch,
state: AppReduxState,
): Promise<void> => {
if (tx.isCancellationTx) {
let newTxStatus: TransactionStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
@ -79,6 +94,20 @@ export const storeTx = async (tx, safeAddress, dispatch, state) => {
}
}
interface CreateTransaction extends WithSnackbarProps {
navigateToTransactionsTab?: boolean
notifiedTransaction: string
operation?: number
origin?: string
safeAddress: string
to: string
txData?: string
txNonce?: number | string
valueInWei: string
}
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction>
const createTransaction = ({
safeAddress,
to,
@ -91,7 +120,10 @@ const createTransaction = ({
operation = CALL,
navigateToTransactionsTab = true,
origin = null,
}) => async (dispatch, getState) => {
}: CreateTransaction): CreateTransactionAction => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
const state = getState()
if (navigateToTransactionsTab) {
@ -241,7 +273,6 @@ const createTransaction = ({
dispatch,
state,
)
dispatch(fetchTransactions(safeAddress))
return receipt.transactionHash

View File

@ -11,7 +11,7 @@ const fetchEtherBalance = (safeAddress: string) => async (
): Promise<void> => {
try {
const state = getState()
const ethBalance = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress, 'ethBalance'])
const ethBalance = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress, 'ethBalance'])
const newEthBalance = await backOff(() => getBalanceInEtherOf(safeAddress))
if (newEthBalance !== ethBalance) {
dispatch(updateSafe({ address: safeAddress, ethBalance: newEthBalance }))

View File

@ -13,8 +13,10 @@ import updateSafe from 'src/routes/safe/store/actions/updateSafe'
import { makeOwner } from 'src/routes/safe/store/models/owner'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeOwner } from '../models/safe'
import { ModulePair, SafeOwner } from 'src/routes/safe/store/models/safe'
import { Dispatch } from 'redux'
import addSafeModules from './addSafeModules'
import { SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
const buildOwnersFrom = (
safeOwners,
@ -37,6 +39,16 @@ const buildOwnersFrom = (
})
})
const buildModulesLinkedList = (modules: string[], nextModule: string): Array<ModulePair> | null => {
if (modules.length) {
return modules.map((moduleAddress, index, modules) => {
const prevModule = modules[index + 1]
return [moduleAddress, prevModule !== undefined ? prevModule : nextModule]
})
}
return null
}
export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?: any) => {
const safeAddress = checksumAddress(safeAdd)
@ -75,8 +87,14 @@ export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?:
export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch): Promise<void> => {
const safeAddress = checksumAddress(safeAdd)
// Check if the owner's safe did change and update them
const safeParams = ['getThreshold', 'nonce', 'getOwners']
const [[remoteThreshold, remoteNonce, remoteOwners], localSafe] = await Promise.all([
const safeParams = [
'getThreshold',
'nonce',
'getOwners',
// TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled
{ method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] },
]
const [[remoteThreshold, remoteNonce, remoteOwners, modules], localSafe] = await Promise.all([
generateBatchRequests({
abi: GnosisSafeSol.abi,
address: safeAddress,
@ -90,6 +108,13 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
const localThreshold = localSafe ? localSafe.threshold : undefined
const localNonce = localSafe ? localSafe.nonce : undefined
dispatch(
addSafeModules({
safeAddress,
modulesAddresses: buildModulesLinkedList(modules.array, modules.next),
}),
)
if (localNonce !== Number(remoteNonce)) {
dispatch(updateSafe({ address: safeAddress, nonce: Number(remoteNonce) }))
}

View File

@ -1,4 +1,7 @@
import { batch } from 'react-redux'
import { ThunkAction, ThunkDispatch } from 'redux-thunk'
import { AnyAction } from 'redux'
import { backOff } from 'exponential-backoff'
import { addIncomingTransactions } from '../../addIncomingTransactions'
@ -7,12 +10,13 @@ import { loadOutgoingTransactions } from './loadOutgoingTransactions'
import { addOrUpdateCancellationTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { addOrUpdateTransactions } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
import { Dispatch } from 'redux'
import { backOff } from 'exponential-backoff'
import { AppReduxState } from 'src/store'
const noFunc = () => {}
export default (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => {
export default (safeAddress: string): ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction> => async (
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
): Promise<void> => {
try {
const transactions = await backOff(() => loadOutgoingTransactions(safeAddress))

View File

@ -196,7 +196,7 @@ export const loadOutgoingTransactions = async (safeAddress: string): Promise<Saf
const knownTokens = state[TOKEN_REDUCER_ID]
const currentUser = state[PROVIDER_REDUCER_ID].get('account')
const safe = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress])
const safe = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
if (!safe) {
return defaultResponse

View File

@ -341,7 +341,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state): Promi
}
const knownTokens: Map<string, Token> = state[TOKEN_REDUCER_ID]
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress])
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map()
const outgoingTxs = state[TRANSACTIONS_REDUCER_ID].get(safeAddress) || List()

View File

@ -0,0 +1,6 @@
import { ThunkDispatch } from 'redux-thunk'
import { AnyAction } from 'redux'
import { AppReduxState } from 'src/store'
export type Dispatch = ThunkDispatch<AppReduxState, undefined, AnyAction>

View File

@ -105,7 +105,7 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
action.payload.forEach((incomingTransactions, safeAddress) => {
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress)
const viewedSafes = state.currentSession ? state.currentSession.get('viewedSafes') : []
const recurringUser = viewedSafes.includes(safeAddress)
const recurringUser = viewedSafes?.includes(safeAddress)
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)

View File

@ -5,12 +5,15 @@ export type SafeOwner = {
address: string
}
export type ModulePair = [string, string]
export type SafeRecordProps = {
name: string
address: string
threshold: number
ethBalance: string
owners: List<{ name: string; address: string }>
modules: ModulePair[] | null
activeTokens: Set<string>
activeAssets: Set<string>
blacklistedTokens: Set<string>
@ -30,6 +33,7 @@ const makeSafe = Record<SafeRecordProps>({
threshold: 0,
ethBalance: '0',
owners: List([]),
modules: [],
activeTokens: Set(),
activeAssets: Set(),
blacklistedTokens: Set(),

View File

@ -15,6 +15,7 @@ import { makeOwner } from 'src/routes/safe/store/models/owner'
import makeSafe from 'src/routes/safe/store/models/safe'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeReducerMap } from './types/safe'
import { ADD_SAFE_MODULES } from 'src/routes/safe/store/actions/addSafeModules'
export const SAFE_REDUCER_ID = 'safes'
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
@ -49,7 +50,7 @@ export default handleActions(
const safeAddress = safe.address
return state.updateIn(
[SAFE_REDUCER_ID, safeAddress],
['safes', safeAddress],
makeSafe({ name: 'LOADED SAFE', address: safeAddress }),
(prevSafe) => prevSafe.merge(safe),
)
@ -59,13 +60,13 @@ export default handleActions(
return state.withMutations((map) => {
map
.get(SAFE_REDUCER_ID)
.get('safes')
.keySeq()
.forEach((safeAddress) => {
const safeActiveTokens = map.getIn([SAFE_REDUCER_ID, safeAddress, 'activeTokens'])
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
const activeTokens = safeActiveTokens.add(tokenAddress)
map.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => prevSafe.merge({ activeTokens }))
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens }))
})
})
},
@ -76,21 +77,21 @@ export default handleActions(
// in case of update it shouldn't, because a record would be initialized
// with initial props and it would overwrite existing ones
if (state.hasIn([SAFE_REDUCER_ID, safe.address])) {
return state.updateIn([SAFE_REDUCER_ID, safe.address], (prevSafe) => prevSafe.merge(safe))
if (state.hasIn(['safes', safe.address])) {
return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.merge(safe))
}
return state.setIn([SAFE_REDUCER_ID, safe.address], makeSafe(safe))
return state.setIn(['safes', safe.address], makeSafe(safe))
},
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
const safeAddress = action.payload
return state.deleteIn([SAFE_REDUCER_ID, safeAddress])
return state.deleteIn(['safes', safeAddress])
},
[ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { ownerAddress, ownerName, safeAddress } = action.payload
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) =>
return state.updateIn(['safes', safeAddress], (prevSafe) =>
prevSafe.merge({
owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })),
}),
@ -99,7 +100,7 @@ export default handleActions(
[REMOVE_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { ownerAddress, safeAddress } = action.payload
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) =>
return state.updateIn(['safes', safeAddress], (prevSafe) =>
prevSafe.merge({
owners: prevSafe.owners.filter((o) => o.address.toLowerCase() !== ownerAddress.toLowerCase()),
}),
@ -108,7 +109,7 @@ export default handleActions(
[REPLACE_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) =>
return state.updateIn(['safes', safeAddress], (prevSafe) =>
prevSafe.merge({
owners: prevSafe.owners
.filter((o) => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase())
@ -119,7 +120,7 @@ export default handleActions(
[EDIT_SAFE_OWNER]: (state: SafeReducerMap, action) => {
const { ownerAddress, ownerName, safeAddress } = action.payload
return state.updateIn([SAFE_REDUCER_ID, safeAddress], (prevSafe) => {
return state.updateIn(['safes', safeAddress], (prevSafe) => {
const ownerToUpdateIndex = prevSafe.owners.findIndex(
(o) => o.address.toLowerCase() === ownerAddress.toLowerCase(),
)
@ -127,8 +128,13 @@ export default handleActions(
return prevSafe.merge({ owners: updatedOwners })
})
},
[SET_DEFAULT_SAFE]: (state, action) => state.set('defaultSafe', action.payload),
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action) => state.set('latestMasterContractVersion', action.payload),
[ADD_SAFE_MODULES]: (state: SafeReducerMap, action) => {
const { modulesAddresses, safeAddress } = action.payload
return state.setIn(['safes', safeAddress, 'modules'], modulesAddresses)
},
[SET_DEFAULT_SAFE]: (state: SafeReducerMap, action) => state.set('defaultSafe', action.payload),
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerMap, action) =>
state.set('latestMasterContractVersion', action.payload),
},
Map({
defaultSafe: DEFAULT_SAFE_INITIAL_STATE,

View File

@ -10,11 +10,11 @@ import { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transacti
import { AppReduxState } from 'src/store'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeRecord } from 'src/routes/safe/store/models/safe'
import makeSafe, { SafeRecord, SafeRecordProps } from '../models/safe'
const safesStateSelector = (state: AppReduxState) => state[SAFE_REDUCER_ID]
export const safesMapSelector = (state: AppReduxState): SafesMap => state[SAFE_REDUCER_ID].get('safes')
export const safesMapSelector = (state: AppReduxState): SafesMap => safesStateSelector(state).get('safes')
export const safesListSelector = createSelector(safesMapSelector, (safes) => safes.toList())
@ -105,14 +105,14 @@ export const safeIncomingTransactionsSelector = createSelector(
},
)
export const safeSelector = createSelector(safesMapSelector, safeParamAddressFromStateSelector, (safes, address) => {
export const safeSelector = createSelector(safesMapSelector, safeParamAddressFromStateSelector, (safes, address):
| SafeRecord
| undefined => {
if (!address) {
return undefined
}
const checksumed = checksumAddress(address)
const safe = safes.get(checksumed)
return safe
return safes.get(checksumed)
})
export const safeActiveTokensSelector = createSelector(
@ -151,13 +151,16 @@ export const safeBlacklistedTokensSelector = createSelector(safeSelector, (safe)
return safe.blacklistedTokens
})
export const safeBlacklistedAssetsSelector = createSelector(safeSelector, (safe) => {
if (!safe) {
return List()
}
export const safeBlacklistedAssetsSelector = createSelector(
safeSelector,
(safe): Set<string> => {
if (!safe) {
return Set()
}
return safe.blacklistedAssets
})
return safe.blacklistedAssets
},
)
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap) =>
safes.get(safeAddress).get('activeAssets')
@ -165,15 +168,22 @@ export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: Safes
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress, safes) =>
safes.get(safeAddress).get('blacklistedAssets')
export const safeBalancesSelector = createSelector(safeSelector, (safe) => {
if (!safe) {
return Map()
}
export const safeBalancesSelector = createSelector(
safeSelector,
(safe): Map<string, string> => {
if (!safe) {
return Map()
}
return safe.balances
})
return safe.balances
},
)
export const safeFieldSelector = (field: string) => (safe: SafeRecord) => safe?.[field]
const baseSafe = makeSafe()
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
safe: SafeRecord,
): SafeRecordProps[K] | null => (safe ? safe.get(field, baseSafe.get(field)) : null)
export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name'))
@ -189,6 +199,8 @@ export const safeNonceSelector = createSelector(safeSelector, safeFieldSelector(
export const safeOwnersSelector = createSelector(safeSelector, safeFieldSelector('owners'))
export const safeModulesSelector = createSelector(safeSelector, safeFieldSelector('modules'))
export const safeFeaturesEnabledSelector = createSelector(safeSelector, safeFieldSelector('featuresEnabled'))
export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelector, (safes) => {

View File

@ -1,6 +1,7 @@
import { Map } from 'immutable'
import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router'
import { createHashHistory } from 'history'
import { applyMiddleware, CombinedState, combineReducers, compose, createStore } from 'redux'
import { applyMiddleware, combineReducers, compose, createStore, CombinedState } from 'redux'
import thunk from 'redux-thunk'
import addressBookMiddleware from 'src/logic/addressBook/store/middleware/addressBookMiddleware'
@ -16,9 +17,9 @@ import currencyValuesStorageMiddleware from 'src/logic/currencyValues/store/midd
import currencyValues, { CURRENCY_VALUES_KEY } from 'src/logic/currencyValues/store/reducer/currencyValues'
import currentSession, { CURRENT_SESSION_REDUCER_ID } from 'src/logic/currentSession/store/reducer/currentSession'
import notifications, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications'
import tokens, { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher'
import provider, { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
import provider, { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider'
import notificationsMiddleware from 'src/routes/safe/store/middleware/notificationsMiddleware'
import safeStorage from 'src/routes/safe/store/middleware/safeStorage'
import cancellationTransactions, {
@ -29,10 +30,7 @@ import incomingTransactions, {
} from 'src/routes/safe/store/reducer/incomingTransactions'
import safe, { SAFE_REDUCER_ID, SafeReducerMap } from 'src/routes/safe/store/reducer/safe'
import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
import { Map } from 'immutable'
import { NFTAssets, NFTTokens } from '../logic/collectibles/sources/OpenSea'
import { ProviderRecord } from '../logic/wallets/store/model/provider'
import { Token } from 'src/logic/tokens/store/model/token'
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
export const history = createHashHistory({ hashType: 'slash' })
@ -68,11 +66,11 @@ const reducers = combineReducers({
})
export type AppReduxState = CombinedState<{
[PROVIDER_REDUCER_ID]?: ProviderRecord
[PROVIDER_REDUCER_ID]: ProviderState
[SAFE_REDUCER_ID]: SafeReducerMap
[NFT_ASSETS_REDUCER_ID]?: NFTAssets
[NFT_TOKENS_REDUCER_ID]?: NFTTokens
[TOKEN_REDUCER_ID]?: Map<string, Token>
[NFT_ASSETS_REDUCER_ID]: NFTAssets
[NFT_TOKENS_REDUCER_ID]: NFTTokens
[TOKEN_REDUCER_ID]: TokenState
[TRANSACTIONS_REDUCER_ID]: Map<string, any>
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: Map<string, any>
[INCOMING_TRANSACTIONS_REDUCER_ID]: Map<string, any>

View File

@ -1340,13 +1340,13 @@
solc "0.5.14"
truffle "^5.1.21"
"@gnosis.pm/safe-react-components@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-components/-/safe-react-components-0.1.3.tgz#ac80029862fd2a042a4400361db46e92e29a81c0"
integrity sha512-N3EMk1bvsPUaKeuUXirlF8aXEdJucIxt+QjA1IjShrEy40zKT9S3Gj2H0nqMqLVcIo6IpZ0iRwv+MulYfpbqbQ==
"@gnosis.pm/safe-react-components@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-components/-/safe-react-components-0.2.0.tgz#a35947fd5f016b7aabe5ac6105b6b873e00de730"
integrity sha512-8tAJZPywtxqPZw48z92iRVNWfn173mUeHf+mkWVlw9nqy+eurXEKPM5Jy1fnEcLsWQfiafGzunB3OzsHvXq+BQ==
dependencies:
classnames "^2.2.6"
polished "3.6.3"
polished "3.6.5"
react-docgen-typescript-loader "^3.7.2"
react-media "^1.10.0"
url-loader "^4.1.0"
@ -2138,7 +2138,7 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/hoist-non-react-statics@*":
"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@ -2241,6 +2241,16 @@
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.9":
version "7.1.9"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3"
integrity sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-transition-group@^4.2.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
@ -12500,13 +12510,6 @@ pocket-js-core@0.0.3:
dependencies:
axios "^0.18.0"
polished@3.6.3:
version "3.6.3"
resolved "https://registry.yarnpkg.com/polished/-/polished-3.6.3.tgz#68f4fe7ffad46530733029b939dd12978200cb59"
integrity sha512-QJ0q0b6gX1+0OJtPMfgVJxV0vg5XTa4im+Rca989dAtmsd/fEky3X+0A+V+OUXq1nyiDGplJwqD853dTS0gkPg==
dependencies:
"@babel/runtime" "^7.9.2"
polished@3.6.5:
version "3.6.5"
resolved "https://registry.yarnpkg.com/polished/-/polished-3.6.5.tgz#dbefdde64c675935ec55119fe2a2ab627ca82e9c"
@ -14087,7 +14090,7 @@ redux-thunk@^2.3.0:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
redux@4.0.5:
redux@4.0.5, redux@^4.0.0:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==