mirror of
https://github.com/status-im/safe-react.git
synced 2025-03-01 01:40:35 +00:00
(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
133
src/components/SafeVersionProvider/index.jsx
Normal file
133
src/components/SafeVersionProvider/index.jsx
Normal file
@ -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 Footer from '~/components/Footer'
|
||||||
import Header from '~/components/Header'
|
import Header from '~/components/Header'
|
||||||
import Notifier from '~/components/Notifier'
|
import Notifier from '~/components/Notifier'
|
||||||
|
import SafeVersionProvider from '~/components/SafeVersionProvider'
|
||||||
import SidebarProvider from '~/components/Sidebar'
|
import SidebarProvider from '~/components/Sidebar'
|
||||||
import Backdrop from '~/components/layout/Backdrop'
|
import Backdrop from '~/components/layout/Backdrop'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
@ -67,11 +68,13 @@ const PageFrame = ({ children, classes, currentNetwork }: Props) => {
|
|||||||
maxSnack={5}
|
maxSnack={5}
|
||||||
>
|
>
|
||||||
<Notifier />
|
<Notifier />
|
||||||
|
<SafeVersionProvider>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
</SafeVersionProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
<CookiesBanner />
|
<CookiesBanner />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,38 @@
|
|||||||
// @flow
|
// @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 { 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'
|
import type { GlobalState } from '~/store'
|
||||||
|
|
||||||
export const nftAssetsSelector = (state: GlobalState) => state[NFT_ASSETS_REDUCER_ID]
|
export const nftAssetsSelector = (state: GlobalState) => state[NFT_ASSETS_REDUCER_ID]
|
||||||
export const nftTokensSelector = (state: GlobalState) => state[NFT_TOKENS_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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
49
src/logic/tokens/store/actions/activateAssetsByBalance.js
Normal file
49
src/logic/tokens/store/actions/activateAssetsByBalance.js
Normal file
@ -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 { List } from 'immutable'
|
||||||
|
|
||||||
import logo from '~/assets/icons/icon_etherTokens.svg'
|
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 { type Token, makeToken } from '~/logic/tokens/store/model/token'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
@ -65,3 +66,15 @@ export const isMultisendTransaction = (data: string, value: number): boolean =>
|
|||||||
// 7de7edef - changeMasterCopy (550, 8)
|
// 7de7edef - changeMasterCopy (550, 8)
|
||||||
export const isUpgradeTransaction = (data: string) =>
|
export const isUpgradeTransaction = (data: string) =>
|
||||||
!!data && data.substr(308, 8) === 'f08a0323' && data.substr(550, 8) === '7de7edef'
|
!!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 getNetworkIdFrom = async web3Provider => {
|
||||||
const networkId = await web3Provider.eth.net.getId()
|
return await web3Provider.eth.net.getId()
|
||||||
|
|
||||||
return networkId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getProviderInfo: Function = async (
|
export const getProviderInfo: Function = async (
|
||||||
web3Provider,
|
web3Provider,
|
||||||
providerName?: string = 'Wallet',
|
providerName: string = 'Wallet',
|
||||||
): Promise<ProviderProps> => {
|
): Promise<ProviderProps> => {
|
||||||
web3 = new Web3(web3Provider)
|
web3 = new Web3(web3Provider)
|
||||||
|
|
||||||
@ -117,9 +115,7 @@ export const getProviderInfo: Function = async (
|
|||||||
|
|
||||||
export const getAddressFromENS = async (name: string) => {
|
export const getAddressFromENS = async (name: string) => {
|
||||||
const ens = new ENS(web3)
|
const ens = new ENS(web3)
|
||||||
const address = await ens.resolver(name).addr()
|
return await ens.resolver(name).addr()
|
||||||
|
|
||||||
return address
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setWeb3 = (provider: Object) => {
|
export const setWeb3 = (provider: Object) => {
|
||||||
|
129
src/routes/safe/components/Balances/Coins/index.jsx
Normal file
129
src/routes/safe/components/Balances/Coins/index.jsx
Normal file
@ -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
|
47
src/routes/safe/components/Balances/Coins/styles.js
Normal file
47
src/routes/safe/components/Balances/Coins/styles.js
Normal file
@ -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 Item from './components/Item'
|
||||||
|
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
import type { NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||||
import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
|
import { activeNftAssetsListSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
|
||||||
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
||||||
|
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||||
import { fontColor, lg, screenSm, screenXs } from '~/theme/variables'
|
import { fontColor, lg, screenSm, screenXs } from '~/theme/variables'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@ -79,9 +80,9 @@ const Collectibles = () => {
|
|||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [selectedToken, setSelectedToken] = React.useState({})
|
const [selectedToken, setSelectedToken] = React.useState({})
|
||||||
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
|
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
|
||||||
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
|
const { address, ethBalance, name } = useSelector(safeSelector)
|
||||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||||
const nftAssetsKeys = Object.keys(nftAssets)
|
const activeAssetsList = useSelector(activeNftAssetsListSelector)
|
||||||
|
|
||||||
const handleItemSend = nftToken => {
|
const handleItemSend = nftToken => {
|
||||||
setSelectedToken(nftToken)
|
setSelectedToken(nftToken)
|
||||||
@ -91,10 +92,8 @@ const Collectibles = () => {
|
|||||||
return (
|
return (
|
||||||
<Card className={classes.cardOuter}>
|
<Card className={classes.cardOuter}>
|
||||||
<div className={classes.cardInner}>
|
<div className={classes.cardInner}>
|
||||||
{nftAssetsKeys.length ? (
|
{activeAssetsList.size ? (
|
||||||
nftAssetsKeys.map(assetAddress => {
|
activeAssetsList.map(nftAsset => {
|
||||||
const nftAsset = nftAssets[assetAddress]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={nftAsset.slug}>
|
<React.Fragment key={nftAsset.slug}>
|
||||||
<div className={classes.title}>
|
<div className={classes.title}>
|
||||||
@ -122,8 +121,11 @@ const Collectibles = () => {
|
|||||||
</div>
|
</div>
|
||||||
<SendModal
|
<SendModal
|
||||||
activeScreenType="sendCollectible"
|
activeScreenType="sendCollectible"
|
||||||
|
ethBalance={ethBalance}
|
||||||
isOpen={sendNFTsModalOpen}
|
isOpen={sendNFTsModalOpen}
|
||||||
onClose={() => setSendNFTsModalOpen(false)}
|
onClose={() => setSendNFTsModalOpen(false)}
|
||||||
|
safeAddress={address}
|
||||||
|
safeName={name}
|
||||||
selectedToken={selectedToken}
|
selectedToken={selectedToken}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -24,7 +24,7 @@ import Img from '~/components/layout/Img'
|
|||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
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 type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
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 SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = {} }: Props) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||||
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
|
const nftAssets: NFTAssetsState = useSelector(safeActiveSelectorMap)
|
||||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||||
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
|
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 Close from '@material-ui/icons/Close'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { connect } from 'react-redux'
|
import { connect, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import actions, { type Actions } from './actions'
|
import actions, { type Actions } from './actions'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
@ -13,11 +13,18 @@ import Hairline from '~/components/layout/Hairline'
|
|||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
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 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 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'
|
export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn'
|
||||||
|
|
||||||
|
type ActiveScreen = 'tokenList' | 'addCustomToken' | 'assetsList' | 'addCustomAsset'
|
||||||
|
|
||||||
type Props = Actions & {
|
type Props = Actions & {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
@ -25,24 +32,25 @@ type Props = Actions & {
|
|||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
activeTokens: List<Token>,
|
activeTokens: List<Token>,
|
||||||
blacklistedTokens: List<Token>,
|
blacklistedTokens: List<Token>,
|
||||||
|
modalScreen: ActiveScreen,
|
||||||
}
|
}
|
||||||
type ActiveScreen = 'tokenList' | 'addCustomToken'
|
|
||||||
|
|
||||||
const Tokens = (props: Props) => {
|
const Tokens = (props: Props) => {
|
||||||
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('tokenList')
|
|
||||||
const {
|
const {
|
||||||
activateTokenForAllSafes,
|
activateTokenForAllSafes,
|
||||||
activeTokens,
|
|
||||||
addToken,
|
addToken,
|
||||||
blacklistedTokens,
|
|
||||||
classes,
|
classes,
|
||||||
fetchTokens,
|
fetchTokens,
|
||||||
|
modalScreen,
|
||||||
onClose,
|
onClose,
|
||||||
safeAddress,
|
safeAddress,
|
||||||
tokens,
|
|
||||||
updateActiveTokens,
|
updateActiveTokens,
|
||||||
updateBlacklistedTokens,
|
updateBlacklistedTokens,
|
||||||
} = props
|
} = props
|
||||||
|
const tokens = useSelector(orderedTokenListSelector)
|
||||||
|
const activeTokens = useSelector(extendedSafeTokensSelector)
|
||||||
|
const blacklistedTokens = useSelector(safeBlacklistedTokensSelector)
|
||||||
|
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(modalScreen)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -67,18 +75,23 @@ const Tokens = (props: Props) => {
|
|||||||
updateBlacklistedTokens={updateBlacklistedTokens}
|
updateBlacklistedTokens={updateBlacklistedTokens}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{activeScreen === 'assetsList' && <AssetsList setActiveScreen={setActiveScreen} />}
|
||||||
{activeScreen === 'addCustomToken' && (
|
{activeScreen === 'addCustomToken' && (
|
||||||
<AddCustomToken
|
<AddCustomToken
|
||||||
activateTokenForAllSafes={activateTokenForAllSafes}
|
activateTokenForAllSafes={activateTokenForAllSafes}
|
||||||
activeTokens={activeTokens}
|
activeTokens={activeTokens}
|
||||||
addToken={addToken}
|
addToken={addToken}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
parentList={'tokenList'}
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
setActiveScreen={setActiveScreen}
|
setActiveScreen={setActiveScreen}
|
||||||
tokens={tokens}
|
tokens={tokens}
|
||||||
updateActiveTokens={updateActiveTokens}
|
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,
|
setActiveScreen: Function,
|
||||||
onClose: Function,
|
onClose: Function,
|
||||||
activateTokenForAllSafes: Function,
|
activateTokenForAllSafes: Function,
|
||||||
|
parentList: 'assetsList' | 'tokenList',
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_FORM_STATE = {
|
const INITIAL_FORM_STATE = {
|
||||||
@ -54,6 +55,7 @@ const AddCustomToken = (props: Props) => {
|
|||||||
addToken,
|
addToken,
|
||||||
classes,
|
classes,
|
||||||
onClose,
|
onClose,
|
||||||
|
parentList,
|
||||||
safeAddress,
|
safeAddress,
|
||||||
setActiveScreen,
|
setActiveScreen,
|
||||||
tokens,
|
tokens,
|
||||||
@ -112,8 +114,8 @@ const AddCustomToken = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const goBackToTokenList = () => {
|
const goBack = () => {
|
||||||
setActiveScreen('tokenList')
|
setActiveScreen(parentList)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -187,7 +189,7 @@ const AddCustomToken = (props: Props) => {
|
|||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button minHeight={42} minWidth={140} onClick={goBackToTokenList}>
|
<Button minHeight={42} minWidth={140} onClick={goBack}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
|
<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
|
// @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 { 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 { List } from 'immutable'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import AssetTableCell from './AssetTableCell'
|
|
||||||
import Receive from './Receive'
|
import Receive from './Receive'
|
||||||
import Tokens from './Tokens'
|
import Tokens from './Tokens'
|
||||||
import { BALANCE_TABLE_ASSET_ID, type BalanceRow, generateColumns, getBalanceData } from './dataFetcher'
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
import Modal from '~/components/Modal'
|
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 ButtonLink from '~/components/layout/ButtonLink'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Divider from '~/components/layout/Divider'
|
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 { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||||
|
import Coins from '~/routes/safe/components/Balances/Coins'
|
||||||
import Collectibles from '~/routes/safe/components/Balances/Collectibles'
|
import Collectibles from '~/routes/safe/components/Balances/Collectibles'
|
||||||
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
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 DropdownCurrency from '~/routes/safe/components/DropdownCurrency'
|
||||||
import { history } from '~/store'
|
import { history } from '~/store'
|
||||||
|
|
||||||
@ -44,17 +33,19 @@ type State = {
|
|||||||
showCollectibles: boolean,
|
showCollectibles: boolean,
|
||||||
showReceive: boolean,
|
showReceive: boolean,
|
||||||
showToken: boolean,
|
showToken: boolean,
|
||||||
|
showManageCollectibleModal: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activateTokensByBalance: Function,
|
activateTokensByBalance: Function,
|
||||||
|
activateAssetsByBalance: Function,
|
||||||
activeTokens: List<Token>,
|
activeTokens: List<Token>,
|
||||||
blacklistedTokens: List<Token>,
|
blacklistedTokens: List<Token>,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
createTransaction?: Function,
|
createTransaction: Function,
|
||||||
currencySelected: string,
|
currencySelected: string,
|
||||||
currencyValues: BalanceCurrencyType[],
|
currencyValues: BalanceCurrencyType[],
|
||||||
ethBalance?: string,
|
ethBalance: string,
|
||||||
featuresEnabled: string[],
|
featuresEnabled: string[],
|
||||||
fetchCurrencyValues: Function,
|
fetchCurrencyValues: Function,
|
||||||
fetchTokens: Function,
|
fetchTokens: Function,
|
||||||
@ -64,7 +55,7 @@ type Props = {
|
|||||||
tokens: List<Token>,
|
tokens: List<Token>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = 'Token' | 'Send' | 'Receive'
|
type Action = 'Token' | 'Send' | 'Receive' | 'ManageCollectibleModal'
|
||||||
|
|
||||||
class Balances extends React.Component<Props, State> {
|
class Balances extends React.Component<Props, State> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -73,6 +64,7 @@ class Balances extends React.Component<Props, State> {
|
|||||||
erc721Enabled: false,
|
erc721Enabled: false,
|
||||||
subMenuOptions: [],
|
subMenuOptions: [],
|
||||||
showToken: false,
|
showToken: false,
|
||||||
|
showManageCollectibleModal: false,
|
||||||
sendFunds: {
|
sendFunds: {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
selectedToken: undefined,
|
selectedToken: undefined,
|
||||||
@ -88,9 +80,10 @@ class Balances extends React.Component<Props, State> {
|
|||||||
static isCollectiblesLocation = /\/balances\/collectibles$/
|
static isCollectiblesLocation = /\/balances\/collectibles$/
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
const { activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props
|
const { activateAssetsByBalance, activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props
|
||||||
fetchCurrencyValues(safeAddress)
|
fetchCurrencyValues(safeAddress)
|
||||||
activateTokensByBalance(safeAddress)
|
activateTokensByBalance(safeAddress)
|
||||||
|
activateAssetsByBalance(safeAddress)
|
||||||
|
|
||||||
const showCollectibles = Balances.isCollectiblesLocation.test(history.location.pathname)
|
const showCollectibles = Balances.isCollectiblesLocation.test(history.location.pathname)
|
||||||
const showCoins = Balances.isCoinsLocation.test(history.location.pathname)
|
const showCoins = Balances.isCoinsLocation.test(history.location.pathname)
|
||||||
@ -151,23 +144,17 @@ class Balances extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { erc721Enabled, sendFunds, showCoins, showCollectibles, showReceive, showToken, subMenuOptions } = this.state
|
|
||||||
const {
|
const {
|
||||||
activeTokens,
|
erc721Enabled,
|
||||||
blacklistedTokens,
|
sendFunds,
|
||||||
classes,
|
showCoins,
|
||||||
currencySelected,
|
showCollectibles,
|
||||||
currencyValues,
|
showManageCollectibleModal,
|
||||||
granted,
|
showReceive,
|
||||||
safeAddress,
|
showToken,
|
||||||
safeName,
|
subMenuOptions,
|
||||||
tokens,
|
} = this.state
|
||||||
} = this.props
|
const { activeTokens, classes, createTransaction, ethBalance, safeAddress, safeName } = this.props
|
||||||
|
|
||||||
const columns = generateColumns()
|
|
||||||
const autoColumns = columns.filter(c => !c.custom)
|
|
||||||
|
|
||||||
const filteredData = getBalanceData(activeTokens, currencySelected, currencyValues)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -190,121 +177,43 @@ class Balances extends React.Component<Props, State> {
|
|||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
<Col className={classes.tokenControls} end="sm" sm={6} xs={12}>
|
<Col className={classes.tokenControls} end="sm" sm={6} xs={12}>
|
||||||
{showCoins && (
|
{showCoins && <DropdownCurrency />}
|
||||||
<>
|
|
||||||
<DropdownCurrency />
|
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
className={classes.manageTokensButton}
|
className={classes.manageTokensButton}
|
||||||
onClick={this.onShow('Token')}
|
onClick={erc721Enabled && showCollectibles ? this.onShow('ManageCollectibleModal') : this.onShow('Token')}
|
||||||
size="lg"
|
size="lg"
|
||||||
testId="manage-tokens-btn"
|
testId="manage-tokens-btn"
|
||||||
>
|
>
|
||||||
Manage List
|
Manage List
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
<Modal
|
<Modal
|
||||||
description="Enable and disable tokens to be listed"
|
description={
|
||||||
handleClose={this.onHide('Token')}
|
erc721Enabled ? 'Enable and disables assets to be listed' : 'Enable and disable tokens to be listed'
|
||||||
open={showToken}
|
}
|
||||||
|
handleClose={showManageCollectibleModal ? this.onHide('ManageCollectibleModal') : this.onHide('Token')}
|
||||||
|
open={showToken || showManageCollectibleModal}
|
||||||
title="Manage List"
|
title="Manage List"
|
||||||
>
|
>
|
||||||
<Tokens
|
<Tokens
|
||||||
activeTokens={activeTokens}
|
modalScreen={showManageCollectibleModal ? 'assetsList' : 'tokenList'}
|
||||||
blacklistedTokens={blacklistedTokens}
|
onClose={showManageCollectibleModal ? this.onHide('ManageCollectibleModal') : this.onHide('Token')}
|
||||||
onClose={this.onHide('Token')}
|
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
tokens={tokens}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{showCoins && (
|
{showCoins && <Coins showReceiveFunds={this.onShow('Receive')} showSendFunds={this.showSendFunds} />}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
{erc721Enabled && showCollectibles && <Collectibles />}
|
{erc721Enabled && showCollectibles && <Collectibles />}
|
||||||
<SendModal
|
<SendModal
|
||||||
activeScreenType="sendFunds"
|
activeScreenType="sendFunds"
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
ethBalance={ethBalance}
|
||||||
isOpen={sendFunds.isOpen}
|
isOpen={sendFunds.isOpen}
|
||||||
onClose={this.hideSendFunds}
|
onClose={this.hideSendFunds}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
safeName={safeName}
|
||||||
selectedToken={sendFunds.selectedToken}
|
selectedToken={sendFunds.selectedToken}
|
||||||
|
tokens={activeTokens}
|
||||||
/>
|
/>
|
||||||
<Modal
|
<Modal
|
||||||
description="Receive Tokens Form"
|
description="Receive Tokens Form"
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { md, screenSm, secondary, sm, xs } from '~/theme/variables'
|
import { md, screenSm, secondary, xs } from '~/theme/variables'
|
||||||
|
|
||||||
export const styles = (theme: Object) => ({
|
export const styles = () => ({
|
||||||
root: {
|
|
||||||
marginRight: sm,
|
|
||||||
width: '20px',
|
|
||||||
},
|
|
||||||
controls: {
|
controls: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
@ -62,42 +58,12 @@ export const styles = (theme: Object) => ({
|
|||||||
marginLeft: '0',
|
marginLeft: '0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actionIcon: {
|
|
||||||
marginRight: theme.spacing(1),
|
|
||||||
},
|
|
||||||
iconSmall: {
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
receiveModal: {
|
receiveModal: {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
maxWidth: 'calc(100% - 30px)',
|
maxWidth: 'calc(100% - 30px)',
|
||||||
minHeight: '544px',
|
minHeight: '544px',
|
||||||
overflow: 'hidden',
|
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: {
|
send: {
|
||||||
width: '75px',
|
width: '75px',
|
||||||
minWidth: '75px',
|
minWidth: '75px',
|
||||||
@ -106,17 +72,4 @@ export const styles = (theme: Object) => ({
|
|||||||
fontSize: '14px',
|
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 Layout = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
|
activateAssetsByBalance,
|
||||||
activateTokensByBalance,
|
activateTokensByBalance,
|
||||||
activeTokens,
|
activeTokens,
|
||||||
addressBook,
|
addressBook,
|
||||||
@ -313,6 +314,7 @@ const Layout = (props: Props) => {
|
|||||||
path={`${match.path}/balances/:assetType?`}
|
path={`${match.path}/balances/:assetType?`}
|
||||||
render={() => (
|
render={() => (
|
||||||
<Balances
|
<Balances
|
||||||
|
activateAssetsByBalance={activateAssetsByBalance}
|
||||||
activateTokensByBalance={activateTokensByBalance}
|
activateTokensByBalance={activateTokensByBalance}
|
||||||
activeTokens={activeTokens}
|
activeTokens={activeTokens}
|
||||||
blacklistedTokens={blacklistedTokens}
|
blacklistedTokens={blacklistedTokens}
|
||||||
|
@ -4,6 +4,7 @@ import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/update
|
|||||||
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
|
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
|
||||||
import fetchCurrencyValues from '~/logic/currencyValues/store/actions/fetchCurrencyValues'
|
import fetchCurrencyValues from '~/logic/currencyValues/store/actions/fetchCurrencyValues'
|
||||||
import addViewedSafe from '~/logic/currentSession/store/actions/addViewedSafe'
|
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 activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance'
|
||||||
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||||
@ -27,6 +28,7 @@ export type Actions = {
|
|||||||
fetchEtherBalance: typeof fetchEtherBalance,
|
fetchEtherBalance: typeof fetchEtherBalance,
|
||||||
fetchLatestMasterContractVersion: typeof fetchLatestMasterContractVersion,
|
fetchLatestMasterContractVersion: typeof fetchLatestMasterContractVersion,
|
||||||
activateTokensByBalance: typeof activateTokensByBalance,
|
activateTokensByBalance: typeof activateTokensByBalance,
|
||||||
|
activateAssetsByBalance: typeof activateAssetsByBalance,
|
||||||
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe,
|
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe,
|
||||||
fetchCurrencyValues: typeof fetchCurrencyValues,
|
fetchCurrencyValues: typeof fetchCurrencyValues,
|
||||||
loadAddressBook: typeof loadAddressBookFromStorage,
|
loadAddressBook: typeof loadAddressBookFromStorage,
|
||||||
@ -43,6 +45,7 @@ export default {
|
|||||||
fetchTokens,
|
fetchTokens,
|
||||||
fetchTransactions,
|
fetchTransactions,
|
||||||
activateTokensByBalance,
|
activateTokensByBalance,
|
||||||
|
activateAssetsByBalance,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
fetchEtherBalance,
|
fetchEtherBalance,
|
||||||
fetchLatestMasterContractVersion,
|
fetchLatestMasterContractVersion,
|
||||||
|
@ -136,6 +136,7 @@ class SafeView extends React.Component<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const { sendFunds, showReceive } = this.state
|
const { sendFunds, showReceive } = this.state
|
||||||
const {
|
const {
|
||||||
|
activateAssetsByBalance,
|
||||||
activateTokensByBalance,
|
activateTokensByBalance,
|
||||||
activeTokens,
|
activeTokens,
|
||||||
addressBook,
|
addressBook,
|
||||||
@ -161,6 +162,7 @@ class SafeView extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Layout
|
<Layout
|
||||||
|
activateAssetsByBalance={activateAssetsByBalance}
|
||||||
activateTokensByBalance={activateTokensByBalance}
|
activateTokensByBalance={activateTokensByBalance}
|
||||||
activeTokens={activeTokens}
|
activeTokens={activeTokens}
|
||||||
addressBook={addressBook}
|
addressBook={addressBook}
|
||||||
|
25
src/routes/safe/store/actions/updateActiveAssets.js
Normal file
25
src/routes/safe/store/actions/updateActiveAssets.js
Normal file
@ -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
|
15
src/routes/safe/store/actions/updateBlacklistedAssets.js
Normal file
15
src/routes/safe/store/actions/updateBlacklistedAssets.js
Normal file
@ -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>,
|
owners: List<Owner>,
|
||||||
balances?: Map<string, string>,
|
balances?: Map<string, string>,
|
||||||
activeTokens: Set<string>,
|
activeTokens: Set<string>,
|
||||||
|
activeAssets: Set<string>,
|
||||||
blacklistedTokens: Set<string>,
|
blacklistedTokens: Set<string>,
|
||||||
|
blacklistedAssets: Set<string>,
|
||||||
ethBalance?: string,
|
ethBalance?: string,
|
||||||
nonce: number,
|
nonce: number,
|
||||||
latestIncomingTxBlock?: number,
|
latestIncomingTxBlock?: number,
|
||||||
@ -28,7 +30,9 @@ const SafeRecord: RecordFactory<SafeProps> = Record({
|
|||||||
ethBalance: 0,
|
ethBalance: 0,
|
||||||
owners: List([]),
|
owners: List([]),
|
||||||
activeTokens: new Set(),
|
activeTokens: new Set(),
|
||||||
|
activeAssets: new Set(),
|
||||||
blacklistedTokens: new Set(),
|
blacklistedTokens: new Set(),
|
||||||
|
blacklistedAssets: new Set(),
|
||||||
balances: Map({}),
|
balances: Map({}),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
latestIncomingTxBlock: 0,
|
latestIncomingTxBlock: 0,
|
||||||
|
@ -26,7 +26,9 @@ export const buildSafe = (storedSafe: SafeProps) => {
|
|||||||
const addresses = storedSafe.owners.map(owner => getWeb3().utils.toChecksumAddress(owner.address))
|
const addresses = storedSafe.owners.map(owner => getWeb3().utils.toChecksumAddress(owner.address))
|
||||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||||
const activeTokens = Set(storedSafe.activeTokens)
|
const activeTokens = Set(storedSafe.activeTokens)
|
||||||
|
const activeAssets = Set(storedSafe.activeAssets)
|
||||||
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
|
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
|
||||||
|
const blacklistedAssets = Set(storedSafe.blacklistedAssets)
|
||||||
const balances = Map(storedSafe.balances)
|
const balances = Map(storedSafe.balances)
|
||||||
|
|
||||||
const safe: SafeProps = {
|
const safe: SafeProps = {
|
||||||
@ -35,6 +37,8 @@ export const buildSafe = (storedSafe: SafeProps) => {
|
|||||||
balances,
|
balances,
|
||||||
activeTokens,
|
activeTokens,
|
||||||
blacklistedTokens,
|
blacklistedTokens,
|
||||||
|
activeAssets,
|
||||||
|
blacklistedAssets,
|
||||||
}
|
}
|
||||||
|
|
||||||
return safe
|
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(
|
export const safeBlacklistedTokensSelector: OutputSelector<GlobalState, RouterProps, List<string>> = createSelector(
|
||||||
safeSelector,
|
safeSelector,
|
||||||
(safe: Safe) => {
|
(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> =>
|
export const safeActiveTokensSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
|
||||||
safes.get(safeAddress).get('activeTokens')
|
safes.get(safeAddress).get('activeTokens')
|
||||||
|
|
||||||
export const safeBlacklistedTokensSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
|
export const safeBlacklistedTokensSelectorBySafe = (safeAddress: string, safes: Map<string, Safe>): List<string> =>
|
||||||
safes.get(safeAddress).get('blacklistedTokens')
|
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(
|
export const safeBalancesSelector: OutputSelector<GlobalState, RouterProps, Map<string, string>> = createSelector(
|
||||||
safeSelector,
|
safeSelector,
|
||||||
(safe: Safe) => {
|
(safe: Safe) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user