mirror of
https://github.com/status-im/safe-react.git
synced 2025-01-27 09:54:51 +00:00
Merge pull request #1074 from gnosis/feature/#934-advanced-settings
(Feature) Advanced Settings - Safe Details
This commit is contained in:
commit
57248c6985
10
.gitignore
vendored
10
.gitignore
vendored
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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">
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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
11
src/components/Table/types.d.ts
vendored
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -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
|
||||
}, {})
|
||||
},
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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()
|
||||
|
@ -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: () => {},
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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}>
|
||||
|
@ -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} />
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 } })
|
||||
|
@ -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}
|
||||
|
@ -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',
|
||||
|
125
src/routes/safe/components/Settings/Advanced/ModulesTable.tsx
Normal file
125
src/routes/safe/components/Settings/Advanced/ModulesTable.tsx
Normal 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
|
@ -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
|
35
src/routes/safe/components/Settings/Advanced/dataFetcher.ts
Normal file
35
src/routes/safe/components/Settings/Advanced/dataFetcher.ts
Normal 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])
|
||||
}
|
93
src/routes/safe/components/Settings/Advanced/index.tsx
Normal file
93
src/routes/safe/components/Settings/Advanced/index.tsx
Normal 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
|
112
src/routes/safe/components/Settings/Advanced/style.ts
Normal file
112
src/routes/safe/components/Settings/Advanced/style.ts
Normal 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',
|
||||
},
|
||||
})
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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 && (
|
||||
|
@ -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
|
||||
|
@ -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(() => {
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
7
src/routes/safe/store/actions/addSafeModules.ts
Normal file
7
src/routes/safe/store/actions/addSafeModules.ts
Normal 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
|
@ -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
|
||||
|
@ -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 }))
|
||||
|
@ -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) }))
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
6
src/routes/safe/store/actions/types.d.ts
vendored
Normal file
6
src/routes/safe/store/actions/types.d.ts
vendored
Normal 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>
|
@ -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)
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
31
yarn.lock
31
yarn.lock
@ -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==
|
||||
|
Loading…
x
Reference in New Issue
Block a user