Commit 50a96d5d authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #1735 from blockscout/fe-1725

Add support of ERC-404 type
parents b3d0a432 dc346019
......@@ -22,6 +22,7 @@ SocketMessage.AddressTokenBalance |
SocketMessage.AddressTokenBalancesErc20 |
SocketMessage.AddressTokenBalancesErc721 |
SocketMessage.AddressTokenBalancesErc1155 |
SocketMessage.AddressTokenBalancesErc404 |
SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending |
......@@ -57,6 +58,7 @@ export namespace SocketMessage {
export type AddressTokenBalancesErc20 = SocketMessageParamsGeneric<'updated_token_balances_erc_20', AddressTokensBalancesSocketMessage>;
export type AddressTokenBalancesErc721 = SocketMessageParamsGeneric<'updated_token_balances_erc_721', AddressTokensBalancesSocketMessage>;
export type AddressTokenBalancesErc1155 = SocketMessageParamsGeneric<'updated_token_balances_erc_1155', AddressTokensBalancesSocketMessage>;
export type AddressTokenBalancesErc404 = SocketMessageParamsGeneric<'updated_token_balances_erc_404', AddressTokensBalancesSocketMessage>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>;
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transactions: Array<Transaction> }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transactions: Array<Transaction> }>;
......
......@@ -3,6 +3,7 @@ import type { NFTTokenType, TokenType } from 'types/api/token';
export const NFT_TOKEN_TYPES: Array<{ title: string; id: NFTTokenType }> = [
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
{ title: 'ERC-404', id: 'ERC-404' },
];
export const TOKEN_TYPES: Array<{ title: string; id: TokenType }> = [
......
......@@ -105,6 +105,20 @@ export const erc1155LongId: AddressTokenBalance = {
value: '42',
};
export const erc404a: AddressTokenBalance = {
token: tokens.tokenInfoERC404,
token_id: '42',
token_instance: tokenInstance.base,
value: '240000000000000',
};
export const erc404b: AddressTokenBalance = {
token: tokens.tokenInfoERC404,
token_instance: null,
value: '11',
token_id: null,
};
export const erc20List = {
items: [
erc20a,
......@@ -129,6 +143,13 @@ export const erc1155List = {
],
};
export const erc404List = {
items: [
erc404a,
erc404b,
],
};
export const nfts: AddressNFTsResponse = {
items: [
{
......@@ -143,6 +164,12 @@ export const nfts: AddressNFTsResponse = {
token_type: 'ERC-721',
value: '1',
},
{
...tokenInstance.unique,
token: tokens.tokenInfoERC404,
token_type: 'ERC-404',
value: '11000',
},
],
next_page_params: null,
};
......
......@@ -28,7 +28,7 @@ export const tokenInfoERC20a: TokenInfo<'ERC-20'> = {
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
icon_url: 'https://example.com/token-icon.png',
icon_url: 'http://localhost:3000/token-icon.png',
};
export const tokenInfoERC20b: TokenInfo<'ERC-20'> = {
......@@ -174,6 +174,19 @@ export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = {
icon_url: null,
};
export const tokenInfoERC404: TokenInfo<'ERC-404'> = {
address: '0xB5C457dDB4cE3312a6C5a2b056a1652bd542a208',
circulating_market_cap: '0.0',
decimals: '18',
exchange_rate: '1484.13',
holders: '81',
icon_url: null,
name: 'OMNI404',
symbol: 'O404',
total_supply: '6482275000000000000',
type: 'ERC-404',
};
export const bridgedTokenA: TokenInfo<'ERC-20'> = {
...tokenInfoERC20a,
is_bridged: true,
......
......@@ -170,6 +170,63 @@ export const erc1155D: TokenTransfer = {
total: { token_id: '456', value: '42', decimals: null },
};
export const erc404A: TokenTransfer = {
from: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
to: {
hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
},
token: {
address: '0xF56b7693E4212C584de4a83117f805B8E89224CB',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '1',
name: null,
symbol: 'MY_SYMBOL_IS_VERY_LONG',
type: 'ERC-404',
total_supply: '0',
icon_url: null,
},
total: {
value: '42000000000000000000000000',
decimals: '18',
},
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_transfer',
method: 'swap',
timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1',
log_index: '1',
};
export const erc404B: TokenTransfer = {
...erc404A,
token: {
...erc404A.token,
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
total: { token_id: '4625304364899952' },
};
export const mixTokens: TokenTransferResponse = {
items: [
erc20,
......@@ -178,6 +235,8 @@ export const mixTokens: TokenTransferResponse = {
erc1155B,
erc1155C,
erc1155D,
erc404A,
erc404B,
],
next_page_params: null,
};
......@@ -127,6 +127,8 @@ export const withTokenTransfer: Transaction = {
tokenTransferMock.erc1155B,
tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D,
tokenTransferMock.erc404A,
tokenTransferMock.erc404B,
],
token_transfers_overflow: true,
tx_types: [
......
......@@ -62,6 +62,7 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_bal
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_20', payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_721', payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_1155', payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_404', payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transactions: Array<Transaction> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
......
......@@ -10,7 +10,7 @@ import type {
import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams';
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INSTANCE } from './token';
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token';
import { TX_HASH } from './tx';
export const ADDRESS_INFO: Address = {
......@@ -104,6 +104,13 @@ export const ADDRESS_NFT_1155: AddressNFT = {
...TOKEN_INSTANCE,
};
export const ADDRESS_NFT_404: AddressNFT = {
token_type: 'ERC-404',
token: TOKEN_INFO_ERC_404,
value: '10',
...TOKEN_INSTANCE,
};
export const ADDRESS_COLLECTION: AddressCollection = {
token: TOKEN_INFO_ERC_1155,
amount: '4',
......
import type { TokenCounters, TokenHolder, TokenInfo, TokenInstance, TokenType } from 'types/api/token';
import type {
TokenCounters,
TokenHolder,
TokenHolders,
TokenHoldersPagination,
TokenInfo,
TokenInstance,
TokenType,
} from 'types/api/token';
import type { TokenInstanceTransferPagination, TokenInstanceTransferResponse } from 'types/api/tokens';
import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
......@@ -31,6 +40,12 @@ export const TOKEN_INFO_ERC_1155: TokenInfo<'ERC-1155'> = {
type: 'ERC-1155',
};
export const TOKEN_INFO_ERC_404: TokenInfo<'ERC-404'> = {
...TOKEN_INFO_ERC_20,
circulating_market_cap: null,
type: 'ERC-404',
};
export const TOKEN_COUNTERS: TokenCounters = {
token_holders_count: '123456',
transfers_count: '123456',
......@@ -47,6 +62,32 @@ export const TOKEN_HOLDER_ERC_1155: TokenHolder = {
value: '1021378038331138520',
};
export const getTokenHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => {
switch (type) {
case 'ERC-721':
return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination });
case 'ERC-1155':
return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination });
case 'ERC-404':
return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination });
default:
return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination });
}
};
export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => {
switch (type) {
case 'ERC-721':
return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination });
case 'ERC-1155':
return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination });
case 'ERC-404':
return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination });
default:
return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination });
}
};
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH,
from: ADDRESS_PARAMS,
......@@ -81,17 +122,42 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = {
token: TOKEN_INFO_ERC_1155,
};
export const TOKEN_TRANSFER_ERC_404: TokenTransfer = {
...TOKEN_TRANSFER_ERC_20,
total: {
token_id: '35870',
value: '123',
decimals: '18',
},
token: TOKEN_INFO_ERC_404,
};
export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => {
switch (type) {
case 'ERC-721':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, { next_page_params: pagination });
case 'ERC-1155':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, { next_page_params: pagination });
case 'ERC-404':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_404, 50, { next_page_params: pagination });
default:
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, { next_page_params: pagination });
}
};
export const getTokenInstanceTransfersStub = (type?: TokenType, pagination: TokenInstanceTransferPagination | null = null): TokenInstanceTransferResponse => {
switch (type) {
case 'ERC-721':
return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_721, 10, { next_page_params: pagination });
case 'ERC-1155':
return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_1155, 10, { next_page_params: pagination });
case 'ERC-404':
return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_404, 10, { next_page_params: pagination });
default:
return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_20, 10, { next_page_params: pagination });
}
};
export const TOKEN_INSTANCE: TokenInstance = {
animation_url: null,
external_app_url: 'https://vipsland.com/nft/collections/genesis/188882',
......
import type { TokenInfoApplication } from './account';
import type { AddressParam } from './addressParams';
export type NFTTokenType = 'ERC-721' | 'ERC-1155';
export type NFTTokenType = 'ERC-721' | 'ERC-1155' | 'ERC-404';
export type TokenType = 'ERC-20' | NFTTokenType;
export interface TokenInfo<T extends TokenType = TokenType> {
......
......@@ -16,6 +16,13 @@ export type Erc1155TotalPayload = {
token_id: string | null;
}
export type Erc404TotalPayload = {
decimals: string | null;
value: string | null;
} | {
token_id: string | null;
};
export type TokenTransfer = (
{
token: TokenInfo<'ERC-20'>;
......@@ -28,6 +35,10 @@ export type TokenTransfer = (
{
token: TokenInfo<'ERC-1155'>;
total: Erc1155TotalPayload;
} |
{
token: TokenInfo<'ERC-404'>;
total: Erc404TotalPayload;
}
) & TokenTransferBase
......
......@@ -41,6 +41,13 @@ export interface TxStateChangeTokenErc1155 {
token_id: string;
}
export interface TxStateChangeTokenErc404 {
type: 'token';
token: TokenInfo<'ERC-404'>;
change: string;
token_id: string;
}
export type TxStateChanges = {
items: Array<TxStateChange>;
next_page_params: {
......
......@@ -20,6 +20,7 @@ const API_URL_COUNTERS = buildApiUrl('address_counters', { hash: ADDRESS_HASH })
const API_URL_TOKENS_ERC20 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-20';
const API_URL_TOKENS_ERC721 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-721';
const API_URL_TOKENS_ER1155 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-1155';
const API_URL_TOKENS_ERC404 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-404';
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH },
......@@ -70,6 +71,10 @@ test('token', async({ mount, page }) => {
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 });
await page.route(API_URL_TOKENS_ERC404, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc404List),
}), { times: 1 });
await page.evaluate(() => {
window.ethereum = {
......
......@@ -37,6 +37,10 @@ const test = base.extend({
items: [ tokensMock.erc1155a, tokensMock.erc1155b ],
next_page_params: nextPageParams,
};
const response404 = {
items: [ tokensMock.erc404a, tokensMock.erc404b ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
......@@ -54,6 +58,10 @@ const test = base.extend({
status: 200,
body: JSON.stringify(response1155),
}));
await page.route(API_URL_TOKENS + '?type=ERC-404', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response404),
}));
await page.route(API_URL_NFT, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.nfts),
......@@ -217,6 +225,10 @@ base.describe('update balances via socket', () => {
items: [ tokensMock.erc1155a ],
next_page_params: null,
};
const response404 = {
items: [ tokensMock.erc404a ],
next_page_params: null,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
......@@ -234,6 +246,10 @@ base.describe('update balances via socket', () => {
status: 200,
body: JSON.stringify(response1155),
}));
await page.route(API_URL_TOKENS + '?type=ERC-404', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response404),
}));
const component = await mount(
<TestApp withSocket>
......@@ -248,6 +264,7 @@ base.describe('update balances via socket', () => {
await page.waitForResponse(API_URL_TOKENS + '?type=ERC-20');
await page.waitForResponse(API_URL_TOKENS + '?type=ERC-721');
await page.waitForResponse(API_URL_TOKENS + '?type=ERC-1155');
await page.waitForResponse(API_URL_TOKENS + '?type=ERC-404');
await expect(component).toHaveScreenshot();
......
......@@ -14,6 +14,7 @@ const ASSET_URL = tokenInfoERC20a.icon_url as string;
const TOKENS_ERC20_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-20';
const TOKENS_ERC721_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-721';
const TOKENS_ER1155_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-1155';
const TOKENS_ER404_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-404';
const ADDRESS_API_URL = buildApiUrl('address', { hash: '1' });
const hooksConfig = {
router: {
......@@ -46,6 +47,10 @@ const test = base.extend({
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 });
await page.route(TOKENS_ER404_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc404List),
}), { times: 1 });
use(page);
},
......@@ -158,6 +163,10 @@ base('long values', async({ mount, page }) => {
status: 200,
body: JSON.stringify({ items: [ tokensMock.erc1155LongId ] }),
}), { times: 1 });
await page.route(TOKENS_ER404_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc404List),
}), { times: 1 });
await mount(
<TestApp>
......
......@@ -4,6 +4,7 @@ import React from 'react';
import { route } from 'nextjs-routes';
import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import LinkInternal from 'ui/shared/LinkInternal';
import TruncatedValue from 'ui/shared/TruncatedValue';
......@@ -45,6 +46,25 @@ const TokenSelectItem = ({ data }: Props) => {
</>
);
}
case 'ERC-404': {
return (
<>
{ data.token_id !== null && (
<chakra.span textOverflow="ellipsis" overflow="hidden" mr={ 6 }>
#{ data.token_id || 0 }
</chakra.span>
) }
{ data.value !== null && (
<span>
{ data.token.decimals ?
getCurrencyValue({ value: data.value, decimals: data.token.decimals, accuracy: 2 }).valueStr :
BigNumber(data.value).toFormat()
}
</span>
) }
</>
);
}
}
})();
......
......@@ -16,12 +16,13 @@ interface Props {
searchTerm: string;
erc20sort: Sort;
erc1155sort: Sort;
erc404sort: Sort;
filteredData: FormattedData;
onInputChange: (event: ChangeEvent<HTMLInputElement>) => void;
onSortClick: (event: React.SyntheticEvent) => void;
}
const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
......@@ -43,13 +44,15 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange,
</InputGroup>
<Flex flexDir="column" rowGap={ 6 }>
{ Object.entries(filteredData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => {
if (tokenInfo.items.length === 0) {
return null;
}
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-404' && erc404sort === 'desc') ||
(type === 'ERC-20' && erc20sort === 'desc') ?
'rotate(90deg)' :
'rotate(-90deg)';
const sortDirection: Sort = (() => {
......@@ -62,7 +65,10 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange,
return 'desc';
}
})();
const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.items.some(({ usd }) => usd));
const hasSort =
(type === 'ERC-404' && tokenInfo.items.some(item => item.value)) ||
type === 'ERC-1155' ||
(type === 'ERC-20' && tokenInfo.items.some(({ usd }) => usd));
const numPrefix = tokenInfo.isOverflow ? '>' : '';
return (
......
......@@ -10,6 +10,7 @@ import { filterTokens } from '../utils/tokenUtils';
export default function useTokenSelect(data: FormattedData) {
const [ searchTerm, setSearchTerm ] = React.useState('');
const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc');
const [ erc404sort, setErc404Sort ] = React.useState<Sort>('desc');
const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc');
const onInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
......@@ -21,6 +22,9 @@ export default function useTokenSelect(data: FormattedData) {
if (tokenType === 'ERC-1155') {
setErc1155Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
}
if (tokenType === 'ERC-404') {
setErc404Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
}
if (tokenType === 'ERC-20') {
setErc20Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
}
......@@ -37,6 +41,7 @@ export default function useTokenSelect(data: FormattedData) {
searchTerm,
erc20sort,
erc1155sort,
erc404sort,
onInputChange,
onSortClick,
data,
......
......@@ -5,6 +5,7 @@ import type { AddressNFT } from 'types/api/address';
import { route } from 'nextjs-routes';
import getCurrencyValue from 'lib/getCurrencyValue';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import NftMedia from 'ui/shared/nft/NftMedia';
......@@ -14,6 +15,7 @@ import NFTItemContainer from './NFTItemContainer';
type Props = AddressNFT & { isLoading: boolean; withTokenLink?: boolean };
const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: Props) => {
const valueResult = token.decimals && value ? getCurrencyValue({ value, decimals: token.decimals, accuracy: 2 }).valueStr : value;
const tokenInstanceLink = tokenInstance.id ?
route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenInstance.id } }) :
undefined;
......@@ -31,13 +33,18 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P
isLoading={ isLoading }
/>
</Link>
<Flex justifyContent="space-between" w="100%">
<Flex justifyContent="space-between" w="100%" flexWrap="wrap">
<Flex ml={ 1 } overflow="hidden">
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<NftEntity hash={ token.address } id={ tokenInstance.id } isLoading={ isLoading } noIcon/>
</Flex>
<Skeleton isLoaded={ !isLoading }>
{ Number(value) > 1 && <Flex><Text variant="secondary" whiteSpace="pre">Qty </Text>{ value }</Flex> }
<Skeleton isLoaded={ !isLoading } overflow="hidden" ml={ 1 }>
{ valueResult && (
<Flex>
<Text variant="secondary" whiteSpace="pre">Qty </Text>
<Text overflow="hidden" wordBreak="break-all">{ valueResult }</Text>
</Flex>
) }
</Skeleton>
</Flex>
{ withTokenLink && (
......
......@@ -22,13 +22,13 @@ export interface TokenSelectDataItem {
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', 'ERC-404' ];
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;
};
const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: AddressTokenBalance) => {
const sortErc1155or404Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: AddressTokenBalance) => {
if (dataA.value === dataB.value) {
return 0;
}
......@@ -38,6 +38,7 @@ const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: Ad
return Number(dataA.value) > Number(dataB.value) ? 1 : -1;
};
const sortErc20Tokens = (sort: Sort) => (dataA: TokenEnhancedData, dataB: TokenEnhancedData) => {
if (!dataA.usd && !dataB.usd) {
return 0;
......@@ -63,7 +64,8 @@ const sortErc721Tokens = () => () => 0;
export const sortingFns = {
'ERC-20': sortErc20Tokens,
'ERC-721': sortErc721Tokens,
'ERC-1155': sortErc1155Tokens,
'ERC-1155': sortErc1155or404Tokens,
'ERC-404': sortErc1155or404Tokens,
};
export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBalance) => {
......
......@@ -36,6 +36,11 @@ export default function useFetchTokens({ hash }: Props) {
queryParams: { type: 'ERC-1155' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const erc404query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-404' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const queryClient = useQueryClient();
......@@ -78,6 +83,10 @@ export default function useFetchTokens({ hash }: Props) {
updateTokensData('ERC-1155', payload);
}, [ updateTokensData ]);
const handleTokenBalancesErc404Message: SocketMessage.AddressTokenBalancesErc1155['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-404', payload);
}, [ updateTokensData ]);
const channel = useSocketChannel({
topic: `addresses:${ hash?.toLowerCase() }`,
isDisabled: Boolean(hash) && (erc20query.isPlaceholderData || erc721query.isPlaceholderData || erc1155query.isPlaceholderData),
......@@ -98,6 +107,11 @@ export default function useFetchTokens({ hash }: Props) {
event: 'updated_token_balances_erc_1155',
handler: handleTokenBalancesErc1155Message,
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_404',
handler: handleTokenBalancesErc404Message,
});
const data = React.useMemo(() => {
return {
......@@ -113,12 +127,16 @@ export default function useFetchTokens({ hash }: Props) {
items: erc1155query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc1155query.data?.next_page_params),
},
'ERC-404': {
items: erc404query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc1155query.data?.next_page_params),
},
};
}, [ erc1155query.data, erc20query.data, erc721query.data ]);
}, [ erc1155query.data, erc20query.data, erc721query.data, erc404query.data ]);
return {
isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending,
isError: erc20query.isError || erc721query.isError || erc1155query.isError,
isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending || erc404query.isPending,
isError: erc20query.isError || erc721query.isError || erc1155query.isError || erc404query.isError,
data,
};
}
......@@ -17,8 +17,10 @@ import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import * as addressStubs from 'stubs/address';
import * as tokenStubs from 'stubs/token';
import { getTokenHoldersStub } from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import AddressContract from 'ui/address/AddressContract';
import AddressQrCode from 'ui/address/details/AddressQrCode';
......@@ -118,7 +120,7 @@ const TokenPageContent = () => {
}, [ tokenQuery.data, tokenQuery.isPlaceholderData ]);
const hasData = (tokenQuery.data && !tokenQuery.isPlaceholderData) && (contractQuery.data && !contractQuery.isPlaceholderData);
const hasInventoryTab = tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721';
const hasInventoryTab = tokenQuery.data?.type && NFT_TOKEN_TYPE_IDS.includes(tokenQuery.data.type);
const transfersQuery = useQueryWithPages({
resourceName: 'token_transfers',
......@@ -161,8 +163,7 @@ const TokenPageContent = () => {
scrollRef,
options: {
enabled: Boolean(hashString && tab === 'holders' && hasData),
placeholderData: generateListStub<'token_holders'>(
tokenQuery.data?.type === 'ERC-1155' ? tokenStubs.TOKEN_HOLDER_ERC_1155 : tokenStubs.TOKEN_HOLDER_ERC_20, 50, { next_page_params: null }),
placeholderData: getTokenHoldersStub(tokenQuery.data?.type, null),
},
});
......@@ -174,7 +175,7 @@ const TokenPageContent = () => {
const contractTabs = useContractTabs(contractQuery.data);
const tabs: Array<RoutedTab> = [
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ? {
hasInventoryTab ? {
id: 'inventory',
title: 'Inventory',
component: <TokenInventory inventoryQuery={ inventoryQuery } tokenQuery={ tokenQuery } ownerFilter={ ownerFilter }/>,
......@@ -212,7 +213,7 @@ const TokenPageContent = () => {
}
// default tab for nfts is token inventory
if (((tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') && !tab) || tab === 'inventory') {
if ((hasInventoryTab && !tab) || tab === 'inventory') {
pagination = inventoryQuery.pagination;
}
......
......@@ -11,9 +11,12 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as metadata from 'lib/metadata';
import * as regexp from 'lib/regexp';
import { TOKEN_INSTANCE, TOKEN_INFO_ERC_1155 } from 'stubs/token';
import * as tokenStubs from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import {
TOKEN_INSTANCE,
TOKEN_INFO_ERC_1155,
getTokenInstanceTransfersStub,
getTokenInstanceHoldersStub,
} from 'stubs/token';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd';
......@@ -66,11 +69,7 @@ const TokenInstanceContent = () => {
scrollRef,
options: {
enabled: Boolean(hash && id && (!tab || tab === 'token_transfers') && !tokenInstanceQuery.isPlaceholderData && tokenInstanceQuery.data),
placeholderData: generateListStub<'token_instance_transfers'>(
tokenQuery.data?.type === 'ERC-1155' ? tokenStubs.TOKEN_TRANSFER_ERC_1155 : tokenStubs.TOKEN_TRANSFER_ERC_721,
10,
{ next_page_params: null },
),
placeholderData: getTokenInstanceTransfersStub(tokenQuery.data?.type, null),
},
});
......@@ -86,8 +85,7 @@ const TokenInstanceContent = () => {
scrollRef,
options: {
enabled: Boolean(hash && tab === 'holders' && shouldFetchHolders),
placeholderData: generateListStub<'token_instance_holders'>(
tokenQuery.data?.type === 'ERC-1155' ? tokenStubs.TOKEN_HOLDER_ERC_1155 : tokenStubs.TOKEN_HOLDER_ERC_20, 10, { next_page_params: null }),
placeholderData: getTokenInstanceHoldersStub(tokenQuery.data?.type, null),
},
});
......
......@@ -35,7 +35,7 @@ const TokenTransferListItem = ({
isLoading,
}: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
const { usd, valueStr } = 'value' in total ? getCurrencyValue({
const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({
value: total.value,
exchangeRate: token.exchange_rate,
accuracy: 8,
......
......@@ -34,7 +34,7 @@ const TokenTransferTableItem = ({
isLoading,
}: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
const { usd, valueStr } = 'value' in total ? getCurrencyValue({
const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({
value: total.value,
exchangeRate: token.exchange_rate,
accuracy: 8,
......
......@@ -77,7 +77,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
let totalSupplyValue;
if (type === 'ERC-20') {
if (decimals) {
const totalValue = totalSupply ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
totalSupplyValue = totalValue?.valueStr;
} else {
......
......@@ -29,7 +29,7 @@ const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => {
/>
</ListItemMobileGrid.Value>
{ token.type === 'ERC-1155' && 'token_id' in holder && (
{ (token.type === 'ERC-1155' || token.type === 'ERC-404') && 'token_id' in holder && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>ID#</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
......@@ -47,7 +47,7 @@ const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => {
</Skeleton>
</ListItemMobileGrid.Value>
{ token.total_supply && (
{ token.total_supply && token.type !== 'ERC-404' && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Percentage</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
......
......@@ -19,9 +19,9 @@ const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
<Thead top={ top }>
<Tr>
<Th>Holder</Th>
{ token.type === 'ERC-1155' && <Th>ID#</Th> }
{ (token.type === 'ERC-1155' || token.type === 'ERC-404') && <Th>ID#</Th> }
<Th isNumeric>Quantity</Th>
{ token.total_supply && <Th isNumeric width="175px">Percentage</Th> }
{ token.total_supply && token.type !== 'ERC-404' && <Th isNumeric width="175px">Percentage</Th> }
</Tr>
</Thead>
<Tbody>
......
......@@ -26,7 +26,7 @@ const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => {
fontWeight="700"
/>
</Td>
{ token.type === 'ERC-1155' && 'token_id' in holder && (
{ (token.type === 'ERC-1155' || token.type === 'ERC-404') && 'token_id' in holder && (
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ 'token_id' in holder && holder.token_id }
......@@ -38,7 +38,7 @@ const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => {
{ quantity }
</Skeleton>
</Td>
{ token.total_supply && (
{ token.total_supply && token.type !== 'ERC-404' && (
<Td verticalAlign="middle" isNumeric>
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
......
......@@ -5,6 +5,7 @@ import type { TokenTransfer } from 'types/api/tokenTransfer';
import getCurrencyValue from 'lib/getCurrencyValue';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
......@@ -26,7 +27,7 @@ const TokenTransferListItem = ({
isLoading,
}: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, true);
const { usd, valueStr } = 'value' in total ? getCurrencyValue({
const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({
value: total.value,
exchangeRate: token.exchange_rate,
accuracy: 8,
......@@ -87,7 +88,7 @@ const TokenTransferListItem = ({
) }
</Grid>
) }
{ 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && total.token_id !== null && (
{ 'token_id' in total && (NFT_TOKEN_TYPE_IDS.includes(token.type)) && total.token_id !== null && (
<NftEntity
hash={ token.address }
id={ total.token_id }
......
......@@ -5,6 +5,7 @@ import type { TokenInfo } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky';
import TruncatedValue from 'ui/shared/TruncatedValue';
......@@ -31,12 +32,12 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket
<Tr>
<Th width="280px">Txn hash</Th>
<Th width="200px">Method</Th>
<Th width={{ lg: '224px', xl: '420px' }}>From/To</Th>
{ (tokenType === 'ERC-721' || tokenType === 'ERC-1155') &&
<Th width={ tokenType === 'ERC-1155' ? '50%' : '100%' }>Token ID</Th>
<Th width={{ lg: '224px', xl: '380px' }}>From/To</Th>
{ (NFT_TOKEN_TYPE_IDS.includes(tokenType)) &&
<Th width={ tokenType === 'ERC-1155' || tokenType === 'ERC-404' ? '50%' : '100%' }>Token ID</Th>
}
{ (tokenType === 'ERC-20' || tokenType === 'ERC-1155') && (
<Th width={ tokenType === 'ERC-1155' ? '50%' : '100%' } isNumeric>
{ (tokenType === 'ERC-20' || tokenType === 'ERC-1155' || tokenType === 'ERC-404') && (
<Th width={ tokenType === 'ERC-20' ? '100%' : '50%' } isNumeric>
<TruncatedValue value={ `Value ${ token?.symbol || '' }` } w="100%" verticalAlign="middle"/>
</Th>
) }
......
......@@ -5,6 +5,7 @@ import type { TokenTransfer } from 'types/api/tokenTransfer';
import getCurrencyValue from 'lib/getCurrencyValue';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
......@@ -24,7 +25,7 @@ const TokenTransferTableItem = ({
isLoading,
}: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, true);
const { usd, valueStr } = 'value' in total ? getCurrencyValue({
const { usd, valueStr } = 'value' in total && total.value !== null ? getCurrencyValue({
value: total.value,
exchangeRate: token.exchange_rate,
accuracy: 8,
......@@ -69,7 +70,7 @@ const TokenTransferTableItem = ({
tokenHash={ token.address }
/>
</Td>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
{ (NFT_TOKEN_TYPE_IDS.includes(token.type)) && (
<Td>
{ 'token_id' in total && total.token_id !== null ? (
<NftEntity
......@@ -82,7 +83,7 @@ const TokenTransferTableItem = ({
}
</Td>
) }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
{ (token.type === 'ERC-20' || token.type === 'ERC-1155' || token.type === 'ERC-404') && (
<Td isNumeric verticalAlign="top">
{ valueStr && (
<Skeleton isLoaded={ !isLoading } display="inline-block" mt="7px" wordBreak="break-all">
......
......@@ -15,6 +15,29 @@ interface Props {
const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => {
const num = value === '1' ? '' : value;
const tokenIdContent = (() => {
if (tokenId === null) {
// ERC-404 may not have an ID
if (token.type === 'ERC-404') {
return null;
}
return <chakra.span color="text_secondary"> N/A </chakra.span>;
}
return (
<NftEntity
hash={ token.address }
id={ tokenId }
fontWeight={ 600 }
iconSize="md"
maxW={{ base: '100%', lg: '150px' }}
w="auto"
flexShrink={ 0 }
/>
);
})();
return (
<>
{ num ? (
......@@ -26,18 +49,7 @@ const NftTokenTransferSnippet = ({ value, token, tokenId }: Props) => {
) : (
<chakra.span color="text_secondary">for token ID</chakra.span>
) }
{ tokenId !== null ? (
<NftEntity
hash={ token.address }
id={ tokenId }
fontWeight={ 600 }
iconSize="md"
maxW={{ base: '100%', lg: '150px' }}
w="auto"
flexShrink={ 0 }
/>
) : <chakra.span color="text_secondary"> N/A </chakra.span>
}
{ tokenIdContent }
<chakra.span color="text_secondary">of</chakra.span>
<TokenEntity
token={ token }
......
import { Flex, chakra } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer as TTokenTransfer, Erc20TotalPayload, Erc721TotalPayload, Erc1155TotalPayload } from 'types/api/tokenTransfer';
import type {
TokenTransfer as TTokenTransfer,
Erc20TotalPayload,
Erc721TotalPayload,
Erc1155TotalPayload,
Erc404TotalPayload,
} from 'types/api/tokenTransfer';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
......@@ -62,6 +68,20 @@ const TxDetailsTokenTransfer = ({ data }: Props) => {
/>
);
}
case 'ERC-404': {
const total = data.total as Erc404TotalPayload;
return (
<NftTokenTransferSnippet
token={ data.token }
tokenId={ 'token_id' in total ? total.token_id : null }
value={ 'value' in total && total.value ?
getCurrencyValue({ value: total.value, decimals: total.decimals || '0', accuracy: 2 }).valueStr :
'1'
}
/>
);
}
}
})();
......
......@@ -8,7 +8,7 @@ import CheckboxInput from 'ui/shared/CheckboxInput';
// does it depend on the network?
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const NOTIFICATIONS_NAMES = [ config.chain.currency.symbol, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
const NOTIFICATIONS_NAMES = [ config.chain.currency.symbol, 'ERC-20', 'ERC-721, ERC-1155, ERC-404 (NFT)' ];
type Props<Inputs extends FieldValues> = {
control: Control<Inputs>;
......
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