From 13d1ed5eb04379b7bc9aafa383e6f9ab2bb6dbcf Mon Sep 17 00:00:00 2001 From: Albina Nikiforova Date: Thu, 21 Nov 2024 12:21:27 +0100 Subject: [PATCH] feat(suite): nft section --- .../suite-desktop-ui/src/support/Router.tsx | 2 + packages/suite-web/src/support/Router.tsx | 1 + .../AccountTopPanel/AccountNavigation.tsx | 12 + packages/suite/src/support/messages.ts | 24 ++ packages/suite/src/utils/wallet/nftUtils.ts | 60 +++++ .../src/views/wallet/nfts/HiddenNfts.tsx | 52 ++++ .../src/views/wallet/nfts/NftsNavigation.tsx | 87 +++++++ .../views/wallet/nfts/NftsTable/NftsRow.tsx | 227 ++++++++++++++++++ .../views/wallet/nfts/NftsTable/NftsTable.tsx | 93 +++++++ .../suite/src/views/wallet/nfts/ShownNfts.tsx | 32 +++ .../suite/src/views/wallet/nfts/index.tsx | 51 ++++ suite-common/suite-config/src/routes.ts | 14 ++ suite-common/wallet-utils/src/accountUtils.ts | 13 + .../wallet-utils/src/transactionUtils.ts | 7 + 14 files changed, 675 insertions(+) create mode 100644 packages/suite/src/utils/wallet/nftUtils.ts create mode 100644 packages/suite/src/views/wallet/nfts/HiddenNfts.tsx create mode 100644 packages/suite/src/views/wallet/nfts/NftsNavigation.tsx create mode 100644 packages/suite/src/views/wallet/nfts/NftsTable/NftsRow.tsx create mode 100644 packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx create mode 100644 packages/suite/src/views/wallet/nfts/ShownNfts.tsx create mode 100644 packages/suite/src/views/wallet/nfts/index.tsx diff --git a/packages/suite-desktop-ui/src/support/Router.tsx b/packages/suite-desktop-ui/src/support/Router.tsx index 4f954558a955..dd7e7a928d52 100644 --- a/packages/suite-desktop-ui/src/support/Router.tsx +++ b/packages/suite-desktop-ui/src/support/Router.tsx @@ -33,6 +33,7 @@ import { SettingsCoins } from 'src/views/settings/SettingsCoins/SettingsCoins'; import { SettingsDebug } from 'src/views/settings/SettingsDebug/SettingsDebug'; import { SettingsDevice } from 'src/views/settings/SettingsDevice/SettingsDevice'; import { Tokens } from 'src/views/wallet/tokens'; +import { Nfts } from 'src/views/wallet/nfts'; import PasswordManager from 'src/views/password-manager'; const components: { [key: string]: ComponentType } = { @@ -47,6 +48,7 @@ const components: { [key: string]: ComponentType } = { 'wallet-sign-verify': WalletSignVerify, 'wallet-anonymize': WalletAnonymize, 'wallet-tokens': Tokens, + 'wallet-nfts': Nfts, 'wallet-coinmarket-buy': CoinmarketBuyForm, 'wallet-coinmarket-buy-detail': CoinmarketBuyDetail, 'wallet-coinmarket-buy-offers': CoinmarketBuyOffers, diff --git a/packages/suite-web/src/support/Router.tsx b/packages/suite-web/src/support/Router.tsx index 5e09600f4b35..77f67f69128f 100644 --- a/packages/suite-web/src/support/Router.tsx +++ b/packages/suite-web/src/support/Router.tsx @@ -31,6 +31,7 @@ const components: Record>> = { () => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/details'), ), 'wallet-tokens': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/tokens')), + 'wallet-nfts': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/nfts')), 'wallet-send': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/send')), 'wallet-staking': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/staking/WalletStaking').then( diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountTopPanel/AccountNavigation.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountTopPanel/AccountNavigation.tsx index 9a61238f7b9e..609ef8d5b206 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountTopPanel/AccountNavigation.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountTopPanel/AccountNavigation.tsx @@ -13,6 +13,8 @@ export const ACCOUNT_TABS = [ 'wallet-index', 'wallet-details', 'wallet-tokens', + 'wallet-nfts', + 'wallet-nfts-hidden', 'wallet-tokens-hidden', 'wallet-staking', ]; @@ -54,6 +56,16 @@ export const AccountNavigation = () => { activeRoutes: ['wallet-tokens', 'wallet-tokens-hidden'], 'data-testid': '@wallet/menu/wallet-tokens', }, + { + id: 'wallet-nfts', + callback: () => { + goToWithAnalytics('wallet-nfts', { preserveParams: true }); + }, + title: , + isHidden: !['ethereum'].includes(networkType), + activeRoutes: ['wallet-nfts', 'wallet-nfts-hidden'], + 'data-testid': '@wallet/menu/wallet-nfts', + }, { id: 'wallet-staking', callback: () => { diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 2d308dc09ffa..a81be8c206cd 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -2598,6 +2598,14 @@ const messages = defineMessagesWithTypeCheck({ defaultMessage: 'Unrecognized tokens pose potential risks. Use caution.', id: 'TR_TOKEN_UNRECOGNIZED_BY_TREZOR_TOOLTIP', }, + TR_COLLECTIONS_UNRECOGNIZED_BY_TREZOR: { + defaultMessage: 'Unrecognized collections', + id: 'TR_COLLECTIONS_UNRECOGNIZED_BY_TREZOR', + }, + TR_NFT_UNRECOGNIZED_BY_TREZOR_TOOLTIP: { + defaultMessage: 'Unrecognized NFTs pose potential risks. Use caution.', + id: 'TR_NFT_UNRECOGNIZED_BY_TREZOR_TOOLTIP', + }, TR_LEARN: { defaultMessage: 'Learn', description: 'Link to Suite Guide.', @@ -2721,6 +2729,10 @@ const messages = defineMessagesWithTypeCheck({ defaultMessage: 'Tokens', id: 'TR_NAV_TOKENS', }, + TR_NAV_NFTS: { + defaultMessage: 'NFT', + id: 'TR_NAV_NFTS', + }, TR_NAV_SIGN_AND_VERIFY: { defaultMessage: 'Sign & Verify', description: @@ -5285,6 +5297,18 @@ const messages = defineMessagesWithTypeCheck({ id: 'TR_TOKENS', defaultMessage: 'Tokens', }, + TR_COLLECTION: { + id: 'TR_COLLECTION', + defaultMessage: 'Collection', + }, + TR_ID: { + id: 'TR_ID', + defaultMessage: 'ID', + }, + TR_NFT: { + id: 'TR_NFT', + defaultMessage: 'NFT', + }, TR_TOKENS_EMPTY: { id: 'TR_TOKENS_EMPTY', defaultMessage: 'No tokens... yet.', diff --git a/packages/suite/src/utils/wallet/nftUtils.ts b/packages/suite/src/utils/wallet/nftUtils.ts new file mode 100644 index 000000000000..d5a56325d1e4 --- /dev/null +++ b/packages/suite/src/utils/wallet/nftUtils.ts @@ -0,0 +1,60 @@ +import { isTokenDefinitionKnown, TokenDefinition } from '@suite-common/token-definitions'; +import { isNftMatchesSearch, filterNftTokens } from '@suite-common/wallet-utils'; +import { NetworkSymbol, getNetworkFeatures } from '@suite-common/wallet-config'; +import { Token } from '@trezor/blockchain-link-types/src/blockbook-api'; + +type GetNfts = { + tokens: Token[]; + symbol: NetworkSymbol; + nftDefinitions?: TokenDefinition; + searchQuery?: string; +}; + +export type NftType = 'ERC721' | 'ERC1155'; + +export const getNfts = ({ tokens, symbol, nftDefinitions, searchQuery }: GetNfts) => { + // filter out NFT tokens until we implement them + const nfts = filterNftTokens(tokens); + + const hasNftDefinitions = getNetworkFeatures(symbol).includes('nft-definitions'); + + const shownVerified: Token[] = []; + const shownUnverified: Token[] = []; + const hiddenVerified: Token[] = []; + const hiddenUnverified: Token[] = []; + + nfts.forEach(token => { + const isKnown = isTokenDefinitionKnown(nftDefinitions?.data, symbol, token.contract || ''); + const isHidden = nftDefinitions?.hide.includes(token.contract || ''); + const isShown = nftDefinitions?.show.includes(token.contract || ''); + + const query = searchQuery ? searchQuery.trim().toLowerCase() : ''; + + if (searchQuery && !isNftMatchesSearch(token, query)) return; + + const pushToArray = (arrayVerified: Token[], arrayUnverified: Token[]) => { + if (isKnown) { + arrayVerified.push(token); + } else { + arrayUnverified.push(token); + } + }; + + if (isShown) { + pushToArray(shownVerified, shownUnverified); + } else if (hasNftDefinitions && !isKnown) { + pushToArray(hiddenVerified, hiddenUnverified); + } else if (isHidden) { + pushToArray(hiddenVerified, hiddenUnverified); + } else { + pushToArray(shownVerified, shownUnverified); + } + }); + + return { + shownVerified, + shownUnverified, + hiddenVerified, + hiddenUnverified, + }; +}; diff --git a/packages/suite/src/views/wallet/nfts/HiddenNfts.tsx b/packages/suite/src/views/wallet/nfts/HiddenNfts.tsx new file mode 100644 index 000000000000..e4357beb0a8e --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/HiddenNfts.tsx @@ -0,0 +1,52 @@ +import { Banner, Column, H3 } from '@trezor/components'; +import { SelectedAccountLoaded } from '@suite-common/wallet-types'; +import NftsTable from './NftsTable/NftsTable'; +import { Translation } from 'src/components/suite'; + +type NftsTableProps = { + selectedAccount: SelectedAccountLoaded; + searchQuery: string; +}; + +const HiddenNfts = ({ selectedAccount, searchQuery }: NftsTableProps) => { + return ( + + + +

