(Feature) Erc721 modal lists (#661)
* Add Assets sections * (add) collectibles tab * (add) criptokitty items * (add) collectible items, definitive edition * (fix) collectibles were overlapping with bottom banner * (fix) wording * (fix) responsive issues * Install `async-sema` dependency * Create collectible source classes - source from mocked data and opensea, it's extendable to import information from other sources * Update `Collectible` implementation to use new data source * Create constants file to better handle env variables and default values * Add description to item's cards - also added a mocked class with real data * Fix `saveTxToHistory`, remove hardcoded `CALL` * Fix after merge development * Set background color for collectible based on data info - Changed `withStyles` in favor of a hook-like approach with `makeStyles` * Enhance collectible card info and group title * Use current safeAddress to query for collectibles information - also migrated from `withStyles` to `makeStyles` * Use proper key values for lists and set more significant names * update yarn.lock after merge * Fix linting error * Move ethAsToken verification outside loop * Use absolute route for `SendModal` import * Move Collectibles into redux store * Update yarn.lock file * Selectable NFTs * Divide the `collectible` store into `nftAssets` and `nftTokens` - Also updated components to retain functionality - Created a `textShortener` function for better presentation * Update `yarn.lock` * Update `yarn.lock` * Fix item background color * Clears the tokenID select field when the collectible selected changes * Open Send modal from the assets section * Use token name for the token selection dropdown * Refactor Balances tabs: reduces the amount of props received, exported tokens lists to a component * Refactor Balances tabs: reduces the amount of props received, exported tokens lists to a component * Add openZeppelin contracts dependency * Create ERC721 getter * Fix types, default values and clean code * Fix: properly refresh list of collectibles when switching safes * Add ReviewCollectible step in send NFT * Displays the assets in the manage list * Fixs add custom token/asset modal cancel button * Change items shadow * Give option to choose what to send by clicking 'Send' button in AddressBook * Disable [Send] button for Collectibles if not owner * Set Coins as default option in assets tab - also fixed styles for `Coins` option * Use collectible icon in send modal * Set default message when no assets available - removed pagination feature * Create SafeVersionProvider to better handle version-related tasks Provides: - current and latest versions, - a boolean indicating a need for update, - an upgradeSafe callback to trigger upgrade from any place, - a list of enabled features, depending on the current version - the latter needs a refactor like extract features outside the provider and define constants for the features. * Force build * Update `yarn.lock` * Disable Manage list for NFTs * Implements manage list to add/remove assets * Implements manage list to add/remove assets * Merge branch 'feature/#469-ERC721-feature-implementation' of https://github.com/gnosis/safe-react into feature/#469-ERC721-feature-implementation # Conflicts: # src/routes/safe/components/Balances/Collectibles/index.jsx # src/routes/safe/components/Balances/index.jsx # src/utils/constants.js * Implements blacklisted assets * Fix container shadow - Also fixes tables shadow, thanks to @gabitoesmiapodo * Enable nested routes for balances (assets) tab * Default to `/balance` if invalid nested path * Disable [Send Collectible] button, if not supported by safe * Change sub-menu buttons to clickable text * Replace Paragraph with Link * Fix invalid props errors for Link component * Fallback to `transferFrom` if `safeTransferFrom` is not implemented * Use `transfer` as fallback to ERC-721's `safeTransferFrom` - need to identify ERC721 token using `transfer` and `name` methods * Display failed transactions * Use react.lazy for collectibles' modals * Identify ERC-721 token transaction * Adds initial components for AddCustomAsset support * Fix Send Collectibles modal layout/behavior - disable dropdown list if there's no item to pick - fix placeholder for tokens list - fix dropdown list styles * Set default `isSuccessful` flag to `true` * Fixs erc721Enabled check * Adds margin to modal icon Fix search bar * Fix hidding buttons in coins table * Fixs display all available assets by default * Fixs modal assets * Fixs blacklistedAssetsAddresses save to storage Fixs show token button Co-authored-by: fernandomg <fernando.greco@gmail.com> Co-authored-by: Gabriel Rodriguez Alsina <gabriel.rodriguez@altoros.com>
This commit is contained in:
parent
5d7fa6428f
commit
b5bc0304f3
|
@ -0,0 +1,133 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import semverLessThan from 'semver/functions/lt'
|
||||
import satisfies from 'semver/functions/satisfies'
|
||||
import semverValid from 'semver/functions/valid'
|
||||
|
||||
import Modal from '~/components/Modal'
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
import { getCurrentMasterContractLastVersion } from '~/logic/safe/utils/safeVersion'
|
||||
import UpdateSafeModal from '~/routes/safe/components/Settings/UpdateSafeModal'
|
||||
import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
|
||||
|
||||
type TSafeVersion = {
|
||||
currentVersion: string,
|
||||
featuresEnabled: string[],
|
||||
lastVersion: string,
|
||||
needsUpdate: boolean,
|
||||
upgradeSafe: () => void,
|
||||
}
|
||||
|
||||
export const SafeVersionContext = React.createContext<TSafeVersion>({
|
||||
currentVersion: '',
|
||||
featuresEnabled: [],
|
||||
lastVersion: '',
|
||||
needsUpdate: false,
|
||||
upgradeSafe: () => {},
|
||||
})
|
||||
|
||||
const SafeVersionProvider = ({ children }: { children: React.Node }) => {
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const { currentVersion, lastVersion } = useSafeVersions(safeAddress)
|
||||
const needsUpdate = useUpdateChecker(currentVersion, lastVersion)
|
||||
const featuresEnabled = useFeaturesEnabled(currentVersion)
|
||||
const [willUpgrade, setWillUpgrade] = React.useState(false)
|
||||
|
||||
const upgradeSafe = () => {
|
||||
setWillUpgrade(true)
|
||||
}
|
||||
|
||||
const closeUpgrade = () => {
|
||||
setWillUpgrade(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeVersionContext.Provider value={{ currentVersion, featuresEnabled, lastVersion, needsUpdate, upgradeSafe }}>
|
||||
<>
|
||||
{children}
|
||||
<Modal description="Update Safe" handleClose={closeUpgrade} open={willUpgrade} title="Update Safe">
|
||||
<UpdateSafeModal onClose={closeUpgrade} safeAddress={safeAddress} />
|
||||
</Modal>
|
||||
</>
|
||||
</SafeVersionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function useSafeVersions(safeAddress: string) {
|
||||
const [versions, setVersions] = React.useState({ currentVersion: '', lastVersion: '' })
|
||||
|
||||
React.useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
async function getVersions() {
|
||||
const [currentSafeInstance, lastSafeVersion] = await Promise.all([
|
||||
getGnosisSafeInstanceAt(safeAddress),
|
||||
getCurrentMasterContractLastVersion(),
|
||||
])
|
||||
const version = await currentSafeInstance.VERSION()
|
||||
|
||||
setVersions({ currentVersion: version, lastVersion: lastSafeVersion })
|
||||
}
|
||||
|
||||
if (isCurrent) {
|
||||
getVersions().catch(console.error)
|
||||
}
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [safeAddress])
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
function useUpdateChecker(currentVersion, lastSafeVersion) {
|
||||
const [needsUpdate, setNeedsUpdate] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
async function checkIfNeedsUpdate() {
|
||||
const current = semverValid(currentVersion)
|
||||
const latest = semverValid(lastSafeVersion)
|
||||
const needsUpdate = latest ? semverLessThan(current, latest) : false
|
||||
|
||||
setNeedsUpdate(needsUpdate)
|
||||
}
|
||||
|
||||
if (isCurrent) {
|
||||
checkIfNeedsUpdate()
|
||||
}
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [currentVersion])
|
||||
|
||||
return needsUpdate
|
||||
}
|
||||
|
||||
function useFeaturesEnabled(currentVersion) {
|
||||
const [featuresEnabled, setFeaturesEnabled] = React.useState([])
|
||||
|
||||
React.useEffect(() => {
|
||||
const features = [
|
||||
{ name: 'ERC721', validVersion: '>=1.1.1' },
|
||||
{ name: 'ERC1155', validVersion: '>=1.1.1' },
|
||||
]
|
||||
|
||||
const enabledFeatures = features.reduce((acc, feature) => {
|
||||
if (satisfies(currentVersion, feature.validVersion)) {
|
||||
acc.push(feature.name)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
setFeaturesEnabled(enabledFeatures)
|
||||
}, [currentVersion])
|
||||
|
||||
return featuresEnabled
|
||||
}
|
||||
|
||||
export default SafeVersionProvider
|
|
@ -14,6 +14,7 @@ import CookiesBanner from '~/components/CookiesBanner'
|
|||
import Footer from '~/components/Footer'
|
||||
import Header from '~/components/Header'
|
||||
import Notifier from '~/components/Notifier'
|
||||
import SafeVersionProvider from '~/components/SafeVersionProvider'
|
||||
import SidebarProvider from '~/components/Sidebar'
|
||||
import Backdrop from '~/components/layout/Backdrop'
|
||||
import Img from '~/components/layout/Img'
|
||||
|
@ -67,11 +68,13 @@ const PageFrame = ({ children, classes, currentNetwork }: Props) => {
|
|||
maxSnack={5}
|
||||
>
|
||||
<Notifier />
|
||||
<SidebarProvider>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</SidebarProvider>
|
||||
<SafeVersionProvider>
|
||||
<SidebarProvider>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</SidebarProvider>
|
||||
</SafeVersionProvider>
|
||||
</SnackbarProvider>
|
||||
<CookiesBanner />
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,38 @@
|
|||
// @flow
|
||||
import { List, Set } from 'immutable'
|
||||
import type { Selector } from 'reselect'
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import type { NFTAssets } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
import { safeActiveAssetsSelector } from '~/routes/safe/store/selectors'
|
||||
import type { GlobalState } from '~/store'
|
||||
|
||||
export const nftAssetsSelector = (state: GlobalState) => state[NFT_ASSETS_REDUCER_ID]
|
||||
export const nftTokensSelector = (state: GlobalState) => state[NFT_TOKENS_REDUCER_ID]
|
||||
|
||||
export const nftAssetsListSelector: Selector<GlobalState, NFTAssets, List<NFTAssets>> = createSelector(
|
||||
nftAssetsSelector,
|
||||
(assets: NFTAssets) => {
|
||||
return assets ? List(Object.entries(assets).map(item => item[1])) : List([])
|
||||
},
|
||||
)
|
||||
|
||||
export const activeNftAssetsListSelector: Selector<GlobalState, NFTAssets, List<NFTAssets>> = createSelector(
|
||||
nftAssetsListSelector,
|
||||
safeActiveAssetsSelector,
|
||||
(assets: List<NFTAssets>, activeAssetsList: Set<string>) => {
|
||||
return assets.filter(asset => activeAssetsList.has(asset.address))
|
||||
},
|
||||
)
|
||||
|
||||
export const safeActiveSelectorMap: Selector<GlobalState, NFTAssets, List<NFTAssets>> = createSelector(
|
||||
activeNftAssetsListSelector,
|
||||
(activeAssets: List<NFTAssets>) => {
|
||||
let assetsMap = {}
|
||||
activeAssets.forEach(asset => {
|
||||
assetsMap[asset.address] = asset
|
||||
})
|
||||
return assetsMap
|
||||
},
|
||||
)
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
// @flow
|
||||
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
|
||||
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
|
||||
import { nftAssetsSelector } from '~/logic/collectibles/store/selectors'
|
||||
import updateActiveAssets from '~/routes/safe/store/actions/updateActiveAssets'
|
||||
import {
|
||||
safeActiveAssetsSelectorBySafe,
|
||||
safeBlacklistedAssetsSelectorBySafe,
|
||||
safesMapSelector,
|
||||
} from '~/routes/safe/store/selectors'
|
||||
import { type GetState, type GlobalState } from '~/store'
|
||||
|
||||
const activateAssetsByBalance = (safeAddress: string) => async (
|
||||
dispatch: ReduxDispatch<GlobalState>,
|
||||
getState: GetState,
|
||||
) => {
|
||||
try {
|
||||
await dispatch(fetchCollectibles())
|
||||
const state = getState()
|
||||
const safes = safesMapSelector(state)
|
||||
const availableAssets = nftAssetsSelector(state)
|
||||
const alreadyActiveAssets = safeActiveAssetsSelectorBySafe(safeAddress, safes)
|
||||
const blacklistedAssets = safeBlacklistedAssetsSelectorBySafe(safeAddress, safes)
|
||||
|
||||
// active tokens by balance, excluding those already blacklisted and the `null` address
|
||||
const activeByBalance = Object.entries(availableAssets)
|
||||
.filter(asset => {
|
||||
const { address, numberOfTokens } = asset[1]
|
||||
return address !== null && !blacklistedAssets.has(address) && numberOfTokens > 0
|
||||
})
|
||||
.map(asset => {
|
||||
return asset[0]
|
||||
})
|
||||
|
||||
// need to persist those already active assets, despite its balances
|
||||
const activeAssets = alreadyActiveAssets.union(activeByBalance)
|
||||
|
||||
// update list of active tokens
|
||||
dispatch(updateActiveAssets(safeAddress, activeAssets))
|
||||
} catch (err) {
|
||||
console.error('Error fetching active assets list', err)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default activateAssetsByBalance
|
|
@ -2,6 +2,7 @@
|
|||
import { List } from 'immutable'
|
||||
|
||||
import logo from '~/assets/icons/icon_etherTokens.svg'
|
||||
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
||||
import { type Token, makeToken } from '~/logic/tokens/store/model/token'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
|
@ -65,3 +66,15 @@ export const isMultisendTransaction = (data: string, value: number): boolean =>
|
|||
// 7de7edef - changeMasterCopy (550, 8)
|
||||
export const isUpgradeTransaction = (data: string) =>
|
||||
!!data && data.substr(308, 8) === 'f08a0323' && data.substr(550, 8) === '7de7edef'
|
||||
|
||||
export const isERC721Contract = async (contractAddress: string): boolean => {
|
||||
const ERC721Token = await getStandardTokenContract()
|
||||
let isERC721 = false
|
||||
try {
|
||||
isERC721 = true
|
||||
await ERC721Token.at(contractAddress)
|
||||
} catch (error) {
|
||||
console.warn('Asset not found')
|
||||
}
|
||||
return isERC721
|
||||
}
|
||||
|
|
|
@ -89,14 +89,12 @@ export const getAccountFrom: Function = async (web3Provider): Promise<string | n
|
|||
}
|
||||
|
||||
const getNetworkIdFrom = async web3Provider => {
|
||||
const networkId = await web3Provider.eth.net.getId()
|
||||
|
||||
return networkId
|
||||
return await web3Provider.eth.net.getId()
|
||||
}
|
||||
|
||||
export const getProviderInfo: Function = async (
|
||||
web3Provider,
|
||||
providerName?: string = 'Wallet',
|
||||
providerName: string = 'Wallet',
|
||||
): Promise<ProviderProps> => {
|
||||
web3 = new Web3(web3Provider)
|
||||
|
||||
|
@ -117,9 +115,7 @@ export const getProviderInfo: Function = async (
|
|||
|
||||
export const getAddressFromENS = async (name: string) => {
|
||||
const ens = new ENS(web3)
|
||||
const address = await ens.resolver(name).addr()
|
||||
|
||||
return address
|
||||
return await ens.resolver(name).addr()
|
||||
}
|
||||
|
||||
export const setWeb3 = (provider: Object) => {
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
// @flow
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import CallMade from '@material-ui/icons/CallMade'
|
||||
import CallReceived from '@material-ui/icons/CallReceived'
|
||||
import classNames from 'classnames/bind'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { styles } from './styles'
|
||||
|
||||
import Table from '~/components/Table'
|
||||
import type { Column } from '~/components/Table/TableHead'
|
||||
import { cellWidth } from '~/components/Table/TableHead'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { currencyValuesListSelector, currentCurrencySelector } from '~/logic/currencyValues/store/selectors'
|
||||
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
||||
import AssetTableCell from '~/routes/safe/components/Balances/AssetTableCell'
|
||||
import type { BalanceRow } from '~/routes/safe/components/Balances/dataFetcher'
|
||||
import {
|
||||
BALANCE_TABLE_ASSET_ID,
|
||||
BALANCE_TABLE_BALANCE_ID,
|
||||
BALANCE_TABLE_VALUE_ID,
|
||||
generateColumns,
|
||||
getBalanceData,
|
||||
} from '~/routes/safe/components/Balances/dataFetcher'
|
||||
import { extendedSafeTokensSelector, grantedSelector } from '~/routes/safe/container/selector'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type Props = {
|
||||
showSendFunds: Function,
|
||||
showReceiveFunds: Function,
|
||||
}
|
||||
|
||||
const Coins = (props: Props) => {
|
||||
const { showReceiveFunds, showSendFunds } = props
|
||||
const classes = useStyles()
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter(c => !c.custom)
|
||||
const currencySelected = useSelector(currentCurrencySelector)
|
||||
const activeTokens = useSelector(extendedSafeTokensSelector)
|
||||
const currencyValues = useSelector(currencyValuesListSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const filteredData = getBalanceData(activeTokens, currencySelected, currencyValues)
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={filteredData}
|
||||
defaultFixed
|
||||
defaultOrderBy={BALANCE_TABLE_ASSET_ID}
|
||||
defaultRowsPerPage={10}
|
||||
label="Balances"
|
||||
size={filteredData.size}
|
||||
>
|
||||
{(sortedData: Array<BalanceRow>) =>
|
||||
sortedData.map((row: any, index: number) => (
|
||||
<TableRow className={classes.hide} data-testid={BALANCE_ROW_TEST_ID} key={index} tabIndex={-1}>
|
||||
{autoColumns.map((column: Column) => {
|
||||
const { align, id, width } = column
|
||||
let cellItem
|
||||
switch (id) {
|
||||
case BALANCE_TABLE_ASSET_ID: {
|
||||
cellItem = <AssetTableCell asset={row[id]} />
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_BALANCE_ID: {
|
||||
cellItem = <div>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_VALUE_ID: {
|
||||
cellItem = <div className={classes.currencyValueRow}>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
default: {
|
||||
cellItem = null
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<TableCell align={align} component="td" key={id} style={cellWidth(width)}>
|
||||
{cellItem}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
{granted && (
|
||||
<Button
|
||||
className={classes.send}
|
||||
color="primary"
|
||||
onClick={() => showSendFunds(row.asset.address)}
|
||||
size="small"
|
||||
testId="balance-send-btn"
|
||||
variant="contained"
|
||||
>
|
||||
<CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||
Send
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className={classes.receive}
|
||||
color="primary"
|
||||
onClick={showReceiveFunds}
|
||||
size="small"
|
||||
variant="contained"
|
||||
>
|
||||
<CallReceived
|
||||
alt="Receive Transaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
/>
|
||||
Receive
|
||||
</Button>
|
||||
</Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Coins
|
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
import { sm, xs } from '~/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
iconSmall: {
|
||||
fontSize: 16,
|
||||
},
|
||||
hide: {
|
||||
'&:hover': {
|
||||
backgroundColor: '#fff3e2',
|
||||
},
|
||||
'&:hover $actions': {
|
||||
visibility: 'initial',
|
||||
},
|
||||
'&:focus $actions': {
|
||||
visibility: 'initial',
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
justifyContent: 'flex-end',
|
||||
visibility: 'hidden',
|
||||
},
|
||||
receive: {
|
||||
width: '95px',
|
||||
minWidth: '95px',
|
||||
marginLeft: sm,
|
||||
borderRadius: xs,
|
||||
'& > span': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
send: {
|
||||
width: '75px',
|
||||
minWidth: '75px',
|
||||
borderRadius: xs,
|
||||
'& > span': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: sm,
|
||||
},
|
||||
currencyValueRow: {
|
||||
maxWidth: '125px',
|
||||
textAlign: 'right',
|
||||
},
|
||||
})
|
|
@ -7,9 +7,10 @@ import { useSelector } from 'react-redux'
|
|||
import Item from './components/Item'
|
||||
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
|
||||
import type { NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { activeNftAssetsListSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
|
||||
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { fontColor, lg, screenSm, screenXs } from '~/theme/variables'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
|
@ -79,9 +80,9 @@ const Collectibles = () => {
|
|||
const classes = useStyles()
|
||||
const [selectedToken, setSelectedToken] = React.useState({})
|
||||
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
|
||||
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
|
||||
const { address, ethBalance, name } = useSelector(safeSelector)
|
||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||
const nftAssetsKeys = Object.keys(nftAssets)
|
||||
const activeAssetsList = useSelector(activeNftAssetsListSelector)
|
||||
|
||||
const handleItemSend = nftToken => {
|
||||
setSelectedToken(nftToken)
|
||||
|
@ -91,10 +92,8 @@ const Collectibles = () => {
|
|||
return (
|
||||
<Card className={classes.cardOuter}>
|
||||
<div className={classes.cardInner}>
|
||||
{nftAssetsKeys.length ? (
|
||||
nftAssetsKeys.map(assetAddress => {
|
||||
const nftAsset = nftAssets[assetAddress]
|
||||
|
||||
{activeAssetsList.size ? (
|
||||
activeAssetsList.map(nftAsset => {
|
||||
return (
|
||||
<React.Fragment key={nftAsset.slug}>
|
||||
<div className={classes.title}>
|
||||
|
@ -122,8 +121,11 @@ const Collectibles = () => {
|
|||
</div>
|
||||
<SendModal
|
||||
activeScreenType="sendCollectible"
|
||||
ethBalance={ethBalance}
|
||||
isOpen={sendNFTsModalOpen}
|
||||
onClose={() => setSendNFTsModalOpen(false)}
|
||||
safeAddress={address}
|
||||
safeName={name}
|
||||
selectedToken={selectedToken}
|
||||
/>
|
||||
</Card>
|
||||
|
|
|
@ -24,7 +24,7 @@ import Img from '~/components/layout/Img'
|
|||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
|
||||
import { nftTokensSelector, safeActiveSelectorMap } from '~/logic/collectibles/store/selectors'
|
||||
import type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
|
@ -58,7 +58,7 @@ const useStyles = makeStyles(styles)
|
|||
const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = {} }: Props) => {
|
||||
const classes = useStyles()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
|
||||
const nftAssets: NFTAssetsState = useSelector(safeActiveSelectorMap)
|
||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
|
||||
|
|
|
@ -4,7 +4,7 @@ import { withStyles } from '@material-ui/core/styles'
|
|||
import Close from '@material-ui/icons/Close'
|
||||
import { List } from 'immutable'
|
||||
import React, { useState } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { connect, useSelector } from 'react-redux'
|
||||
|
||||
import actions, { type Actions } from './actions'
|
||||
import { styles } from './style'
|
||||
|
@ -13,11 +13,18 @@ import Hairline from '~/components/layout/Hairline'
|
|||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { orderedTokenListSelector } from '~/logic/tokens/store/selectors'
|
||||
import AddCustomAssetComponent from '~/routes/safe/components/Balances/Tokens/screens/AddCustomAsset'
|
||||
import AddCustomToken from '~/routes/safe/components/Balances/Tokens/screens/AddCustomToken'
|
||||
import AssetsList from '~/routes/safe/components/Balances/Tokens/screens/AssetsList'
|
||||
import TokenList from '~/routes/safe/components/Balances/Tokens/screens/TokenList'
|
||||
import { extendedSafeTokensSelector } from '~/routes/safe/container/selector'
|
||||
import { safeBlacklistedTokensSelector } from '~/routes/safe/store/selectors'
|
||||
|
||||
export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn'
|
||||
|
||||
type ActiveScreen = 'tokenList' | 'addCustomToken' | 'assetsList' | 'addCustomAsset'
|
||||
|
||||
type Props = Actions & {
|
||||
onClose: () => void,
|
||||
classes: Object,
|
||||
|
@ -25,24 +32,25 @@ type Props = Actions & {
|
|||
safeAddress: string,
|
||||
activeTokens: List<Token>,
|
||||
blacklistedTokens: List<Token>,
|
||||
modalScreen: ActiveScreen,
|
||||
}
|
||||
type ActiveScreen = 'tokenList' | 'addCustomToken'
|
||||
|
||||
const Tokens = (props: Props) => {
|
||||
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('tokenList')
|
||||
const {
|
||||
activateTokenForAllSafes,
|
||||
activeTokens,
|
||||
addToken,
|
||||
blacklistedTokens,
|
||||
classes,
|
||||
fetchTokens,
|
||||
modalScreen,
|
||||
onClose,
|
||||
safeAddress,
|
||||
tokens,
|
||||
updateActiveTokens,
|
||||
updateBlacklistedTokens,
|
||||
} = props
|
||||
const tokens = useSelector(orderedTokenListSelector)
|
||||
const activeTokens = useSelector(extendedSafeTokensSelector)
|
||||
const blacklistedTokens = useSelector(safeBlacklistedTokensSelector)
|
||||
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(modalScreen)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -67,18 +75,23 @@ const Tokens = (props: Props) => {
|
|||
updateBlacklistedTokens={updateBlacklistedTokens}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'assetsList' && <AssetsList setActiveScreen={setActiveScreen} />}
|
||||
{activeScreen === 'addCustomToken' && (
|
||||
<AddCustomToken
|
||||
activateTokenForAllSafes={activateTokenForAllSafes}
|
||||
activeTokens={activeTokens}
|
||||
addToken={addToken}
|
||||
onClose={onClose}
|
||||
parentList={'tokenList'}
|
||||
safeAddress={safeAddress}
|
||||
setActiveScreen={setActiveScreen}
|
||||
tokens={tokens}
|
||||
updateActiveTokens={updateActiveTokens}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'addCustomAsset' && (
|
||||
<AddCustomAssetComponent onClose={onClose} parentList={'assetsList'} setActiveScreen={setActiveScreen} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
// @flow
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { List } from 'immutable'
|
||||
import React, { useState } from 'react'
|
||||
import { FormSpy } from 'react-final-form'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { styles } from './style'
|
||||
import { getSymbolAndDecimalsFromContract } from './utils'
|
||||
|
||||
import Checkbox from '~/components/forms/Checkbox'
|
||||
import Field from '~/components/forms/Field'
|
||||
import GnoForm from '~/components/forms/GnoForm'
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import { composeValidators, minMaxLength, mustBeEthereumAddress, required } from '~/components/forms/validator'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import type { NFTAssetsState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { nftAssetsListSelector } from '~/logic/collectibles/store/selectors'
|
||||
import { type Token, type TokenProps } from '~/logic/tokens/store/model/token'
|
||||
import {
|
||||
addressIsAssetContract,
|
||||
doesntExistInAssetsList,
|
||||
} from '~/routes/safe/components/Balances/Tokens/screens/AddCustomAsset/validators'
|
||||
import TokenPlaceholder from '~/routes/safe/components/Balances/assets/token_placeholder.svg'
|
||||
|
||||
export const ADD_CUSTOM_ASSET_ADDRESS_INPUT_TEST_ID = 'add-custom-asset-address-input'
|
||||
export const ADD_CUSTOM_ASSET_SYMBOLS_INPUT_TEST_ID = 'add-custom-asset-symbols-input'
|
||||
export const ADD_CUSTOM_ASSET_DECIMALS_INPUT_TEST_ID = 'add-custom-asset-decimals-input'
|
||||
export const ADD_CUSTOM_ASSET_FORM = 'add-custom-asset-form'
|
||||
|
||||
type Props = {
|
||||
classes: Object,
|
||||
addToken: Function,
|
||||
updateActiveTokens: Function,
|
||||
safeAddress: string,
|
||||
activeTokens: List<TokenProps>,
|
||||
tokens: List<Token>,
|
||||
setActiveScreen: Function,
|
||||
onClose: Function,
|
||||
activateTokenForAllSafes: Function,
|
||||
parentList: 'assetsList' | 'tokenList',
|
||||
}
|
||||
|
||||
const INITIAL_FORM_STATE = {
|
||||
address: '',
|
||||
decimals: '',
|
||||
symbol: '',
|
||||
logoUri: '',
|
||||
}
|
||||
|
||||
const AddCustomAsset = (props: Props) => {
|
||||
const { classes, onClose, parentList, setActiveScreen } = props
|
||||
|
||||
const nftAssetsList: NFTAssetsState = useSelector(nftAssetsListSelector)
|
||||
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
|
||||
|
||||
const handleSubmit = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const populateFormValuesFromAddress = async (tokenAddress: string) => {
|
||||
const tokenData = await getSymbolAndDecimalsFromContract(tokenAddress)
|
||||
|
||||
if (tokenData.length) {
|
||||
const [symbol, decimals] = tokenData
|
||||
|
||||
setFormValues({
|
||||
address: tokenAddress,
|
||||
symbol,
|
||||
decimals,
|
||||
name: symbol,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const formSpyOnChangeHandler = async state => {
|
||||
const { dirty, errors, submitSucceeded, validating, values } = state
|
||||
// for some reason this is called after submitting, we don't need to update the values
|
||||
// after submit
|
||||
if (submitSucceeded) {
|
||||
return
|
||||
}
|
||||
|
||||
if (dirty && !validating && errors.address) {
|
||||
setFormValues(INITIAL_FORM_STATE)
|
||||
}
|
||||
|
||||
if (!errors.address && !validating && dirty) {
|
||||
await populateFormValuesFromAddress(values.address)
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
setActiveScreen(parentList)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<GnoForm initialValues={formValues} onSubmit={handleSubmit} testId={ADD_CUSTOM_ASSET_FORM}>
|
||||
{() => (
|
||||
<>
|
||||
<Block className={classes.formContainer}>
|
||||
<Paragraph className={classes.title} noMargin size="lg" weight="bolder">
|
||||
Add custom asset
|
||||
</Paragraph>
|
||||
<Field
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
name="address"
|
||||
placeholder="Asset contract address*"
|
||||
testId={ADD_CUSTOM_ASSET_ADDRESS_INPUT_TEST_ID}
|
||||
text="Token contract address*"
|
||||
type="text"
|
||||
validate={composeValidators(
|
||||
required,
|
||||
mustBeEthereumAddress,
|
||||
doesntExistInAssetsList(nftAssetsList),
|
||||
addressIsAssetContract,
|
||||
)}
|
||||
/>
|
||||
<FormSpy
|
||||
onChange={formSpyOnChangeHandler}
|
||||
subscription={{
|
||||
values: true,
|
||||
errors: true,
|
||||
validating: true,
|
||||
dirty: true,
|
||||
submitSucceeded: true,
|
||||
}}
|
||||
/>
|
||||
<Row>
|
||||
<Col layout="column" xs={6}>
|
||||
<Field
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
name="symbol"
|
||||
placeholder="Token symbol*"
|
||||
testId={ADD_CUSTOM_ASSET_SYMBOLS_INPUT_TEST_ID}
|
||||
text="Token symbol"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(2, 12))}
|
||||
/>
|
||||
<Field
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
disabled
|
||||
name="decimals"
|
||||
placeholder="Token decimals*"
|
||||
testId={ADD_CUSTOM_ASSET_DECIMALS_INPUT_TEST_ID}
|
||||
text="Token decimals*"
|
||||
type="text"
|
||||
/>
|
||||
<Block justify="left">
|
||||
<Field className={classes.checkbox} component={Checkbox} name="showForAllSafes" type="checkbox" />
|
||||
<Paragraph className={classes.checkboxLabel} size="md" weight="bolder">
|
||||
Activate assets for all Safes
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
<Col align="center" layout="column" xs={6}>
|
||||
<Paragraph className={classes.tokenImageHeading}>Token Image</Paragraph>
|
||||
<Img alt="Token image" height={100} src={TokenPlaceholder} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={goBack}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
|
||||
Save
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</GnoForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const AddCustomAssetComponent = withStyles(styles)(AddCustomAsset)
|
||||
|
||||
export default AddCustomAssetComponent
|
|
@ -0,0 +1,34 @@
|
|||
// @flow
|
||||
import { lg, md } from '~/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
title: {
|
||||
padding: `${lg} 0 20px`,
|
||||
fontSize: md,
|
||||
},
|
||||
formContainer: {
|
||||
padding: '0 20px',
|
||||
minHeight: '369px',
|
||||
},
|
||||
addressInput: {
|
||||
marginBottom: '15px',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
tokenImageHeading: {
|
||||
margin: '0 0 15px',
|
||||
},
|
||||
checkbox: {
|
||||
padding: '0 7px 0 0',
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
},
|
||||
checkboxLabel: {
|
||||
letterSpacing: '-0.5px',
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// @flow
|
||||
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
|
||||
|
||||
export const getSymbolAndDecimalsFromContract = async (tokenAddress: string) => {
|
||||
const tokenContract = await getHumanFriendlyToken()
|
||||
const token = await tokenContract.at(tokenAddress)
|
||||
let values
|
||||
|
||||
try {
|
||||
const [symbol, decimals] = await Promise.all([token.symbol(), token.decimals()])
|
||||
values = [symbol, decimals.toString()]
|
||||
} catch {
|
||||
values = []
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import { List } from 'immutable'
|
||||
|
||||
import { simpleMemoize } from '~/components/forms/validator'
|
||||
import { isERC721Contract } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
import type { NFTAsset } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const addressIsAssetContract = simpleMemoize(async (tokenAddress: string) => {
|
||||
const isAsset = await isERC721Contract(tokenAddress)
|
||||
if (!isAsset) {
|
||||
return 'Not a asset address'
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const doesntExistInAssetsList = (assetsList: List<NFTAsset>) =>
|
||||
simpleMemoize((tokenAddress: string) => {
|
||||
const tokenIndex = assetsList.findIndex(({ address }) => sameAddress(address, tokenAddress))
|
||||
|
||||
if (tokenIndex !== -1) {
|
||||
return 'Token already exists in your token list'
|
||||
}
|
||||
})
|
|
@ -38,6 +38,7 @@ type Props = {
|
|||
setActiveScreen: Function,
|
||||
onClose: Function,
|
||||
activateTokenForAllSafes: Function,
|
||||
parentList: 'assetsList' | 'tokenList',
|
||||
}
|
||||
|
||||
const INITIAL_FORM_STATE = {
|
||||
|
@ -54,6 +55,7 @@ const AddCustomToken = (props: Props) => {
|
|||
addToken,
|
||||
classes,
|
||||
onClose,
|
||||
parentList,
|
||||
safeAddress,
|
||||
setActiveScreen,
|
||||
tokens,
|
||||
|
@ -112,8 +114,8 @@ const AddCustomToken = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const goBackToTokenList = () => {
|
||||
setActiveScreen('tokenList')
|
||||
const goBack = () => {
|
||||
setActiveScreen(parentList)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -187,7 +189,7 @@ const AddCustomToken = (props: Props) => {
|
|||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={goBackToTokenList}>
|
||||
<Button minHeight={42} minWidth={140} onClick={goBack}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// @flow
|
||||
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { List, Set } from 'immutable'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import Img from '~/components/layout/Img'
|
||||
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import type { NFTAsset } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
|
||||
export const TOGGLE_ASSET_TEST_ID = 'toggle-asset-btn'
|
||||
|
||||
type Props = {
|
||||
data: {
|
||||
activeAssetsAddresses: Set<string>,
|
||||
assets: List<NFTAsset>,
|
||||
onSwitch: Function,
|
||||
},
|
||||
style: Object,
|
||||
index: number,
|
||||
classes: Object,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const AssetRow = memo(({ classes, data, index, style }: Props) => {
|
||||
const { activeAssetsAddresses, assets, onSwitch } = data
|
||||
const asset: NFTAsset = assets.get(index)
|
||||
const { address, image, name, symbol } = asset
|
||||
const isActive = activeAssetsAddresses.has(asset.address)
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<ListItem classes={{ root: classes.tokenRoot }} className={classes.token}>
|
||||
<ListItemIcon className={classes.tokenIcon}>
|
||||
<Img alt={name} height={28} onError={setImageToPlaceholder} src={image} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={symbol} secondary={name} />
|
||||
{address !== ETH_ADDRESS && (
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
checked={isActive}
|
||||
inputProps={{ 'data-testid': `${symbol}_${TOGGLE_ASSET_TEST_ID}` }}
|
||||
onChange={onSwitch(asset)}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
)}
|
||||
</ListItem>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default withStyles(styles)(AssetRow)
|
|
@ -0,0 +1,164 @@
|
|||
// @flow
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import MuiList from '@material-ui/core/List'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Search from '@material-ui/icons/Search'
|
||||
import cn from 'classnames'
|
||||
import { List } from 'immutable'
|
||||
import SearchBar from 'material-ui-search-bar'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import Spacer from '~/components/Spacer'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Divider from '~/components/layout/Divider'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Row from '~/components/layout/Row'
|
||||
import type { NFTAssetsState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { nftAssetsListSelector } from '~/logic/collectibles/store/selectors'
|
||||
import type { NFTAsset } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
import AssetRow from '~/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow'
|
||||
import updateActiveAssets from '~/routes/safe/store/actions/updateActiveAssets'
|
||||
import updateBlacklistedAssets from '~/routes/safe/store/actions/updateBlacklistedAssets'
|
||||
import {
|
||||
safeActiveAssetsListSelector,
|
||||
safeBlacklistedAssetsSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
} from '~/routes/safe/store/selectors'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
export const ADD_CUSTOM_ASSET_BUTTON_TEST_ID = 'add-custom-asset-btn'
|
||||
type Props = {
|
||||
setActiveScreen: Function,
|
||||
}
|
||||
|
||||
const filterBy = (filter: string, nfts: List<NFTAsset>): List<NFTAsset> =>
|
||||
nfts.filter(
|
||||
(asset: NFTAsset) =>
|
||||
!filter ||
|
||||
asset.description.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
asset.name.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
asset.symbol.toLowerCase().includes(filter.toLowerCase()),
|
||||
)
|
||||
|
||||
const AssetsList = (props: Props) => {
|
||||
const classes = useStyles()
|
||||
const searchClasses = {
|
||||
input: classes.searchInput,
|
||||
root: classes.searchRoot,
|
||||
iconButton: classes.searchIcon,
|
||||
searchContainer: classes.searchContainer,
|
||||
}
|
||||
const dispatch = useDispatch()
|
||||
const activeAssetsList = useSelector(safeActiveAssetsListSelector)
|
||||
const blacklistedAssets = useSelector(safeBlacklistedAssetsSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [filterValue, setFilterValue] = useState('')
|
||||
const [activeAssetsAddresses, setActiveAssetsAddresses] = useState(activeAssetsList)
|
||||
const [blacklistedAssetsAddresses, setBlacklistedAssetsAddresses] = useState(blacklistedAssets)
|
||||
const nftAssetsList: NFTAssetsState = useSelector(nftAssetsListSelector)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(updateActiveAssets(safeAddress, activeAssetsAddresses))
|
||||
dispatch(updateBlacklistedAssets(safeAddress, blacklistedAssetsAddresses))
|
||||
}, [activeAssetsAddresses, blacklistedAssetsAddresses])
|
||||
|
||||
const onCancelSearch = () => {
|
||||
setFilterValue('')
|
||||
}
|
||||
|
||||
const onChangeSearchBar = (value: string) => {
|
||||
setFilterValue(value)
|
||||
}
|
||||
|
||||
const getItemKey = index => {
|
||||
return index
|
||||
}
|
||||
|
||||
const onSwitch = (asset: NFTAsset) => () => {
|
||||
const { address } = asset
|
||||
const activeAssetsAddressesResult = activeAssetsAddresses.contains(address)
|
||||
? activeAssetsAddresses.remove(address)
|
||||
: activeAssetsAddresses.add(address)
|
||||
const blacklistedAssetsAddressesResult = activeAssetsAddresses.has(address)
|
||||
? blacklistedAssetsAddresses.add(address)
|
||||
: blacklistedAssetsAddresses.remove(address)
|
||||
setActiveAssetsAddresses(activeAssetsAddressesResult)
|
||||
setBlacklistedAssetsAddresses(blacklistedAssetsAddressesResult)
|
||||
return {
|
||||
activeAssetsAddresses: activeAssetsAddressesResult,
|
||||
blacklistedAssetsAddresses: blacklistedAssetsAddressesResult,
|
||||
}
|
||||
}
|
||||
|
||||
const createItemData = assetsList => {
|
||||
return {
|
||||
assets: assetsList,
|
||||
activeAssetsAddresses,
|
||||
onSwitch,
|
||||
}
|
||||
}
|
||||
|
||||
const nftAssetsFilteredList = filterBy(filterValue, nftAssetsList)
|
||||
const itemData = createItemData(nftAssetsFilteredList)
|
||||
const switchToAddCustomAssetScreen = () => props.setActiveScreen('addCustomAsset')
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.root}>
|
||||
<Row align="center" className={cn(classes.padding, classes.actions)}>
|
||||
<Search className={classes.search} />
|
||||
<SearchBar
|
||||
classes={searchClasses}
|
||||
onCancelSearch={onCancelSearch}
|
||||
onChange={onChangeSearchBar}
|
||||
placeholder="Search by name or symbol"
|
||||
searchIcon={<div />}
|
||||
value={filterValue}
|
||||
/>
|
||||
<Spacer />
|
||||
<Divider />
|
||||
<Spacer />
|
||||
<Button
|
||||
classes={{ label: classes.addBtnLabel }}
|
||||
className={classes.add}
|
||||
color="primary"
|
||||
disabled
|
||||
onClick={switchToAddCustomAssetScreen}
|
||||
size="small"
|
||||
testId={ADD_CUSTOM_ASSET_BUTTON_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
+ Add custom asset
|
||||
</Button>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Block>
|
||||
{!nftAssetsList.size && (
|
||||
<Block className={classes.progressContainer} justify="center">
|
||||
<CircularProgress />
|
||||
</Block>
|
||||
)}
|
||||
{nftAssetsList.size > 0 && (
|
||||
<MuiList className={classes.list}>
|
||||
<FixedSizeList
|
||||
height={413}
|
||||
itemCount={nftAssetsFilteredList.size}
|
||||
itemData={itemData}
|
||||
itemKey={getItemKey}
|
||||
itemSize={51}
|
||||
overscanCount={process.env.NODE_ENV === 'test' ? 100 : 10}
|
||||
width={500}
|
||||
>
|
||||
{AssetRow}
|
||||
</FixedSizeList>
|
||||
</MuiList>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssetsList
|
|
@ -0,0 +1,76 @@
|
|||
// @flow
|
||||
import { md, mediumFontSize, secondaryText, sm, xs } from '~/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
root: {
|
||||
minHeight: '52px',
|
||||
},
|
||||
search: {
|
||||
color: secondaryText,
|
||||
paddingLeft: sm,
|
||||
},
|
||||
padding: {
|
||||
padding: `0 ${md}`,
|
||||
},
|
||||
add: {
|
||||
fontSize: '11px',
|
||||
fontWeight: 'normal',
|
||||
paddingRight: md,
|
||||
paddingLeft: md,
|
||||
},
|
||||
addBtnLabel: {
|
||||
fontSize: mediumFontSize,
|
||||
},
|
||||
actions: {
|
||||
height: '50px',
|
||||
},
|
||||
list: {
|
||||
overflow: 'hidden',
|
||||
overflowY: 'scroll',
|
||||
padding: 0,
|
||||
height: '100%',
|
||||
},
|
||||
tokenIcon: {
|
||||
marginRight: sm,
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
},
|
||||
searchInput: {
|
||||
backgroundColor: 'transparent',
|
||||
lineHeight: 'initial',
|
||||
fontSize: '13px',
|
||||
padding: 0,
|
||||
'& > input::placeholder': {
|
||||
letterSpacing: '-0.5px',
|
||||
fontSize: mediumFontSize,
|
||||
color: 'black',
|
||||
},
|
||||
'& > input': {
|
||||
letterSpacing: '-0.5px',
|
||||
},
|
||||
},
|
||||
progressContainer: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
searchContainer: {
|
||||
width: '180px',
|
||||
marginLeft: xs,
|
||||
marginRight: xs,
|
||||
},
|
||||
searchRoot: {
|
||||
letterSpacing: '-0.5px',
|
||||
fontSize: '13px',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
'& > button': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
searchIcon: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
},
|
||||
})
|
|
@ -1,24 +1,13 @@
|
|||
// @flow
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import CallMade from '@material-ui/icons/CallMade'
|
||||
import CallReceived from '@material-ui/icons/CallReceived'
|
||||
import classNames from 'classnames/bind'
|
||||
import { List } from 'immutable'
|
||||
import * as React from 'react'
|
||||
|
||||
import AssetTableCell from './AssetTableCell'
|
||||
import Receive from './Receive'
|
||||
import Tokens from './Tokens'
|
||||
import { BALANCE_TABLE_ASSET_ID, type BalanceRow, generateColumns, getBalanceData } from './dataFetcher'
|
||||
import { styles } from './style'
|
||||
|
||||
import Modal from '~/components/Modal'
|
||||
import Table from '~/components/Table'
|
||||
import { type Column, cellWidth } from '~/components/Table/TableHead'
|
||||
import Button from '~/components/layout/Button'
|
||||
import ButtonLink from '~/components/layout/ButtonLink'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Divider from '~/components/layout/Divider'
|
||||
|
@ -27,9 +16,9 @@ import Row from '~/components/layout/Row'
|
|||
import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
import Coins from '~/routes/safe/components/Balances/Coins'
|
||||
import Collectibles from '~/routes/safe/components/Balances/Collectibles'
|
||||
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
||||
import { BALANCE_TABLE_BALANCE_ID, BALANCE_TABLE_VALUE_ID } from '~/routes/safe/components/Balances/dataFetcher'
|
||||
import DropdownCurrency from '~/routes/safe/components/DropdownCurrency'
|
||||
import { history } from '~/store'
|
||||
|
||||
|
@ -44,17 +33,19 @@ type State = {
|
|||
showCollectibles: boolean,
|
||||
showReceive: boolean,
|
||||
showToken: boolean,
|
||||
showManageCollectibleModal: boolean,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
activateTokensByBalance: Function,
|
||||
activateAssetsByBalance: Function,
|
||||
activeTokens: List<Token>,
|
||||
blacklistedTokens: List<Token>,
|
||||
classes: Object,
|
||||
createTransaction?: Function,
|
||||
createTransaction: Function,
|
||||
currencySelected: string,
|
||||
currencyValues: BalanceCurrencyType[],
|
||||
ethBalance?: string,
|
||||
ethBalance: string,
|
||||
featuresEnabled: string[],
|
||||
fetchCurrencyValues: Function,
|
||||
fetchTokens: Function,
|
||||
|
@ -64,7 +55,7 @@ type Props = {
|
|||
tokens: List<Token>,
|
||||
}
|
||||
|
||||
type Action = 'Token' | 'Send' | 'Receive'
|
||||
type Action = 'Token' | 'Send' | 'Receive' | 'ManageCollectibleModal'
|
||||
|
||||
class Balances extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
|
@ -73,6 +64,7 @@ class Balances extends React.Component<Props, State> {
|
|||
erc721Enabled: false,
|
||||
subMenuOptions: [],
|
||||
showToken: false,
|
||||
showManageCollectibleModal: false,
|
||||
sendFunds: {
|
||||
isOpen: false,
|
||||
selectedToken: undefined,
|
||||
|
@ -88,9 +80,10 @@ class Balances extends React.Component<Props, State> {
|
|||
static isCollectiblesLocation = /\/balances\/collectibles$/
|
||||
|
||||
componentDidMount(): void {
|
||||
const { activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props
|
||||
const { activateAssetsByBalance, activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props
|
||||
fetchCurrencyValues(safeAddress)
|
||||
activateTokensByBalance(safeAddress)
|
||||
activateAssetsByBalance(safeAddress)
|
||||
|
||||
const showCollectibles = Balances.isCollectiblesLocation.test(history.location.pathname)
|
||||
const showCoins = Balances.isCoinsLocation.test(history.location.pathname)
|
||||
|
@ -151,23 +144,17 @@ class Balances extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { erc721Enabled, sendFunds, showCoins, showCollectibles, showReceive, showToken, subMenuOptions } = this.state
|
||||
const {
|
||||
activeTokens,
|
||||
blacklistedTokens,
|
||||
classes,
|
||||
currencySelected,
|
||||
currencyValues,
|
||||
granted,
|
||||
safeAddress,
|
||||
safeName,
|
||||
tokens,
|
||||
} = this.props
|
||||
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter(c => !c.custom)
|
||||
|
||||
const filteredData = getBalanceData(activeTokens, currencySelected, currencyValues)
|
||||
erc721Enabled,
|
||||
sendFunds,
|
||||
showCoins,
|
||||
showCollectibles,
|
||||
showManageCollectibleModal,
|
||||
showReceive,
|
||||
showToken,
|
||||
subMenuOptions,
|
||||
} = this.state
|
||||
const { activeTokens, classes, createTransaction, ethBalance, safeAddress, safeName } = this.props
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -190,121 +177,43 @@ class Balances extends React.Component<Props, State> {
|
|||
))}
|
||||
</Col>
|
||||
<Col className={classes.tokenControls} end="sm" sm={6} xs={12}>
|
||||
{showCoins && (
|
||||
<>
|
||||
<DropdownCurrency />
|
||||
<ButtonLink
|
||||
className={classes.manageTokensButton}
|
||||
onClick={this.onShow('Token')}
|
||||
size="lg"
|
||||
testId="manage-tokens-btn"
|
||||
>
|
||||
Manage List
|
||||
</ButtonLink>
|
||||
<Modal
|
||||
description="Enable and disable tokens to be listed"
|
||||
handleClose={this.onHide('Token')}
|
||||
open={showToken}
|
||||
title="Manage List"
|
||||
>
|
||||
<Tokens
|
||||
activeTokens={activeTokens}
|
||||
blacklistedTokens={blacklistedTokens}
|
||||
onClose={this.onHide('Token')}
|
||||
safeAddress={safeAddress}
|
||||
tokens={tokens}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
{showCoins && <DropdownCurrency />}
|
||||
<ButtonLink
|
||||
className={classes.manageTokensButton}
|
||||
onClick={erc721Enabled && showCollectibles ? this.onShow('ManageCollectibleModal') : this.onShow('Token')}
|
||||
size="lg"
|
||||
testId="manage-tokens-btn"
|
||||
>
|
||||
Manage List
|
||||
</ButtonLink>
|
||||
<Modal
|
||||
description={
|
||||
erc721Enabled ? 'Enable and disables assets to be listed' : 'Enable and disable tokens to be listed'
|
||||
}
|
||||
handleClose={showManageCollectibleModal ? this.onHide('ManageCollectibleModal') : this.onHide('Token')}
|
||||
open={showToken || showManageCollectibleModal}
|
||||
title="Manage List"
|
||||
>
|
||||
<Tokens
|
||||
modalScreen={showManageCollectibleModal ? 'assetsList' : 'tokenList'}
|
||||
onClose={showManageCollectibleModal ? this.onHide('ManageCollectibleModal') : this.onHide('Token')}
|
||||
safeAddress={safeAddress}
|
||||
/>
|
||||
</Modal>
|
||||
</Col>
|
||||
</Row>
|
||||
{showCoins && (
|
||||
<TableContainer>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={filteredData}
|
||||
defaultFixed
|
||||
defaultOrderBy={BALANCE_TABLE_ASSET_ID}
|
||||
defaultRowsPerPage={10}
|
||||
label="Balances"
|
||||
size={filteredData.size}
|
||||
>
|
||||
{(sortedData: Array<BalanceRow>) =>
|
||||
sortedData.map((row: any, index: number) => (
|
||||
<TableRow className={classes.hide} data-testid={BALANCE_ROW_TEST_ID} key={index} tabIndex={-1}>
|
||||
{autoColumns.map((column: Column) => {
|
||||
const { align, id, width } = column
|
||||
let cellItem
|
||||
switch (id) {
|
||||
case BALANCE_TABLE_ASSET_ID: {
|
||||
cellItem = <AssetTableCell asset={row[id]} />
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_BALANCE_ID: {
|
||||
cellItem = <div>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_VALUE_ID: {
|
||||
cellItem = <div className={classes.currencyValueRow}>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
default: {
|
||||
cellItem = null
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<TableCell align={align} component="td" key={id} style={cellWidth(width)}>
|
||||
{cellItem}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
{granted && (
|
||||
<Button
|
||||
className={classes.send}
|
||||
color="primary"
|
||||
onClick={() => this.showSendFunds(row.asset.address)}
|
||||
size="small"
|
||||
testId="balance-send-btn"
|
||||
variant="contained"
|
||||
>
|
||||
<CallMade
|
||||
alt="Send Transaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
/>
|
||||
Send
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className={classes.receive}
|
||||
color="primary"
|
||||
onClick={this.onShow('Receive')}
|
||||
size="small"
|
||||
variant="contained"
|
||||
>
|
||||
<CallReceived
|
||||
alt="Receive Transaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
/>
|
||||
Receive
|
||||
</Button>
|
||||
</Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{showCoins && <Coins showReceiveFunds={this.onShow('Receive')} showSendFunds={this.showSendFunds} />}
|
||||
{erc721Enabled && showCollectibles && <Collectibles />}
|
||||
<SendModal
|
||||
activeScreenType="sendFunds"
|
||||
createTransaction={createTransaction}
|
||||
ethBalance={ethBalance}
|
||||
isOpen={sendFunds.isOpen}
|
||||
onClose={this.hideSendFunds}
|
||||
safeAddress={safeAddress}
|
||||
safeName={safeName}
|
||||
selectedToken={sendFunds.selectedToken}
|
||||
tokens={activeTokens}
|
||||
/>
|
||||
<Modal
|
||||
description="Receive Tokens Form"
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
// @flow
|
||||
import { md, screenSm, secondary, sm, xs } from '~/theme/variables'
|
||||
import { md, screenSm, secondary, xs } from '~/theme/variables'
|
||||
|
||||
export const styles = (theme: Object) => ({
|
||||
root: {
|
||||
marginRight: sm,
|
||||
width: '20px',
|
||||
},
|
||||
export const styles = () => ({
|
||||
controls: {
|
||||
alignItems: 'center',
|
||||
boxSizing: 'border-box',
|
||||
|
@ -62,42 +58,12 @@ export const styles = (theme: Object) => ({
|
|||
marginLeft: '0',
|
||||
},
|
||||
},
|
||||
actionIcon: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
iconSmall: {
|
||||
fontSize: 16,
|
||||
},
|
||||
receiveModal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
minHeight: '544px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
hide: {
|
||||
'&:hover': {
|
||||
backgroundColor: '#fff3e2',
|
||||
},
|
||||
'&:hover $actions': {
|
||||
visibility: 'initial',
|
||||
},
|
||||
'&:focus $actions': {
|
||||
visibility: 'initial',
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
justifyContent: 'flex-end',
|
||||
visibility: 'hidden',
|
||||
},
|
||||
receive: {
|
||||
width: '95px',
|
||||
minWidth: '95px',
|
||||
marginLeft: sm,
|
||||
borderRadius: xs,
|
||||
'& > span': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
send: {
|
||||
width: '75px',
|
||||
minWidth: '75px',
|
||||
|
@ -106,17 +72,4 @@ export const styles = (theme: Object) => ({
|
|||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: sm,
|
||||
},
|
||||
links: {
|
||||
textDecoration: 'underline',
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
currencyValueRow: {
|
||||
maxWidth: '125px',
|
||||
textAlign: 'right',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -67,6 +67,7 @@ type Props = SelectorProps &
|
|||
|
||||
const Layout = (props: Props) => {
|
||||
const {
|
||||
activateAssetsByBalance,
|
||||
activateTokensByBalance,
|
||||
activeTokens,
|
||||
addressBook,
|
||||
|
@ -313,6 +314,7 @@ const Layout = (props: Props) => {
|
|||
path={`${match.path}/balances/:assetType?`}
|
||||
render={() => (
|
||||
<Balances
|
||||
activateAssetsByBalance={activateAssetsByBalance}
|
||||
activateTokensByBalance={activateTokensByBalance}
|
||||
activeTokens={activeTokens}
|
||||
blacklistedTokens={blacklistedTokens}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/update
|
|||
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
|
||||
import fetchCurrencyValues from '~/logic/currencyValues/store/actions/fetchCurrencyValues'
|
||||
import addViewedSafe from '~/logic/currentSession/store/actions/addViewedSafe'
|
||||
import activateAssetsByBalance from '~/logic/tokens/store/actions/activateAssetsByBalance'
|
||||
import activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance'
|
||||
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
|
@ -27,6 +28,7 @@ export type Actions = {
|
|||
fetchEtherBalance: typeof fetchEtherBalance,
|
||||
fetchLatestMasterContractVersion: typeof fetchLatestMasterContractVersion,
|
||||
activateTokensByBalance: typeof activateTokensByBalance,
|
||||
activateAssetsByBalance: typeof activateAssetsByBalance,
|
||||
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe,
|
||||
fetchCurrencyValues: typeof fetchCurrencyValues,
|
||||
loadAddressBook: typeof loadAddressBookFromStorage,
|
||||
|
@ -43,6 +45,7 @@ export default {
|
|||
fetchTokens,
|
||||
fetchTransactions,
|
||||
activateTokensByBalance,
|
||||
activateAssetsByBalance,
|
||||
updateSafe,
|
||||
fetchEtherBalance,
|
||||
fetchLatestMasterContractVersion,
|
||||
|
|
|
@ -136,6 +136,7 @@ class SafeView extends React.Component<Props, State> {
|
|||
render() {
|
||||
const { sendFunds, showReceive } = this.state
|
||||
const {
|
||||
activateAssetsByBalance,
|
||||
activateTokensByBalance,
|
||||
activeTokens,
|
||||
addressBook,
|
||||
|
@ -161,6 +162,7 @@ class SafeView extends React.Component<Props, State> {
|
|||
return (
|
||||
<Page>
|
||||
<Layout
|
||||
activateAssetsByBalance={activateAssetsByBalance}
|
||||
activateTokensByBalance={activateTokensByBalance}
|
||||
activeTokens={activeTokens}
|
||||
addressBook={addressBook}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import { Set } from 'immutable'
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
|
||||
import updateSafe from './updateSafe'
|
||||
|
||||
import { type GlobalState } from '~/store'
|
||||
|
||||
// the selector uses ownProps argument/router props to get the address of the safe
|
||||
// so in order to use it I had to recreate the same structure
|
||||
// const generateMatchProps = (safeAddress: string) => ({
|
||||
// match: {
|
||||
// params: {
|
||||
// [SAFE_PARAM_ADDRESS]: safeAddress,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
const updateActiveAssets = (safeAddress: string, activeAssets: Set<string>) => async (
|
||||
dispatch: ReduxDispatch<GlobalState>,
|
||||
) => {
|
||||
dispatch(updateSafe({ address: safeAddress, activeAssets }))
|
||||
}
|
||||
|
||||
export default updateActiveAssets
|
|
@ -0,0 +1,15 @@
|
|||
// @flow
|
||||
import { Set } from 'immutable'
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
|
||||
import updateSafe from './updateSafe'
|
||||
|
||||
import { type GlobalState } from '~/store'
|
||||
|
||||
const updateBlacklistedAssets = (safeAddress: string, blacklistedAssets: Set<string>) => async (
|
||||
dispatch: ReduxDispatch<GlobalState>,
|
||||
) => {
|
||||
dispatch(updateSafe({ address: safeAddress, blacklistedAssets }))
|
||||
}
|
||||
|
||||
export default updateBlacklistedAssets
|
|
@ -11,7 +11,9 @@ export type SafeProps = {
|
|||
owners: List<Owner>,
|
||||
balances?: Map<string, string>,
|
||||
activeTokens: Set<string>,
|
||||
activeAssets: Set<string>,
|
||||
blacklistedTokens: Set<string>,
|
||||
blacklistedAssets: Set<string>,
|
||||
ethBalance?: string,
|
||||
nonce: number,
|
||||
latestIncomingTxBlock?: number,
|
||||
|
@ -28,7 +30,9 @@ const SafeRecord: RecordFactory<SafeProps> = Record({
|
|||
ethBalance: 0,
|
||||
owners: List([]),
|
||||
activeTokens: new Set(),
|
||||
activeAssets: new Set(),
|
||||
blacklistedTokens: new Set(),
|
||||
blacklistedAssets: new Set(),
|
||||
balances: Map({}),
|
||||
nonce: 0,
|
||||
latestIncomingTxBlock: 0,
|
||||
|
|
|
@ -26,7 +26,9 @@ export const buildSafe = (storedSafe: SafeProps) => {
|
|||
const addresses = storedSafe.owners.map(owner => getWeb3().utils.toChecksumAddress(owner.address))
|
||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||
const activeTokens = Set(storedSafe.activeTokens)
|
||||
const activeAssets = Set(storedSafe.activeAssets)
|
||||
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
|
||||
const blacklistedAssets = Set(storedSafe.blacklistedAssets)
|
||||
const balances = Map(storedSafe.balances)
|
||||
|
||||
const safe: SafeProps = {
|
||||
|
@ -35,6 +37,8 @@ export const buildSafe = (storedSafe: SafeProps) => {
|
|||
balances,
|
||||
activeTokens,
|
||||
blacklistedTokens,
|
||||
activeAssets,
|
||||
blacklistedAssets,
|
||||
}
|
||||
|
||||
return safe
|
||||
|
|
|
@ -191,6 +191,26 @@ export const safeActiveTokensSelector: OutputSelector<GlobalState, RouterProps,
|
|||
},
|
||||
)
|
||||
|
||||
export const safeActiveAssetsSelector: OutputSelector<GlobalState, RouterProps, List<string>> = createSelector(
|
||||
safeSelector,
|
||||
(safe: Safe) => {
|
||||
if (!safe) {
|
||||
return List()
|
||||
}
|
||||
return safe.activeAssets
|
||||
},
|
||||
)
|
||||
|
||||
export const safeActiveAssetsListSelector: OutputSelector<GlobalState, RouterProps, List<string>> = createSelector(
|
||||
safeActiveAssetsSelector,
|
||||
(safeList: []) => {
|
||||
if (!safeList) {
|
||||
return Set([])
|
||||
}
|
||||
return Set(safeList)
|
||||
},
|
||||
)
|
||||
|
||||
export const safeBlacklistedTokensSelector: OutputSelector<GlobalState, RouterProps, List<string>> = createSelector(
|
||||
safeSelector,
|
||||
(safe: Safe) => {
|
||||
|
@ -202,12 +222,29 @@ export const safeBlacklistedTokensSelector: OutputSelector<GlobalState, RouterPr
|
|||
},
|
||||
)
|
||||
|
||||
export const safeBlacklistedAssetsSelector: OutputSelector<GlobalState, RouterProps, List<string>> = createSelector(
|
||||
safeSelector,
|
||||
(safe: Safe) => {
|
||||
if (!safe) {
|
||||
return List()
|
||||
}
|
||||
|
||||
return safe.blacklistedAssets
|
||||
},
|
||||
)
|
||||
|
||||
export const safeActiveTokensSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
|
||||
safes.get(safeAddress).get('activeTokens')
|
||||
|
||||
export const safeBlacklistedTokensSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
|
||||
safes.get(safeAddress).get('blacklistedTokens')
|
||||
|
||||
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
|
||||
safes.get(safeAddress).get('activeAssets')
|
||||
|
||||
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
|
||||
safes.get(safeAddress).get('blacklistedAssets')
|
||||
|
||||
export const safeBalancesSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
|
||||
safeSelector,
|
||||
(safe: Safe) => {
|
||||
|
|
Loading…
Reference in New Issue