Skip to content

Commit

Permalink
feat(suite): nft section
Browse files Browse the repository at this point in the history
  • Loading branch information
enjojoy committed Dec 3, 2024
1 parent b5489cf commit deb7eaf
Show file tree
Hide file tree
Showing 24 changed files with 724 additions and 74 deletions.
7 changes: 7 additions & 0 deletions packages/blockchain-link-types/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
AddressAlias,
TokenTransfer as BlockbookTokenTransfer,
ContractInfo,
MultiTokenValue,
StakingPool,
} from './blockbook-api';

Expand Down Expand Up @@ -55,6 +56,8 @@ export interface ServerInfo {

export type TokenStandard = 'ERC20' | 'ERC1155' | 'ERC721' | 'SPL' | 'BEP20';

export type EvmNftTokenStandard = Extract<TokenStandard, 'ERC1155' | 'ERC721'>;

export type TransferType = 'sent' | 'recv' | 'self' | 'unknown';

/* Transaction */
Expand Down Expand Up @@ -188,6 +191,10 @@ export interface TokenInfo {
accounts?: TokenAccount[]; // token accounts for solana
policyId?: string; // Cardano policy id
fingerprint?: string; // Cardano starting with "asset"
multiTokenValues?: MultiTokenValue[];
ids?: string[];
totalReceived?: string;
totalSent?: string;
// transfers: number, // total transactions?
}

Expand Down
2 changes: 2 additions & 0 deletions packages/suite-desktop-ui/src/support/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> } = {
Expand All @@ -47,6 +48,7 @@ const components: { [key: string]: ComponentType<any> } = {
'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,
Expand Down
1 change: 1 addition & 0 deletions packages/suite-web/src/support/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const components: Record<PageName, LazyExoticComponent<ComponentType<any>>> = {
() => 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(
Expand Down
7 changes: 5 additions & 2 deletions packages/suite/src/components/wallet/TokenIconSetWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ export const TokenIconSetWrapper = ({ accounts, symbol }: TokenIconSetWrapperPro

if (!allTokensWithRates.length) return null;

const tokens = getTokens(allTokensWithRates, symbol, coinDefinitions)
.shownWithBalance as TokensWithRates[];
const tokens = getTokens({
tokens: allTokensWithRates,
symbol: symbol,

Check failure on line 35 in packages/suite/src/components/wallet/TokenIconSetWrapper.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

Expected property shorthand
tokenDefinitions: coinDefinitions,
})?.shownWithBalance as TokensWithRates[];

const aggregatedTokens = Object.values(
tokens.reduce((acc: Record<string, TokensWithRates>, token) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const ACCOUNT_TABS = [
'wallet-index',
'wallet-details',
'wallet-tokens',
'wallet-nfts',
'wallet-nfts-hidden',
'wallet-tokens-hidden',
'wallet-staking',
];
Expand Down Expand Up @@ -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: <Translation id="TR_NAV_NFTS" />,
isHidden: !hasNetworkFeatures(account, 'nft'),
activeRoutes: ['wallet-nfts', 'wallet-nfts-hidden'],
'data-testid': '@wallet/menu/wallet-nfts',
},
{
id: 'wallet-staking',
callback: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,22 @@ export const AccountSection = ({

const showGroup = ['ethereum', 'solana', 'cardano'].includes(networkType);

const tokens = getTokens(accountTokens, account.symbol, coinDefinitions);
const tokens = getTokens({
tokens: accountTokens,
symbol: account.symbol,
tokenDefinitions: coinDefinitions,
});

const dataTestKey = `@account-menu/${symbol}/${accountType}/${index}`;

return showGroup && (isStakeShown || tokens.shownWithBalance.length) ? (
return showGroup && (isStakeShown || tokens?.shownWithBalance?.length) ? (
<AccountItemsGroup
key={`${descriptor}-${symbol}`}
account={account}
accountLabel={accountLabel}
selected={selected}
showStaking={isStakeShown}
tokens={tokens.shownWithBalance}
tokens={tokens?.shownWithBalance}
dataTestKey={dataTestKey}
/>
) : (
Expand All @@ -62,7 +66,7 @@ export const AccountSection = ({
onClick={onItemClick}
accountLabel={accountLabel}
formattedBalance={formattedBalance}
tokens={tokens.shownWithBalance}
tokens={tokens?.shownWithBalance}
dataTestKey={dataTestKey}
/>
);
Expand Down
32 changes: 32 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2595,6 +2595,14 @@ export default defineMessages({
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.',
Expand Down Expand Up @@ -2718,6 +2726,10 @@ export default defineMessages({
defaultMessage: 'Tokens',
id: 'TR_NAV_TOKENS',
},
TR_NAV_NFTS: {
defaultMessage: 'NFT',
id: 'TR_NAV_NFTS',
},
TR_NAV_SIGN_AND_VERIFY: {
defaultMessage: 'Sign & verify',
description:
Expand Down Expand Up @@ -5289,14 +5301,34 @@ export default defineMessages({
id: 'TR_TOKENS',
defaultMessage: 'Tokens',
},
TR_COLLECTION_NAME: {
id: 'TR_COLLECTION_NAME',
defaultMessage: 'Collection name',
},
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.',
},
TR_NFT_EMPTY: {
id: 'TR_NFT_EMPTY',
defaultMessage: 'No NFT collections... yet.',
},
TR_TOKENS_EMPTY_CHECK_HIDDEN: {
id: 'TR_TOKENS_EMPTY_CHECK_HIDDEN',
defaultMessage: 'No tokens. They may be hidden.',
},
TR_NFT_EMPTY_CHECK_HIDDEN: {
id: 'TR_NFT_EMPTY_CHECK_HIDDEN',
defaultMessage: 'No NFT collections. They may be hidden.',
},
TR_HIDDEN_TOKENS_EMPTY: {
id: 'TR_HIDDEN_TOKENS_EMPTY',
defaultMessage: 'You have no hidden tokens.',
Expand Down
75 changes: 59 additions & 16 deletions packages/suite/src/utils/wallet/tokenUtils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { BigNumber } from '@trezor/utils/src/bigNumber';
import { Account, Rate, TokenAddress, RatesByKey } from '@suite-common/wallet-types';
import { TokenInfo } from '@trezor/connect';
import { getFiatRateKey, isNftToken, isTokenMatchesSearch } from '@suite-common/wallet-utils';
import {
getFiatRateKey,
isNftMatchesSearch,
isNftToken,
isTokenMatchesSearch,
} from '@suite-common/wallet-utils';
import { NetworkSymbol, getNetworkFeatures } from '@suite-common/wallet-config';
import { FiatCurrencyCode } from '@suite-common/suite-config';
import {
EnhancedTokenInfo,
TokenDefinition,
isTokenDefinitionKnown,
} from '@suite-common/token-definitions';
import { Token } from '@trezor/blockchain-link-types/src/blockbook-api';

export interface TokensWithRates extends TokenInfo {
fiatValue: BigNumber;
Expand Down Expand Up @@ -65,16 +71,46 @@ export const formatTokenSymbol = (symbol: string) => {
return isTokenSymbolLong ? `${upperCasedSymbol.slice(0, 7)}...` : upperCasedSymbol;
};

export const getTokens = (
tokens: EnhancedTokenInfo[] | TokenInfo[],
symbol: NetworkSymbol,
coinDefinitions?: TokenDefinition,
searchQuery?: string,
) => {
type GetTokens = {
tokens: EnhancedTokenInfo[] | TokenInfo[];
symbol: NetworkSymbol;
tokenDefinitions?: TokenDefinition;
searchQuery?: string;
isNft?: boolean;
};

export type TokensResult = {
shownWithBalance: EnhancedTokenInfo[];
shownWithoutBalance: EnhancedTokenInfo[];
hiddenWithBalance: EnhancedTokenInfo[];
hiddenWithoutBalance: EnhancedTokenInfo[];
unverifiedWithBalance: EnhancedTokenInfo[];
unverifiedWithoutBalance: EnhancedTokenInfo[];
};

// export type NftTokensResult = {
// shownVerified: EnhancedTokenInfo[];
// shownUnverified: EnhancedTokenInfo[];
// hiddenVerified: EnhancedTokenInfo[];
// hiddenUnverified: EnhancedTokenInfo[];
// };

export const getTokens = ({
tokens = [],
symbol,
tokenDefinitions,
searchQuery,
isNft = false,
}: GetTokens): TokensResult => {
// filter out NFT tokens until we implement them
const tokensWithoutNFTs = tokens.filter(token => !isNftToken(token));

const hasCoinDefinitions = getNetworkFeatures(symbol).includes('coin-definitions');
const filteredTokens = isNft
? tokens.filter(token => isNftToken(token))
: tokens.filter(token => !isNftToken(token));

const hasDefinitions = getNetworkFeatures(symbol).includes(
isNft ? 'nft-definitions' : 'coin-definitions',
);

const shownWithBalance: EnhancedTokenInfo[] = [];
const shownWithoutBalance: EnhancedTokenInfo[] = [];
Expand All @@ -83,16 +119,23 @@ export const getTokens = (
const unverifiedWithBalance: EnhancedTokenInfo[] = [];
const unverifiedWithoutBalance: EnhancedTokenInfo[] = [];

tokensWithoutNFTs.forEach(token => {
const isKnown = isTokenDefinitionKnown(coinDefinitions?.data, symbol, token.contract);
const isHidden = coinDefinitions?.hide.includes(token.contract);
const isShown = coinDefinitions?.show.includes(token.contract);
filteredTokens.forEach(token => {
const isKnown = isTokenDefinitionKnown(tokenDefinitions?.data, symbol, token.contract);
const isHidden = tokenDefinitions?.hide.includes(token.contract);
const isShown = tokenDefinitions?.show.includes(token.contract);

const query = searchQuery ? searchQuery.trim().toLowerCase() : '';

if (searchQuery && !isTokenMatchesSearch(token, query)) return;
if (
searchQuery &&
(!isTokenMatchesSearch(token, query) ||
(isNft && isNftMatchesSearch(token as Token, query)))
)
return;

const hasBalance = new BigNumber(token?.balance || '0').gt(0);
const hasBalance =
new BigNumber(token?.balance || '0').gt(0) ||
(isNft && (token?.multiTokenValues?.length || 0) > 0);

const pushToArray = (
arrayWithBalance: EnhancedTokenInfo[],
Expand All @@ -107,7 +150,7 @@ export const getTokens = (

if (isShown) {
pushToArray(shownWithBalance, shownWithoutBalance);
} else if (hasCoinDefinitions && !isKnown) {
} else if (hasDefinitions && !isKnown) {
pushToArray(unverifiedWithBalance, unverifiedWithoutBalance);
} else if (isHidden) {
pushToArray(hiddenWithBalance, hiddenWithoutBalance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ export const handleTokensAndStakingData = (
const assetStakingBalance = accountsThatStaked.reduce((total, account) => {
return total.plus(getAccountTotalStakingBalance(account));
}, new BigNumber(0));
const tokens = getTokens(assetTokens ?? [], symbol, coinDefinitions);
const tokens = getTokens({
tokens: assetTokens ?? [],
symbol,
tokenDefinitions: coinDefinitions,
});
const tokensWithRates = enhanceTokensWithRates(
tokens.shownWithBalance ?? [],
tokens?.shownWithBalance ?? [],
localCurrency,
symbol,
currentFiatRates,
Expand Down
74 changes: 74 additions & 0 deletions packages/suite/src/views/wallet/nfts/HiddenNfts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Banner, Column, H3 } from '@trezor/components';
import { SelectedAccountLoaded } from '@suite-common/wallet-types';
import { selectNftDefinitions } from '@suite-common/token-definitions';

import { Translation } from 'src/components/suite';
import { useSelector } from 'src/hooks/suite';
import { getTokens } from 'src/utils/wallet/tokenUtils';

import NftsTable from './NftsTable/NftsTable';
import { NoTokens } from '../tokens/common/NoTokens';

type NftsTableProps = {
selectedAccount: SelectedAccountLoaded;
searchQuery: string;
};

export const HiddenNfts = ({ selectedAccount, searchQuery }: NftsTableProps) => {
// const filteredTokens = filterNftTokens(selectedAccount.account.tokens || []);
const nftDefinitions = useSelector(state =>
selectNftDefinitions(state, selectedAccount.account.symbol),
);
const nfts = getTokens({
tokens: selectedAccount.account.tokens ?? [],
symbol: selectedAccount.account.symbol,
tokenDefinitions: nftDefinitions,
isNft: true,
searchQuery,
});

return nfts &&
(nfts.hiddenWithBalance.length > 0 ||
nfts.hiddenWithoutBalance.length > 0 ||
nfts.unverifiedWithBalance.length > 0 ||
nfts.unverifiedWithoutBalance.length > 0) ? (
<Column gap={24} alignItems="stretch">
<NftsTable
selectedAccount={selectedAccount}
type="ERC721"
isShown={false}
verified={true}
nfts={nfts}
/>
<NftsTable
selectedAccount={selectedAccount}
type="ERC1155"
isShown={false}
verified={true}
nfts={nfts}
/>
<H3>
<Translation id="TR_COLLECTIONS_UNRECOGNIZED_BY_TREZOR" />
</H3>
<Banner variant="warning" icon>
<Translation id="TR_NFT_UNRECOGNIZED_BY_TREZOR_TOOLTIP" />
</Banner>
<NftsTable
selectedAccount={selectedAccount}
type="ERC721"
isShown={false}
verified={false}
nfts={nfts}
/>
<NftsTable
selectedAccount={selectedAccount}
type="ERC1155"
isShown={false}
verified={false}
nfts={nfts}
/>
</Column>
) : (
<NoTokens title={<Translation id="TR_TOKENS_EMPTY" />} />
);
};
Loading

0 comments on commit deb7eaf

Please sign in to comment.