+ +

+ + + + + +
+ ); +}; + +export default HiddenNfts; diff --git a/packages/suite/src/views/wallet/nfts/NftsNavigation.tsx b/packages/suite/src/views/wallet/nfts/NftsNavigation.tsx new file mode 100644 index 000000000000..dbbad4ff1611 --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/NftsNavigation.tsx @@ -0,0 +1,87 @@ +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +import { SelectedAccountLoaded } from '@suite-common/wallet-types'; +import { selectNftDefinitions } from '@suite-common/token-definitions'; +import { spacings } from '@trezor/theme'; +import { Row } from '@trezor/components'; + +import { useSelector } from 'src/hooks/suite'; +import { NavigationItem } from 'src/components/suite/layouts/SuiteLayout/Sidebar/NavigationItem'; +import { getNfts } from 'src/utils/wallet/nftUtils'; +import { selectRouteName } from 'src/reducers/suite/routerReducer'; +import { SearchAction } from 'src/components/wallet/SearchAction'; +import { filterNftTokens } from '@suite-common/wallet-utils'; + +interface NftsNavigationProps { + selectedAccount: SelectedAccountLoaded; + searchQuery: string; + setSearchQuery: Dispatch>; +} + +export const NftsNavigation = ({ + selectedAccount, + searchQuery, + setSearchQuery, +}: NftsNavigationProps) => { + const { account } = selectedAccount; + + const [isExpanded, setExpanded] = useState(false); + + const routeName = useSelector(selectRouteName); + + const nftDefinitions = useSelector(state => + selectNftDefinitions(state, selectedAccount.account.symbol), + ); + + const filteredTokens = filterNftTokens(account.tokens || []); + + const nfts = getNfts({ tokens: filteredTokens, symbol: account.symbol, nftDefinitions }); + + useEffect(() => { + setSearchQuery(''); + setExpanded(false); + }, [account.symbol, account.index, account.accountType, setSearchQuery]); + + return ( + + + + + + + + + + ); +}; diff --git a/packages/suite/src/views/wallet/nfts/NftsTable/NftsRow.tsx b/packages/suite/src/views/wallet/nfts/NftsTable/NftsRow.tsx new file mode 100644 index 000000000000..9e954c5d2cc4 --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/NftsTable/NftsRow.tsx @@ -0,0 +1,227 @@ +import { Network } from '@suite-common/wallet-config'; +import { DefinitionType, tokenDefinitionsActions } from '@suite-common/token-definitions'; +import { TokenManagementAction } from '@suite-common/token-definitions'; +import { Token } from '@trezor/blockchain-link-types/src/blockbook-api'; +import { + Button, + Column, + Dropdown, + GroupedMenuItems, + Icon, + Link, + Row, + Table, + TruncateWithTooltip, +} from '@trezor/components'; +import { Translation } from 'src/components/suite'; + +import { NftType } from 'src/utils/wallet/nftUtils'; + +import { useDispatch, useSelector } from 'src/hooks/suite'; +import { SUITE } from 'src/actions/suite/constants'; + +import { useTranslation } from 'src/hooks/suite'; +import styled from 'styled-components'; +import { spacingsPx } from '@trezor/theme'; +import { selectIsCopyAddressModalShown } from 'src/reducers/suite/suiteReducer'; +import { openModal } from 'src/actions/suite/modalActions'; +import { copyToClipboard } from '@trezor/dom-utils'; +import { notificationsActions } from '@suite-common/toast-notifications'; +import { AddressType } from '@suite-common/wallet-types'; + +type NftsRowProps = { + nft: Token; + type: NftType; + network: Network; + shown?: boolean; +}; + +const ContractAddress = styled.div` + display: inline-block; + max-width: 200px; + word-break: break-all; + white-space: wrap; +`; + +const IconWrapper = styled.div` + display: inline-block; + margin-left: ${spacingsPx.xxs}; +`; + +const NftsRow = ({ nft, type, network, shown }: NftsRowProps) => { + const dispatch = useDispatch(); + + const { translationString } = useTranslation(); + const shouldShowCopyAddressModal = useSelector(selectIsCopyAddressModalShown); + + const getNftContractExplorerUrl = (network: Network, nft: Token) => { + const explorerUrl = network.explorer.account; + const contractAddress = nft.contract; + const queryString = network.explorer.queryString ?? ''; + + return `${explorerUrl}${contractAddress}${queryString}`; + }; + + const getNftExplorerUrl = (network: Network, nft: Token, id?: string) => { + const explorerUrl = network.explorer.nft; + const contractAddressWithId = nft.contract + `/${id}`; + + return `${explorerUrl}${contractAddressWithId}`; + }; + + const onCopyAddress = (address: string, addressType: AddressType) => { + if (shouldShowCopyAddressModal) { + dispatch( + openModal({ + type: 'copy-address', + addressType, + address, + }), + ); + } else { + const result = copyToClipboard(address); + if (typeof result !== 'string') { + dispatch(notificationsActions.addToast({ type: 'copy-to-clipboard' })); + } + } + }; + + const name = nft.name.length > 15 ? `${nft.name.slice(0, 15)}...` : nft.name; + + return ( + + + {name} + + {type === 'ERC1155' && ( + <> + + + {nft.multiTokenValues?.map(value => ( + + + {value.id?.toString()} + + + ))} + + + + + {nft.multiTokenValues?.map(value => ( + {value.value?.toString()} + ))} + + + + )} + {type === 'ERC721' && ( + + + {nft.ids?.map(id => ( + + {id.toString()} + + ))} + + + )} + + , + icon: 'hide', + onClick: () => + dispatch( + tokenDefinitionsActions.setTokenStatus({ + networkSymbol: network.symbol, + contractAddress: nft.contract || '', + status: TokenManagementAction.HIDE, + type: DefinitionType.NFT, + }), + ), + isHidden: shown === false, + }, + { + label: , + icon: 'newspaper', + onClick: () => { + dispatch({ + type: SUITE.SET_TRANSACTION_HISTORY_PREFILL, + payload: nft.contract || '', + }); + }, + }, + { + label: , + icon: 'arrowUpRight', + onClick: () => { + window.open( + getNftContractExplorerUrl(network, nft), + '_blank', + ); + }, + }, + ], + }, + { + key: 'contract-address', + label: translationString('TR_CONTRACT_ADDRESS'), + options: [ + { + label: ( + + {nft.contract} + + + + + ), + onClick: () => + onCopyAddress(nft.contract || '', 'contract'), + }, + ], + }, + ].filter(category => category) as GroupedMenuItems[] + } + /> + {!shown && ( + + )} + + + ); +}; + +export default NftsRow; diff --git a/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx b/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx new file mode 100644 index 000000000000..a1917a27044f --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx @@ -0,0 +1,93 @@ +import { SelectedAccountLoaded } from '@suite-common/wallet-types'; +import { Card, Column, Table, H3 } from '@trezor/components'; +import { selectNftDefinitions } from '@suite-common/token-definitions'; + +import { Translation } from 'src/components/suite/Translation'; +import { useSelector } from 'src/hooks/suite'; +import { getNfts } from 'src/utils/wallet/nftUtils'; + +import NftsRow from './NftsRow'; +import { filterNftTokens } from '@suite-common/wallet-utils'; +import { Token } from '@trezor/blockchain-link-types/src/blockbook-api'; +import { getNetwork } from '@suite-common/wallet-config'; + +type NftsTableProps = { + selectedAccount: SelectedAccountLoaded; + searchQuery: string; + type: 'ERC721' | 'ERC1155'; + shown?: boolean; + verified?: boolean; +}; + +const NftsTable = ({ selectedAccount, searchQuery, type, shown, verified }: NftsTableProps) => { + const { account } = selectedAccount; + const nftDefinitions = useSelector(state => selectNftDefinitions(state, account.symbol)); + const filteredTokens = filterNftTokens(account?.tokens || []); + const nfts = getNfts({ tokens: filteredTokens, symbol: account.symbol, nftDefinitions }); + const network = getNetwork(account.symbol); + + const filterNftsByType = (nfts: Token[]) => { + return nfts.filter(nft => nft.type === type); + }; + + const getNftsToShow = () => { + if (shown) { + return [...nfts.shownVerified, ...nfts.shownUnverified]; + } + + return verified ? nfts.hiddenVerified : nfts.hiddenUnverified; + }; + + const nftsToShow = getNftsToShow(); + const filteredNfts = filterNftsByType(nftsToShow); + + if (filteredNfts.length === 0) { + return null; + } + + return ( + +

{type}

+ + + + + + + + {type === 'ERC1155' && ( + <> + + + + + + + + + )} + {type === 'ERC721' && ( + + + + )} + + + + {filteredNfts.map(nft => ( + + ))} + +
+
+
+ ); +}; + +export default NftsTable; diff --git a/packages/suite/src/views/wallet/nfts/ShownNfts.tsx b/packages/suite/src/views/wallet/nfts/ShownNfts.tsx new file mode 100644 index 000000000000..dfb7cfb2ac5c --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/ShownNfts.tsx @@ -0,0 +1,32 @@ +import { Column } from '@trezor/components'; +import { SelectedAccountLoaded } from '@suite-common/wallet-types'; + +import NftsTable from './NftsTable/NftsTable'; + +type ShownNftsProps = { + selectedAccount: SelectedAccountLoaded; + searchQuery: string; +}; + +const ShownNfts = ({ selectedAccount, searchQuery }: ShownNftsProps) => { + return ( + + + + + ); +}; + +export default ShownNfts; diff --git a/packages/suite/src/views/wallet/nfts/index.tsx b/packages/suite/src/views/wallet/nfts/index.tsx new file mode 100644 index 000000000000..e61bf8a80056 --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/index.tsx @@ -0,0 +1,51 @@ +import { useState, useEffect } from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { WalletLayout } from 'src/components/wallet'; +import { useDispatch, useSelector } from 'src/hooks/suite'; +import { goto } from 'src/actions/suite/routerActions'; + +import { NftsNavigation } from './NftsNavigation'; +import HiddenNfts from './HiddenNfts'; +import ShownNfts from './ShownNfts'; + +export const Nfts = () => { + const [searchQuery, setSearchQuery] = useState(''); + + const { selectedAccount } = useSelector(state => state.wallet); + + const dispatch = useDispatch(); + + useEffect(() => { + if ( + selectedAccount.status === 'loaded' && + !selectedAccount.network?.features.includes('tokens') + ) { + dispatch(goto('wallet-index', { preserveParams: true })); + } + }, [selectedAccount, dispatch]); + + if (selectedAccount.status !== 'loaded') { + return ; + } + + return ( + + + + + + + + + + + + ); +}; + +export default Nfts; diff --git a/suite-common/suite-config/src/routes.ts b/suite-common/suite-config/src/routes.ts index e420bca15a4a..8eb04dd51809 100644 --- a/suite-common/suite-config/src/routes.ts +++ b/suite-common/suite-config/src/routes.ts @@ -276,6 +276,20 @@ export const routes = [ params: walletParams, isNestedRoute: true, }, + { + name: 'wallet-nfts', + pattern: '/accounts/nfts', + app: 'wallet', + params: walletParams, + }, + { + name: 'wallet-nfts-hidden', + pattern: '/accounts/nfts/hidden', + app: 'wallet', + params: walletParams, + isNestedRoute: true, + }, + { name: 'wallet-anonymize', pattern: '/accounts/anonymize', diff --git a/suite-common/wallet-utils/src/accountUtils.ts b/suite-common/wallet-utils/src/accountUtils.ts index 0efa3da3c2e1..721ac3182e71 100644 --- a/suite-common/wallet-utils/src/accountUtils.ts +++ b/suite-common/wallet-utils/src/accountUtils.ts @@ -44,6 +44,7 @@ import { toFiatCurrency } from './fiatConverterUtils'; import { getFiatRateKey } from './fiatRatesUtils'; import { getAccountTotalStakingBalance } from './ethereumStakingUtils'; import { isRbfTransaction } from './transactionUtils'; +import { Token } from '@trezor/blockchain-link-types/src/blockbook-api'; export const isUtxoBased = (account: Account) => account.networkType === 'bitcoin' || account.networkType === 'cardano'; @@ -1177,3 +1178,15 @@ export const isTokenMatchesSearch = (token: TokenInfo, search: string) => { token.policyId?.toLowerCase().includes(search) ); }; + +export const isNftMatchesSearch = (token: Token, search: string) => { + return ( + token.symbol?.toLowerCase().includes(search) || + token.name?.toLowerCase().includes(search) || + token.contract?.toLowerCase().includes(search) || + token.ids + ?.map(id => id.toString()) + .join(', ') + .includes(search) + ); +}; diff --git a/suite-common/wallet-utils/src/transactionUtils.ts b/suite-common/wallet-utils/src/transactionUtils.ts index 12b0be8287e9..cbcd7012662c 100644 --- a/suite-common/wallet-utils/src/transactionUtils.ts +++ b/suite-common/wallet-utils/src/transactionUtils.ts @@ -20,6 +20,7 @@ import { AccountTransaction, TokenTransfer, InternalTransfer, + TokenInfo, } from '@trezor/connect'; import { SignOperator } from '@suite-common/suite-types'; import { arrayPartition } from '@trezor/utils'; @@ -29,6 +30,7 @@ import { FiatCurrencyCode } from '@suite-common/suite-config'; import { formatAmount, formatNetworkAmount, isTokenMatchesSearch } from './accountUtils'; import { toFiatCurrency } from '../src/fiatConverterUtils'; import { getFiatRateKey, roundTimestampToNearestPastHour } from './fiatRatesUtils'; +import { Token } from '@trezor/blockchain-link-types/src/blockbook-api'; export const sortByBlockHeight = (a: { blockHeight?: number }, b: { blockHeight?: number }) => { // if both are missing the blockHeight don't change their order @@ -484,6 +486,11 @@ export const getNftTokenId = (transfer: TokenTransfer) => ? transfer.multiTokenValues[0].id : transfer.amount; +export const isNftToken = (token: Token) => token.type === 'ERC721' || token.type === 'ERC1155'; + +export const filterNftTokens = (tokens: Token[] | TokenInfo[]) => + tokens.filter(token => isNftToken(token as Token)) as Token[]; + export const getTxIcon = (txType: WalletAccountTransaction['type']) => { switch (txType) { case 'recv':