Commit 98141031 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #589 from blockscout/token-select-fix

use tokens resources for token select
parents 7ba4e7e0 8d72bc50
...@@ -2,7 +2,6 @@ import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, Ap ...@@ -2,7 +2,6 @@ import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, Ap
import type { import type {
Address, Address,
AddressCounters, AddressCounters,
AddressTokenBalance,
AddressTransactionsResponse, AddressTransactionsResponse,
AddressTokenTransferResponse, AddressTokenTransferResponse,
AddressCoinBalanceHistoryResponse, AddressCoinBalanceHistoryResponse,
...@@ -151,9 +150,10 @@ export const RESOURCES = { ...@@ -151,9 +150,10 @@ export const RESOURCES = {
address_counters: { address_counters: {
path: '/api/v2/addresses/:id/counters', path: '/api/v2/addresses/:id/counters',
}, },
address_token_balances: { // this resource doesn't have pagination, so causing huge problems on some addresses page
path: '/api/v2/addresses/:id/token-balances', // address_token_balances: {
}, // path: '/api/v2/addresses/:id/token-balances',
// },
address_txs: { address_txs: {
path: '/api/v2/addresses/:id/transactions', path: '/api/v2/addresses/:id/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
...@@ -363,7 +363,6 @@ Q extends 'tx_raw_trace' ? RawTracesResponse : ...@@ -363,7 +363,6 @@ Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'addresses' ? AddressesResponse : Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address : Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters : Q extends 'address_counters' ? AddressCounters :
Q extends 'address_token_balances' ? Array<AddressTokenBalance> :
Q extends 'address_txs' ? AddressTransactionsResponse : Q extends 'address_txs' ? AddressTransactionsResponse :
Q extends 'address_internal_txs' ? AddressInternalTxsResponse : Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
Q extends 'address_token_transfers' ? AddressTokenTransferResponse : Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
......
import type BigNumber from 'bignumber.js';
export default function sumBnReducer(result: BigNumber, item: BigNumber) {
return result.plus(item);
}
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { ZERO } from 'lib/consts';
interface Params { interface Params {
value: string; value: string;
exchangeRate?: string | null; exchangeRate?: string | null;
...@@ -13,10 +15,11 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal ...@@ -13,10 +15,11 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat(); const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
let usdResult: string | undefined; let usdResult: string | undefined;
let usdBn = ZERO;
if (exchangeRate) { if (exchangeRate) {
const exchangeRateBn = new BigNumber(exchangeRate); const exchangeRateBn = new BigNumber(exchangeRate);
const usdBn = valueCurr.times(exchangeRateBn); usdBn = valueCurr.times(exchangeRateBn);
if (accuracyUsd && !usdBn.isEqualTo(0)) { if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd); const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat(); usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
...@@ -25,5 +28,5 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal ...@@ -25,5 +28,5 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
} }
} }
return { valueStr: valueResult, usd: usdResult }; return { valueStr: valueResult, usd: usdResult, usdBn };
} }
...@@ -80,20 +80,26 @@ export const erc1155LongId: AddressTokenBalance = { ...@@ -80,20 +80,26 @@ export const erc1155LongId: AddressTokenBalance = {
value: '42', value: '42',
}; };
export const baseList = [ export const erc20List = {
erc20a, items: [
erc20b, erc20a,
erc20c, erc20b,
erc721a, erc20c,
erc721b, ],
erc721c, };
erc1155withoutName,
erc1155a, export const erc721List = {
erc1155b, items: [
]; erc721a,
erc721b,
export const longValuesList = [ erc721c,
erc20LongSymbol, ],
erc721LongSymbol, };
erc1155LongId,
]; export const erc1155List = {
items: [
erc1155withoutName,
erc1155a,
erc1155b,
],
};
...@@ -8,7 +8,7 @@ import type { Address } from 'types/api/address'; ...@@ -8,7 +8,7 @@ import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters'; import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokensMock from 'mocks/address/tokens';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -18,7 +18,9 @@ import MockAddressPage from './testUtils/MockAddressPage'; ...@@ -18,7 +18,9 @@ import MockAddressPage from './testUtils/MockAddressPage';
const ADDRESS_HASH = addressMock.hash; const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH });
const API_URL_COUNTERS = buildApiUrl('address_counters', { id: ADDRESS_HASH }); const API_URL_COUNTERS = buildApiUrl('address_counters', { id: ADDRESS_HASH });
const API_URL_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH }); const API_URL_TOKENS_ERC20 = buildApiUrl('address_tokens', { id: ADDRESS_HASH }) + '?type=ERC-20';
const API_URL_TOKENS_ERC721 = buildApiUrl('address_tokens', { id: ADDRESS_HASH }) + '?type=ERC-721';
const API_URL_TOKENS_ER1155 = buildApiUrl('address_tokens', { id: ADDRESS_HASH }) + '?type=ERC-1155';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH }, query: { id: ADDRESS_HASH },
...@@ -54,10 +56,18 @@ test('token', async({ mount, page }) => { ...@@ -54,10 +56,18 @@ test('token', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify(countersMock.forToken), body: JSON.stringify(countersMock.forToken),
})); }));
await page.route(API_URL_TOKEN_BALANCES, (route) => route.fulfill({ await page.route(API_URL_TOKENS_ERC20, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(tokenBalanceMock.baseList), body: JSON.stringify(tokensMock.erc20List),
})); }), { times: 1 });
await page.route(API_URL_TOKENS_ERC721, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc721List),
}), { times: 1 });
await page.route(API_URL_TOKENS_ER1155, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 });
await page.evaluate(() => { await page.evaluate(() => {
window.ethereum = { } as MetaMaskInpageProvider; window.ethereum = { } as MetaMaskInpageProvider;
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { withName } from 'mocks/address/address'; import { withName } from 'mocks/address/address';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokensMock from 'mocks/address/tokens';
import { baseList } from 'mocks/address/tokenBalance';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -12,7 +11,6 @@ import AddressTokens from './AddressTokens'; ...@@ -12,7 +11,6 @@ import AddressTokens from './AddressTokens';
const ADDRESS_HASH = withName.hash; const ADDRESS_HASH = withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH });
const API_URL_ADDRESS_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { id: ADDRESS_HASH }); const API_URL_TOKENS = buildApiUrl('address_tokens', { id: ADDRESS_HASH });
const nextPageParams = { const nextPageParams = {
...@@ -22,7 +20,43 @@ const nextPageParams = { ...@@ -22,7 +20,43 @@ const nextPageParams = {
value: 1, value: 1,
}; };
test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { const test = base.extend({
page: async({ page }, use) => {
const response20 = {
items: [ tokensMock.erc20a, tokensMock.erc20b, tokensMock.erc20c, tokensMock.erc20d ],
next_page_params: nextPageParams,
};
const response721 = {
items: [ tokensMock.erc721a, tokensMock.erc721b, tokensMock.erc721c ],
next_page_params: nextPageParams,
};
const response1155 = {
items: [ tokensMock.erc1155a, tokensMock.erc1155b ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response721),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response1155),
}));
use(page);
},
});
test('erc20 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc20' }, query: { id: ADDRESS_HASH, tab: 'tokens_erc20' },
...@@ -30,24 +64,6 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -30,24 +64,6 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
}, },
}; };
const response20 = {
items: [ tokenBalanceMock.erc20a, tokenBalanceMock.erc20b, tokenBalanceMock.erc20c, tokenBalanceMock.erc20d ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
...@@ -59,7 +75,7 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -59,7 +75,7 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc721 +@mobile +@dark-mode', async({ mount, page }) => { test('erc721 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc721' }, query: { id: ADDRESS_HASH, tab: 'tokens_erc721' },
...@@ -67,24 +83,6 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -67,24 +83,6 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => {
}, },
}; };
const response20 = {
items: [ tokenBalanceMock.erc721a, tokenBalanceMock.erc721b, tokenBalanceMock.erc721c ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
...@@ -96,7 +94,7 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -96,7 +94,7 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc1155 +@mobile +@dark-mode', async({ mount, page }) => { test('erc1155 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc1155' }, query: { id: ADDRESS_HASH, tab: 'tokens_erc1155' },
...@@ -104,24 +102,6 @@ test('erc1155 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -104,24 +102,6 @@ test('erc1155 +@mobile +@dark-mode', async({ mount, page }) => {
}, },
}; };
const response20 = {
items: [ tokenBalanceMock.erc1155a, tokenBalanceMock.erc1155b ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
......
...@@ -3,7 +3,7 @@ import { test as base, expect, devices } from '@playwright/experimental-ct-react ...@@ -3,7 +3,7 @@ import { test as base, expect, devices } from '@playwright/experimental-ct-react
import React from 'react'; import React from 'react';
import * as coinBalanceMock from 'mocks/address/coinBalanceHistory'; import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokensMock from 'mocks/address/tokens';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -12,7 +12,9 @@ import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; ...@@ -12,7 +12,9 @@ import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
import TokenSelect from './TokenSelect'; import TokenSelect from './TokenSelect';
const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png'; const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png';
const TOKENS_API_URL = buildApiUrl('address_token_balances', { id: '1' }); const TOKENS_ERC20_API_URL = buildApiUrl('address_tokens', { id: '1' }) + '?type=ERC-20';
const TOKENS_ERC721_API_URL = buildApiUrl('address_tokens', { id: '1' }) + '?type=ERC-721';
const TOKENS_ER1155_API_URL = buildApiUrl('address_tokens', { id: '1' }) + '?type=ERC-1155';
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' }); const ADDRESS_API_URL = buildApiUrl('address', { id: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
...@@ -33,9 +35,17 @@ const test = base.extend({ ...@@ -33,9 +35,17 @@ const test = base.extend({
status: 200, status: 200,
body: JSON.stringify({ hash: '1' }), body: JSON.stringify({ hash: '1' }),
}), { times: 1 }); }), { times: 1 });
await page.route(TOKENS_API_URL, async(route) => route.fulfill({ await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(tokenBalanceMock.baseList), body: JSON.stringify(tokensMock.erc20List),
}), { times: 1 });
await page.route(TOKENS_ERC721_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc721List),
}), { times: 1 });
await page.route(TOKENS_ER1155_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 }); }), { times: 1 });
use(page); use(page);
...@@ -137,9 +147,17 @@ base('long values', async({ mount, page }) => { ...@@ -137,9 +147,17 @@ base('long values', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify({ hash: '1' }), body: JSON.stringify({ hash: '1' }),
}), { times: 1 }); }), { times: 1 });
await page.route(TOKENS_API_URL, async(route) => route.fulfill({ await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokensMock.erc20LongSymbol ] }),
}), { times: 1 });
await page.route(TOKENS_ERC721_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokensMock.erc721LongSymbol ] }),
}), { times: 1 });
await page.route(TOKENS_ER1155_API_URL, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(tokenBalanceMock.longValuesList), body: JSON.stringify({ items: [ tokensMock.erc1155LongId ] }),
}), { times: 1 }); }), { times: 1 });
await mount( await mount(
...@@ -175,13 +193,15 @@ test.describe('socket', () => { ...@@ -175,13 +193,15 @@ test.describe('socket', () => {
{ hooksConfig }, { hooksConfig },
); );
await page.route(TOKENS_API_URL, (route) => route.fulfill({ await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify([ body: JSON.stringify({
...tokenBalanceMock.baseList, items: [
tokenBalanceMock.erc20d, ...tokensMock.erc20List.items,
]), tokensMock.erc20d,
})); ],
}),
}), { times: 1 });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1'); const channel = await socketServer.joinChannel(socket, 'addresses:1');
...@@ -206,13 +226,15 @@ test.describe('socket', () => { ...@@ -206,13 +226,15 @@ test.describe('socket', () => {
{ hooksConfig }, { hooksConfig },
); );
await page.route(TOKENS_API_URL, (route) => route.fulfill({ await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify([ body: JSON.stringify({
...tokenBalanceMock.baseList, items: [
tokenBalanceMock.erc20d, ...tokensMock.erc20List.items,
]), tokensMock.erc20d,
})); ],
}),
}), { times: 1 });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1'); const channel = await socketServer.joinChannel(socket, 'addresses:1');
......
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQueryClient, useIsFetching } from '@tanstack/react-query'; import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import _sumBy from 'lodash/sumBy';
import NextLink from 'next/link'; import NextLink from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -8,12 +9,13 @@ import type { SocketMessage } from 'lib/socket/types'; ...@@ -8,12 +9,13 @@ import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import useFetchTokens from '../utils/useFetchTokens';
import TokenSelectDesktop from './TokenSelectDesktop'; import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile'; import TokenSelectMobile from './TokenSelectMobile';
...@@ -32,12 +34,9 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -32,12 +34,9 @@ const TokenSelect = ({ onClick }: Props) => {
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey); const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isLoading, refetch } = useApiQuery('address_token_balances', { const { data, isError, isLoading, refetch } = useFetchTokens({ hash: addressQueryData?.hash });
pathParams: { id: addressQueryData?.hash }, const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { id: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
queryOptions: { enabled: Boolean(addressQueryData) }, const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
});
const balancesResourceKey = getResourceKey('address_token_balances', { pathParams: { id: addressQueryData?.hash } });
const balancesIsFetching = useIsFetching({ queryKey: balancesResourceKey });
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => { const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) { if (payload.block_number !== blockNumber) {
...@@ -71,15 +70,16 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -71,15 +70,16 @@ const TokenSelect = ({ onClick }: Props) => {
return <Skeleton h={ 8 } w="160px"/>; return <Skeleton h={ 8 } w="160px"/>;
} }
if (isError || data.length === 0) { const hasTokens = _sumBy(Object.values(data), ({ items }) => items.length) > 0;
if (isError || !hasTokens) {
return <Box py="6px">0</Box>; return <Box py="6px">0</Box>;
} }
return ( return (
<Flex columnGap={ 3 } mt={{ base: '6px', lg: 0 }}> <Flex columnGap={ 3 } mt={{ base: '6px', lg: 0 }}>
{ isMobile ? { isMobile ?
<TokenSelectMobile data={ data } isLoading={ balancesIsFetching === 1 }/> : <TokenSelectMobile data={ data } isLoading={ tokensIsFetching === 1 }/> :
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/> <TokenSelectDesktop data={ data } isLoading={ tokensIsFetching === 1 }/>
} }
<Tooltip label="Show all tokens"> <Tooltip label="Show all tokens">
<Box> <Box>
......
import { Box, Button, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Button, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { FormattedData } from './types';
import arrowIcon from 'icons/arrows/east-mini.svg'; import arrowIcon from 'icons/arrows/east-mini.svg';
import tokensIcon from 'icons/tokens.svg'; import tokensIcon from 'icons/tokens.svg';
import type { EnhancedData } from '../utils/tokenUtils'; import { getTokensTotalInfo } from '../utils/tokenUtils';
import { getTokenBalanceTotal } from '../utils/tokenUtils';
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
isLoading: boolean; isLoading: boolean;
onClick: () => void; onClick: () => void;
data: Array<EnhancedData>; data: FormattedData;
} }
const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const totalBn = getTokenBalanceTotal(data); const { usd, num, isOverflow } = getTokensTotalInfo(data);
const skeletonBgColor = useColorModeValue('white', 'black'); const skeletonBgColor = useColorModeValue('white', 'black');
const prefix = isOverflow ? '>' : '';
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (isLoading && !isOpen) { if (isLoading && !isOpen) {
return; return;
...@@ -36,8 +39,8 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea ...@@ -36,8 +39,8 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea
aria-label="Token select" aria-label="Token select"
> >
<Icon as={ tokensIcon } boxSize={ 4 } mr={ 2 }/> <Icon as={ tokensIcon } boxSize={ 4 } mr={ 2 }/>
<Text fontWeight={ 600 }>{ data.length }</Text> <Text fontWeight={ 600 }>{ prefix }{ num }</Text>
<Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> (${ totalBn.toFormat(2) })</Text> <Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> ({ prefix }${ usd.toFormat(2) })</Text>
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 3 }/> <Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 3 }/>
</Button> </Button>
{ isLoading && !isOpen && <Skeleton h="100%" w="100%" position="absolute" top={ 0 } left={ 0 } bgColor={ skeletonBgColor }/> } { isLoading && !isOpen && <Skeleton h="100%" w="100%" position="absolute" top={ 0 } left={ 0 } bgColor={ skeletonBgColor }/> }
......
import { useColorModeValue, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import { useColorModeValue, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { FormattedData } from './types';
import TokenSelectButton from './TokenSelectButton'; import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu'; import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect'; import useTokenSelect from './useTokenSelect';
interface Props { interface Props {
data: Array<AddressTokenBalance>; data: FormattedData;
isLoading: boolean; isLoading: boolean;
} }
...@@ -22,7 +22,7 @@ const TokenSelectDesktop = ({ data, isLoading }: Props) => { ...@@ -22,7 +22,7 @@ const TokenSelectDesktop = ({ data, isLoading }: Props) => {
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger> <PopoverTrigger>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.modifiedData } isLoading={ isLoading }/> <TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.data } isLoading={ isLoading }/>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w="355px" maxH="450px" overflowY="scroll"> <PopoverContent w="355px" maxH="450px" overflowY="scroll">
<PopoverBody px={ 4 } py={ 6 } bgColor={ bgColor } boxShadow="2xl" > <PopoverBody px={ 4 } py={ 6 } bgColor={ bgColor } boxShadow="2xl" >
......
...@@ -7,10 +7,10 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol'; ...@@ -7,10 +7,10 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import type { EnhancedData } from '../utils/tokenUtils'; import type { TokenEnhancedData } from '../utils/tokenUtils';
interface Props { interface Props {
data: EnhancedData; data: TokenEnhancedData;
} }
const TokenSelectItem = ({ data }: Props) => { const TokenSelectItem = ({ data }: Props) => {
......
import { Icon, Text, Box, Input, InputGroup, InputLeftElement, useColorModeValue, Flex, Link } from '@chakra-ui/react'; import { Icon, Text, Box, Input, InputGroup, InputLeftElement, useColorModeValue, Flex, Link } from '@chakra-ui/react';
import type { Dictionary } from 'lodash'; import _sumBy from 'lodash/sumBy';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { FormattedData } from './types';
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import arrowIcon from 'icons/arrows/east.svg'; import arrowIcon from 'icons/arrows/east.svg';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
import type { Sort, EnhancedData } from '../utils/tokenUtils'; import type { Sort } from '../utils/tokenUtils';
import { sortTokenGroups, sortingFns } from '../utils/tokenUtils'; import { sortTokenGroups, sortingFns } from '../utils/tokenUtils';
import TokenSelectItem from './TokenSelectItem'; import TokenSelectItem from './TokenSelectItem';
...@@ -16,16 +17,17 @@ interface Props { ...@@ -16,16 +17,17 @@ interface Props {
searchTerm: string; searchTerm: string;
erc20sort: Sort; erc20sort: Sort;
erc1155sort: Sort; erc1155sort: Sort;
modifiedData: Array<EnhancedData>; filteredData: FormattedData;
groupedData: Dictionary<Array<EnhancedData>>;
onInputChange: (event: ChangeEvent<HTMLInputElement>) => void; onInputChange: (event: ChangeEvent<HTMLInputElement>) => void;
onSortClick: (event: React.SyntheticEvent) => void; onSortClick: (event: React.SyntheticEvent) => void;
} }
const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, onInputChange, onSortClick, searchTerm }: Props) => { const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200'); const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
const hasFilteredResult = _sumBy(Object.values(filteredData), ({ items }) => items.length) > 0;
return ( return (
<> <>
<InputGroup size="xs" mb={ 5 }> <InputGroup size="xs" mb={ 5 }>
...@@ -41,7 +43,12 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on ...@@ -41,7 +43,12 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on
/> />
</InputGroup> </InputGroup>
<Flex flexDir="column" rowGap={ 6 }> <Flex flexDir="column" rowGap={ 6 }>
{ Object.entries(groupedData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => { { Object.entries(filteredData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => {
if (tokenInfo.items.length === 0) {
return null;
}
const type = tokenType as TokenType; const type = tokenType as TokenType;
const arrowTransform = (type === 'ERC-1155' && erc1155sort === 'desc') || (type === 'ERC-20' && erc20sort === 'desc') ? const arrowTransform = (type === 'ERC-1155' && erc1155sort === 'desc') || (type === 'ERC-20' && erc20sort === 'desc') ?
'rotate(90deg)' : 'rotate(90deg)' :
...@@ -56,24 +63,26 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on ...@@ -56,24 +63,26 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on
return 'desc'; return 'desc';
} }
})(); })();
const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.some(({ usd }) => usd)); const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.items.some(({ usd }) => usd));
const numPrefix = tokenInfo.isOverflow ? '>' : '';
return ( return (
<Box key={ type }> <Box key={ type }>
<Flex justifyContent="space-between"> <Flex justifyContent="space-between">
<Text mb={ 3 } color="gray.500" fontWeight={ 600 } fontSize="sm">{ type } tokens ({ tokenInfo.length })</Text> <Text mb={ 3 } color="gray.500" fontWeight={ 600 } fontSize="sm">{ type } tokens ({ numPrefix }{ tokenInfo.items.length })</Text>
{ hasSort && ( { hasSort && (
<Link data-type={ type } onClick={ onSortClick } aria-label={ `Sort ${ type } tokens` }> <Link data-type={ type } onClick={ onSortClick } aria-label={ `Sort ${ type } tokens` }>
<Icon as={ arrowIcon } boxSize={ 5 } transform={ arrowTransform } transitionDuration="faster"/> <Icon as={ arrowIcon } boxSize={ 5 } transform={ arrowTransform } transitionDuration="faster"/>
</Link> </Link>
) } ) }
</Flex> </Flex>
{ tokenInfo.sort(sortingFns[type](sortDirection)).map((data) => <TokenSelectItem key={ data.token.address + data.token_id } data={ data }/>) } { tokenInfo.items.sort(sortingFns[type](sortDirection)).map((data) =>
<TokenSelectItem key={ data.token.address + data.token_id } data={ data }/>) }
</Box> </Box>
); );
}) } }) }
</Flex> </Flex>
{ modifiedData.length === 0 && searchTerm && <Text fontSize="sm">Could not find any matches.</Text> } { Boolean(searchTerm) && !hasFilteredResult && <Text fontSize="sm">Could not find any matches.</Text> }
</> </>
); );
}; };
......
import { useDisclosure, Modal, ModalContent, ModalCloseButton } from '@chakra-ui/react'; import { useDisclosure, Modal, ModalContent, ModalCloseButton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { FormattedData } from './types';
import TokenSelectButton from './TokenSelectButton'; import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu'; import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect'; import useTokenSelect from './useTokenSelect';
interface Props { interface Props {
data: Array<AddressTokenBalance>; data: FormattedData;
isLoading: boolean; isLoading: boolean;
} }
...@@ -18,7 +18,7 @@ const TokenSelectMobile = ({ data, isLoading }: Props) => { ...@@ -18,7 +18,7 @@ const TokenSelectMobile = ({ data, isLoading }: Props) => {
return ( return (
<> <>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.modifiedData } isLoading={ isLoading }/> <TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.data } isLoading={ isLoading }/>
<Modal isOpen={ isOpen } onClose={ onClose } size="full"> <Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent> <ModalContent>
<ModalCloseButton/> <ModalCloseButton/>
......
import type { TokenType } from 'types/api/token';
import type { TokenEnhancedData } from 'ui/address/utils/tokenUtils';
export type FormattedData = Record<TokenType, FormattedDataItem>;
export interface FormattedDataItem {
items: Array<TokenEnhancedData>;
isOverflow: boolean;
}
import _groupBy from 'lodash/groupBy'; import _mapValues from 'lodash/mapValues';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { FormattedData } from './types';
import type { Sort } from '../utils/tokenUtils'; import type { Sort } from '../utils/tokenUtils';
import { calculateUsdValue, filterTokens } from '../utils/tokenUtils'; import { filterTokens } from '../utils/tokenUtils';
export default function useData(data: Array<AddressTokenBalance>) { export default function useTokenSelect(data: FormattedData) {
const [ searchTerm, setSearchTerm ] = React.useState(''); const [ searchTerm, setSearchTerm ] = React.useState('');
const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc'); const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc');
const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc'); const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc');
...@@ -26,12 +26,12 @@ export default function useData(data: Array<AddressTokenBalance>) { ...@@ -26,12 +26,12 @@ export default function useData(data: Array<AddressTokenBalance>) {
} }
}, []); }, []);
const modifiedData = React.useMemo(() => { const filteredData = React.useMemo(() => {
return data.filter(filterTokens(searchTerm.toLowerCase())).map(calculateUsdValue); return _mapValues(data, ({ items, isOverflow }) => ({
isOverflow,
items: items.filter(filterTokens(searchTerm.toLowerCase())),
}));
}, [ data, searchTerm ]); }, [ data, searchTerm ]);
const groupedData = React.useMemo(() => {
return _groupBy(modifiedData, 'token.type');
}, [ modifiedData ]);
return { return {
searchTerm, searchTerm,
...@@ -39,7 +39,7 @@ export default function useData(data: Array<AddressTokenBalance>) { ...@@ -39,7 +39,7 @@ export default function useData(data: Array<AddressTokenBalance>) {
erc1155sort, erc1155sort,
onInputChange, onInputChange,
onSortClick, onSortClick,
modifiedData, data,
groupedData, filteredData,
}; };
} }
import { Flex, Skeleton } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { ZERO } from 'lib/consts';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import { getTokenBalanceTotal, calculateUsdValue } from '../utils/tokenUtils'; import { getTokensTotalInfo } from '../utils/tokenUtils';
import useFetchTokens from '../utils/useFetchTokens';
import TokenBalancesItem from './TokenBalancesItem'; import TokenBalancesItem from './TokenBalancesItem';
const TokenBalances = () => { const TokenBalances = () => {
const router = useRouter(); const router = useRouter();
const hash = router.query.id?.toString();
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() }, pathParams: { id: hash },
queryOptions: { enabled: Boolean(router.query.id) }, queryOptions: { enabled: Boolean(hash) },
}); });
const balancesQuery = useApiQuery('address_token_balances', { const tokenQuery = useFetchTokens({ hash });
pathParams: { id: addressQuery.data?.hash },
queryOptions: { enabled: Boolean(addressQuery.data) },
});
if (addressQuery.isError || balancesQuery.isError) { if (addressQuery.isError || tokenQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (addressQuery.isLoading || balancesQuery.isLoading) { if (addressQuery.isLoading || tokenQuery.isLoading) {
const item = <Skeleton w={{ base: '100%', lg: '240px' }} h="82px" borderRadius="16px"/>; const item = <Skeleton w={{ base: '100%', lg: '240px' }} h="82px" borderRadius="16px"/>;
return ( return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}> <Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
...@@ -40,7 +40,7 @@ const TokenBalances = () => { ...@@ -40,7 +40,7 @@ const TokenBalances = () => {
} }
const addressData = addressQuery.data; const addressData = addressQuery.data;
const { valueStr: nativeValue, usd: nativeUsd } = getCurrencyValue({ const { valueStr: nativeValue, usdBn: nativeUsd } = getCurrencyValue({
value: addressData.coin_balance || '0', value: addressData.coin_balance || '0',
accuracy: 8, accuracy: 8,
accuracyUsd: 2, accuracyUsd: 2,
...@@ -48,22 +48,25 @@ const TokenBalances = () => { ...@@ -48,22 +48,25 @@ const TokenBalances = () => {
decimals: String(appConfig.network.currency.decimals), decimals: String(appConfig.network.currency.decimals),
}); });
const tokenBalanceBn = getTokenBalanceTotal(balancesQuery.data.map(calculateUsdValue)).toFixed(2); const tokensInfo = getTokensTotalInfo(tokenQuery.data);
const prefix = tokensInfo.isOverflow ? '>' : '';
const totalUsd = nativeUsd ? BigNumber(nativeUsd).toNumber() + BigNumber(tokenBalanceBn).toNumber() : undefined; const totalUsd = nativeUsd.plus(tokensInfo.usd);
const tokensNumText = tokensInfo.num > 0 ?
` | ${ prefix }${ tokensInfo.num } ${ tokensInfo.num > 1 ? 'tokens' : 'token' }` :
'';
return ( return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}> <Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
<TokenBalancesItem name="Net Worth" value={ totalUsd ? `$${ totalUsd } USD` : 'N/A' }/> <TokenBalancesItem name="Net Worth" value={ addressData.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) } USD` : 'N/A' }/>
<TokenBalancesItem <TokenBalancesItem
name={ `${ appConfig.network.currency.symbol } Balance` } name={ `${ appConfig.network.currency.symbol } Balance` }
value={ (nativeUsd ? `$${ nativeUsd } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` } value={ (!nativeUsd.eq(ZERO) ? `$${ nativeUsd.toFormat(2) } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` }
/> />
<TokenBalancesItem <TokenBalancesItem
name="Tokens" name="Tokens"
value={ value={
`$${ tokenBalanceBn } USD ` + `${ prefix }$${ tokensInfo.usd.toFormat(2) } USD ` +
(balancesQuery.data.length ? ` | ${ balancesQuery.data.length } ${ balancesQuery.data.length === 1 ? 'token' : 'tokens' }` : '') tokensNumText
} }
/> />
</Flex> </Flex>
......
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import fpAdd from 'lodash/fp/add';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import sumBnReducer from 'lib/bigint/sumBnReducer';
import { ZERO } from 'lib/consts'; import { ZERO } from 'lib/consts';
export type EnhancedData = AddressTokenBalance & { export type TokenEnhancedData = AddressTokenBalance & {
usd?: BigNumber ; usd?: BigNumber ;
} }
export type Sort = 'desc' | 'asc'; export type Sort = 'desc' | 'asc';
export type TokenSelectData = Record<TokenType, TokenSelectDataItem>;
export interface TokenSelectDataItem {
items: Array<TokenEnhancedData>;
isOverflow: boolean;
}
type TokenGroup = [string, TokenSelectDataItem];
const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155' ]; const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155' ];
type TokenGroup = [string, Array<AddressTokenBalance>];
export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => { export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => {
return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1; return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1;
...@@ -27,7 +38,7 @@ const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: Ad ...@@ -27,7 +38,7 @@ const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: Ad
return Number(dataA.value) > Number(dataB.value) ? 1 : -1; return Number(dataA.value) > Number(dataB.value) ? 1 : -1;
}; };
const sortErc20Tokens = (sort: Sort) => (dataA: EnhancedData, dataB: EnhancedData) => { const sortErc20Tokens = (sort: Sort) => (dataA: TokenEnhancedData, dataB: TokenEnhancedData) => {
if (!dataA.usd && !dataB.usd) { if (!dataA.usd && !dataB.usd) {
return 0; return 0;
} }
...@@ -63,7 +74,7 @@ export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBala ...@@ -63,7 +74,7 @@ export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBala
return token.name?.toLowerCase().includes(searchTerm); return token.name?.toLowerCase().includes(searchTerm);
}; };
export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => { export const calculateUsdValue = (data: AddressTokenBalance): TokenEnhancedData => {
if (data.token.type !== 'ERC-20') { if (data.token.type !== 'ERC-20') {
return data; return data;
} }
...@@ -80,6 +91,18 @@ export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => { ...@@ -80,6 +91,18 @@ export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => {
}; };
}; };
export const getTokenBalanceTotal = (data: Array<EnhancedData>) => { export const getTokensTotalInfo = (data: TokenSelectData) => {
return data.reduce((result, item) => !item.usd ? result : result.plus(BigNumber(item.usd)), ZERO); const usd = Object.values(data)
.map(({ items }) => items.reduce(usdValueReducer, ZERO))
.reduce(sumBnReducer, ZERO);
const num = Object.values(data)
.map(({ items }) => items.length)
.reduce(fpAdd, 0);
const isOverflow = Object.values(data).some(({ isOverflow }) => isOverflow);
return { usd, num, isOverflow };
}; };
const usdValueReducer = (result: BigNumber, item: TokenEnhancedData) => !item.usd ? result : result.plus(BigNumber(item.usd));
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { calculateUsdValue } from './tokenUtils';
interface Props {
hash?: string;
}
export default function useFetchTokens({ hash }: Props) {
const erc20query = useApiQuery('address_tokens', {
pathParams: { id: hash },
queryParams: { type: 'ERC-20' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const erc721query = useApiQuery('address_tokens', {
pathParams: { id: hash },
queryParams: { type: 'ERC-721' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const erc1155query = useApiQuery('address_tokens', {
pathParams: { id: hash },
queryParams: { type: 'ERC-1155' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const refetch = React.useCallback(() => {
erc20query.refetch();
erc721query.refetch();
erc1155query.refetch();
}, [ erc1155query, erc20query, erc721query ]);
const data = React.useMemo(() => {
return {
'ERC-20': {
items: erc20query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc20query.data?.next_page_params),
},
'ERC-721': {
items: erc721query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc721query.data?.next_page_params),
},
'ERC-1155': {
items: erc1155query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc1155query.data?.next_page_params),
},
};
}, [ erc1155query.data, erc20query.data, erc721query.data ]);
return {
isLoading: erc20query.isLoading || erc721query.isLoading || erc1155query.isLoading,
isError: erc20query.isError || erc721query.isError || erc1155query.isError,
data,
refetch,
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment