Commit cafee731 authored by tom's avatar tom

Merge branches 'semantic-tokens' and 'main' of github.com:blockscout/frontend into semantic-tokens

parents 016efabd 02977865
...@@ -61,6 +61,10 @@ const oldUrls = [ ...@@ -61,6 +61,10 @@ const oldUrls = [
oldPath: '/address/:id/validations', oldPath: '/address/:id/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`, newPath: `${ PATHS.address_index }?tab=blocks_validated`,
}, },
{
oldPath: '/address/:id/tokens/:hash/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers&token=:hash`,
},
]; ];
async function redirects() { async function redirects() {
......
...@@ -473,6 +473,8 @@ frontend: ...@@ -473,6 +473,8 @@ frontend:
# - "/address" # - "/address"
- "/stats" - "/stats"
- "/search-results" - "/search-results"
- "/tokens"
- "/accounts"
resources: resources:
limits: limits:
memory: memory:
......
...@@ -322,6 +322,8 @@ frontend: ...@@ -322,6 +322,8 @@ frontend:
- "/stats" - "/stats"
- "/search-results" - "/search-results"
- "/token" - "/token"
- "/tokens"
- "/accounts"
resources: resources:
limits: limits:
......
...@@ -14,6 +14,7 @@ import type { ...@@ -14,6 +14,7 @@ import type {
AddressTokensFilter, AddressTokensFilter,
AddressTokensResponse, AddressTokensResponse,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract';
...@@ -24,6 +25,7 @@ import type { RawTracesResponse } from 'types/api/rawTrace'; ...@@ -24,6 +25,7 @@ import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokensResponse, TokensFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
...@@ -127,6 +129,13 @@ export const RESOURCES = { ...@@ -127,6 +129,13 @@ export const RESOURCES = {
path: '/api/v2/transactions/:id/raw-trace', path: '/api/v2/transactions/:id/raw-trace',
}, },
// ADDRESSES
addresses: {
path: '/api/v2/addresses/',
paginationFields: [ 'fetched_coin_balance' as const, 'hash' as const, 'items_count' as const ],
filterFields: [ ],
},
// ADDRESS // ADDRESS
address: { address: {
path: '/api/v2/addresses/:id', path: '/api/v2/addresses/:id',
...@@ -150,7 +159,7 @@ export const RESOURCES = { ...@@ -150,7 +159,7 @@ export const RESOURCES = {
address_token_transfers: { address_token_transfers: {
path: '/api/v2/addresses/:id/token-transfers', path: '/api/v2/addresses/:id/token-transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const ], filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
}, },
address_blocks_validated: { address_blocks_validated: {
path: '/api/v2/addresses/:id/blocks-validated', path: '/api/v2/addresses/:id/blocks-validated',
...@@ -208,6 +217,16 @@ export const RESOURCES = { ...@@ -208,6 +217,16 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'value' as const ], paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
token_transfers: {
path: '/api/v2/tokens/:hash/transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
tokens: {
path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ],
filterFields: [ 'filter' as const, 'type' as const ],
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
...@@ -275,10 +294,11 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }> ...@@ -275,10 +294,11 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' | export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' | 'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' |
'token_holders'; 'token_transfers' | 'token_holders' | 'tokens';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -310,6 +330,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse : ...@@ -310,6 +330,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx : Q extends 'tx_logs' ? LogsResponseTx :
Q extends 'tx_token_transfers' ? TokenTransferResponse : Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse : Q extends 'tx_raw_trace' ? RawTracesResponse :
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_token_balances' ? Array<AddressTokenBalance> :
...@@ -323,7 +344,9 @@ Q extends 'address_logs' ? LogsResponseAddress : ...@@ -323,7 +344,9 @@ Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'token' ? TokenInfo : Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders : Q extends 'token_holders' ? TokenHolders :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
...@@ -338,9 +361,11 @@ export type PaginationFilters<Q extends PaginatedResources> = ...@@ -338,9 +361,11 @@ export type PaginationFilters<Q extends PaginatedResources> =
Q extends 'blocks' ? BlockFilters : Q extends 'blocks' ? BlockFilters :
Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters : Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters : Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters : Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter : Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'search' ? SearchResultFilters : Q extends 'search' ? SearchResultFilters :
Q extends 'tokens' ? TokensFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -12,6 +12,7 @@ import publicTagIcon from 'icons/publictags.svg'; ...@@ -12,6 +12,7 @@ import publicTagIcon from 'icons/publictags.svg';
import statsIcon from 'icons/stats.svg'; import statsIcon from 'icons/stats.svg';
import tokensIcon from 'icons/token.svg'; import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg'; import transactionsIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link'; import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute'; import useCurrentRoute from 'lib/link/useCurrentRoute';
...@@ -27,6 +28,7 @@ export default function useNavItems() { ...@@ -27,6 +28,7 @@ export default function useNavItems() {
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: false }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: false },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: false }, { text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: false },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: false },
isMarketplaceFilled ? isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null, { text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: false }, { text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: false },
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
"token_instance_item": "/token/:hash/instance/:id", "token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id", "address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verifications/new", "address_contract_verification": "/address/:id/contract_verifications/new",
"accounts": "/accounts",
"apps": "/apps", "apps": "/apps",
"app_index": "/apps/:id", "app_index": "/apps/:id",
"search_results": "/search-results", "search_results": "/search-results",
......
...@@ -77,6 +77,12 @@ export const ROUTES = { ...@@ -77,6 +77,12 @@ export const ROUTES = {
crossNetworkNavigation: true, crossNetworkNavigation: true,
}, },
// ACCOUNTS
accounts: {
pattern: PATHS.accounts,
crossNetworkNavigation: true,
},
// APPS // APPS
apps: { apps: {
pattern: PATHS.apps, pattern: PATHS.apps,
......
...@@ -18,6 +18,7 @@ SocketMessage.AddressCoinBalance | ...@@ -18,6 +18,7 @@ SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs | SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -42,5 +43,6 @@ export namespace SocketMessage { ...@@ -42,5 +43,6 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
import type { TokenType } from 'types/api/tokenInfo';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
export default TOKEN_TYPE;
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import * as tokens from 'mocks/tokens/tokenInfo';
export const erc20a: AddressTokenBalance = { export const erc20a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20a,
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '1169320000000000000000000', value: '1169320000000000000000000',
}; };
export const erc20b: AddressTokenBalance = { export const erc20b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20b,
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '872500000000', value: '872500000000',
}; };
export const erc20c: AddressTokenBalance = { export const erc20c: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20c,
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '9852000000000000000000', value: '9852000000000000000000',
}; };
export const erc20d: AddressTokenBalance = { export const erc20d: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20d,
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '39000000000000000000', value: '39000000000000000000',
}; };
export const erc721a: AddressTokenBalance = { export const erc721a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721a,
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '51', value: '51',
}; };
export const erc721b: AddressTokenBalance = { export const erc721b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721b,
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '1', value: '1',
}; };
export const erc721c: AddressTokenBalance = { export const erc721c: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721c,
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '5', value: '5',
}; };
export const erc1155a: AddressTokenBalance = { export const erc1155a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155a,
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
},
token_id: '42', token_id: '42',
value: '24', value: '24',
}; };
export const erc1155b: AddressTokenBalance = { export const erc1155b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155b,
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
},
token_id: '100010000000001', token_id: '100010000000001',
value: '11', value: '11',
}; };
export const erc1155withoutName: AddressTokenBalance = { export const erc1155withoutName: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155WithoutName,
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
},
token_id: '64532245', token_id: '64532245',
value: '42', value: '42',
}; };
......
...@@ -15,3 +15,113 @@ export const tokenCounters: TokenCounters = { ...@@ -15,3 +15,113 @@ export const tokenCounters: TokenCounters = {
token_holders_count: '8838883', token_holders_count: '8838883',
transfers_count: '88282281', transfers_count: '88282281',
}; };
export const tokenInfoERC20a: TokenInfo = {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20b: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20c: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20d: TokenInfo = {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC721a: TokenInfo = {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
};
export const tokenInfoERC721b: TokenInfo = {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
};
export const tokenInfoERC721c: TokenInfo = {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
};
export const tokenInfoERC1155a: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
};
export const tokenInfoERC1155b: TokenInfo = {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
};
export const tokenInfoERC1155WithoutName: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
};
...@@ -40,6 +40,7 @@ export const erc20: TokenTransfer = { ...@@ -40,6 +40,7 @@ export const erc20: TokenTransfer = {
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
method: 'updateSmartAsset',
}; };
export const erc721: TokenTransfer = { export const erc721: TokenTransfer = {
...@@ -81,6 +82,7 @@ export const erc721: TokenTransfer = { ...@@ -81,6 +82,7 @@ export const erc721: TokenTransfer = {
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
method: 'updateSmartAsset',
}; };
export const erc1155: TokenTransfer = { export const erc1155: TokenTransfer = {
......
...@@ -50,7 +50,7 @@ const CustomErrorComponent = (props: Props) => { ...@@ -50,7 +50,7 @@ const CustomErrorComponent = (props: Props) => {
); );
} }
const colorModeCookie = cookies.getFromCookieString(props.cookies, cookies.NAMES.COLOR_MODE); const colorModeCookie = cookies.getFromCookieString(props.cookies || '', cookies.NAMES.COLOR_MODE);
return <NextErrorComponent statusCode={ props.statusCode } withDarkMode={ colorModeCookie === 'dark' }/>; return <NextErrorComponent statusCode={ props.statusCode } withDarkMode={ colorModeCookie === 'dark' }/>;
}; };
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Accounts from 'ui/pages/Accounts';
const AccountsPage: NextPage = () => {
const title = `Top Accounts - ${ getNetworkTitle() }`;
return (
<>
<Head><title>{ title }</title></Head>
<Accounts/>
</>
);
};
export default AccountsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Tokens from 'ui/pages/Tokens';
const TokensPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Tokens/>
</>
);
};
export default TokensPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
...@@ -81,8 +81,9 @@ export interface AddressTokenTransferResponse { ...@@ -81,8 +81,9 @@ export interface AddressTokenTransferResponse {
} }
export type AddressTokenTransferFilters = { export type AddressTokenTransferFilters = {
filter: AddressFromToFilter; filter?: AddressFromToFilter;
type: Array<TokenType>; type?: Array<TokenType>;
token?: string;
} }
export type AddressTokensFilter = { export type AddressTokensFilter = {
......
import type { AddressParam } from './addressParams';
export type AddressesItem = AddressParam &{ tx_count: string; coin_balance: string }
export type AddressesResponse = {
items: Array<AddressesItem>;
next_page_params: {
fetched_coin_balance: string;
hash: string;
items_count: number;
};
total_supply: string;
}
...@@ -39,6 +39,7 @@ interface TokenTransferBase { ...@@ -39,6 +39,7 @@ interface TokenTransferBase {
timestamp: string; timestamp: string;
block_hash: string; block_hash: string;
log_index: string; log_index: string;
method?: string;
} }
export type TokenTransferPagination = { export type TokenTransferPagination = {
......
import type { TokenInfo, TokenType } from './tokenInfo';
export type TokensResponse = {
items: Array<TokenInfo>;
next_page_params: {
holder_count: number;
items_count: number;
name: string;
};
}
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
...@@ -100,7 +100,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -100,7 +100,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
title="Creator" title="Creator"
hint="Transaction and address of creation." hint="Transaction and address of creation."
> >
<AddressLink hash={ addressQuery.data.creator_address_hash } truncation="constant"/> <AddressLink type="address" hash={ addressQuery.data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text> <Text whiteSpace="pre"> at txn </Text>
<AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/> <AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/>
</DetailsInfoItem> </DetailsInfoItem>
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { erc1155 } from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = {
router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
test('with token filter and pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ], next_page_params: { block_number: 1 } }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ] }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Text } from '@chakra-ui/react'; import { Flex, Hide, Icon, Show, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,13 +9,16 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap ...@@ -9,13 +9,16 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
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 TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -23,7 +26,8 @@ import Pagination from 'ui/shared/Pagination'; ...@@ -23,7 +26,8 @@ import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { TOKEN_TYPE, flattenTotal } from 'ui/shared/TokenTransfer/helpers'; import TokenLogo from 'ui/shared/TokenLogo';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
...@@ -61,20 +65,26 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: ...@@ -61,20 +65,26 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?:
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString(); const currentAddress = router.query.id?.toString();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0); const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const tokenFilter = router.query.token ? router.query.token.toString() : undefined;
const [ filters, setFilters ] = React.useState<Filters>( const [ filters, setFilters ] = React.useState<Filters>(
{ type: getTokenFilterValue(router.query.type) || [], filter: getAddressFilterValue(router.query.filter) }, {
type: getTokenFilterValue(router.query.type) || [],
filter: getAddressFilterValue(router.query.filter),
},
); );
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_token_transfers', resourceName: 'address_token_transfers',
pathParams: { id: currentAddress }, pathParams: { id: currentAddress },
filters: filters, filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef, scrollRef,
}); });
...@@ -89,6 +99,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -89,6 +99,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
setFilters((prevState) => ({ ...prevState, filter: filterVal })); setFilters((prevState) => ({ ...prevState, filter: filterVal }));
}, [ filters, onFilterChange ]); }, [ filters, onFilterChange ]);
const resetTokenFilter = React.useCallback(() => {
onFilterChange({});
}, [ onFilterChange ]);
const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
...@@ -131,7 +148,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -131,7 +148,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
topic: `addresses:${ (router.query.id as string).toLowerCase() }`, topic: `addresses:${ (router.query.id as string).toLowerCase() }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: pagination.page !== 1, isDisabled: pagination.page !== 1 || Boolean(tokenFilter),
}); });
useSocketMessage({ useSocketMessage({
...@@ -141,7 +158,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -141,7 +158,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}); });
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !numActiveFilters && !data?.items.length; const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length;
const content = (() => { const content = (() => {
if (isLoading) { if (isLoading) {
...@@ -179,13 +196,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -179,13 +196,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
showTxInfo showTxInfo
top={ 80 } top={ 80 }
enableTimeIncrement enableTimeIncrement
showSocketInfo={ pagination.page === 1 } showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
/> />
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
{ pagination.page === 1 && ( { pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice <SocketNewItemsNotice
url={ window.location.href } url={ window.location.href }
num={ newItemsCount } num={ newItemsCount }
...@@ -205,10 +222,34 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -205,10 +222,34 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
); );
})(); })();
const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" py={ 1 } flexWrap="wrap" mb={{ base: isPaginationVisible ? 6 : 3, lg: 0 }}>
Filtered by token
<TokenLogo hash={ tokenFilter } boxSize={ 6 } mx={ 2 }/>
{ isMobile ? tokenFilter.slice(0, 4) + '...' + tokenFilter.slice(-4) : tokenFilter }
<Tooltip label="Reset filter">
<Flex>
<Icon
as={ crossIcon }
boxSize={ 6 }
ml={ 1 }
color={ resetTokenIconColor }
cursor="pointer"
_hover={{ color: resetTokenIconHoverColor }}
onClick={ resetTokenFilter }
/>
</Flex>
</Tooltip>
</Flex>
);
return ( return (
<> <>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && ( { !isActionBarHidden && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
{ !isMobile && tokenFilterComponent }
{ !tokenFilter && (
<TokenTransferFilter <TokenTransferFilter
defaultTypeFilters={ filters.type } defaultTypeFilters={ filters.type }
onTypeFilterChange={ handleTypeFilterChange } onTypeFilterChange={ handleTypeFilterChange }
...@@ -217,6 +258,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -217,6 +258,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
onAddressFilterChange={ handleAddressFilterChange } onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter } defaultAddressFilter={ filters.filter }
/> />
) }
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> } { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar> </ActionBar>
) } ) }
......
...@@ -10,7 +10,7 @@ import React from 'react'; ...@@ -10,7 +10,7 @@ import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import FilterButton from 'ui/shared/FilterButton'; import FilterButton from 'ui/shared/filters/FilterButton';
interface Props { interface Props {
isActive: boolean; isActive: boolean;
......
...@@ -123,7 +123,7 @@ const ContractCode = () => { ...@@ -123,7 +123,7 @@ const ContractCode = () => {
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span> <span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address> <Address>
<AddressIcon address={{ hash: data.verified_twin_address_hash, is_contract: true, implementation_name: null }}/> <AddressIcon address={{ hash: data.verified_twin_address_hash, is_contract: true, implementation_name: null }}/>
<AddressLink hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/> <AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address> </Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span> <chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<Link href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</Link> <Link href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</Link>
...@@ -135,7 +135,7 @@ const ContractCode = () => { ...@@ -135,7 +135,7 @@ const ContractCode = () => {
<span>Minimal Proxy Contract for </span> <span>Minimal Proxy Contract for </span>
<Address> <Address>
<AddressIcon address={{ hash: data.minimal_proxy_address_hash, is_contract: true, implementation_name: null }}/> <AddressIcon address={{ hash: data.minimal_proxy_address_hash, is_contract: true, implementation_name: null }}/>
<AddressLink hash={ data.minimal_proxy_address_hash } truncation="constant" ml={ 2 }/> <AddressLink type="address" hash={ data.minimal_proxy_address_hash } truncation="constant" ml={ 2 }/>
</Address> </Address>
<span>. </span> <span>. </span>
<Box> <Box>
......
...@@ -36,7 +36,7 @@ const ContractConnectWallet = () => { ...@@ -36,7 +36,7 @@ const ContractConnectWallet = () => {
<Flex alignItems="center"> <Flex alignItems="center">
<span>Connected to </span> <span>Connected to </span>
<AddressIcon address={{ hash: address, is_contract: false, implementation_name: null }} mx={ 2 }/> <AddressIcon address={{ hash: address, is_contract: false, implementation_name: null }} mx={ 2 }/>
<AddressLink fontWeight={ 600 } hash={ address } truncation={ isMobile ? 'constant' : 'dynamic' }/> <AddressLink type="address" fontWeight={ 600 } hash={ address } truncation={ isMobile ? 'constant' : 'dynamic' }/>
</Flex> </Flex>
<Button onClick={ handleDisconnect } size="sm" variant="outline">Disconnect</Button> <Button onClick={ handleDisconnect } size="sm" variant="outline">Disconnect</Button>
</Flex> </Flex>
......
...@@ -24,7 +24,7 @@ const ContractImplementationAddress = ({ hash }: Props) => { ...@@ -24,7 +24,7 @@ const ContractImplementationAddress = ({ hash }: Props) => {
return ( return (
<Address whiteSpace="pre-wrap" flexWrap="wrap" mb={ 6 }> <Address whiteSpace="pre-wrap" flexWrap="wrap" mb={ 6 }>
<span>Implementation address: </span> <span>Implementation address: </span>
<AddressLink hash={ data.implementation_address }/> <AddressLink type="address" hash={ data.implementation_address }/>
</Address> </Address>
); );
}; };
......
...@@ -34,7 +34,7 @@ const ContractMethodStatic = ({ data }: Props) => { ...@@ -34,7 +34,7 @@ const ContractMethodStatic = ({ data }: Props) => {
const content = (() => { const content = (() => {
if (data.type === 'address' && data.value) { if (data.type === 'address' && data.value) {
return <AddressLink hash={ data.value }/>; return <AddressLink type="address" hash={ data.value }/>;
} }
return <chakra.span wordBreak="break-all">({ data.type }): { value }</chakra.span>; return <chakra.span wordBreak="break-all">({ data.type }): { value }</chakra.span>;
......
...@@ -50,7 +50,7 @@ const AddressAddToMetaMask = ({ className, token }: Props) => { ...@@ -50,7 +50,7 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
} }
}, [ toast, token ]); }, [ toast, token ]);
if (token.type !== 'ERC-20' || !('ethereum' in window)) { if (!('ethereum' in window)) {
return null; return null;
} }
......
...@@ -54,7 +54,7 @@ const TxInternalsListItem = ({ ...@@ -54,7 +54,7 @@ const TxInternalsListItem = ({
<Box w="100%" display="flex" columnGap={ 3 }> <Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/>
</Address> </Address>
{ (isIn || isOut) ? { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> : <InOutTag isIn={ isIn } isOut={ isOut }/> :
...@@ -62,7 +62,7 @@ const TxInternalsListItem = ({ ...@@ -62,7 +62,7 @@ const TxInternalsListItem = ({
} }
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/> <AddressIcon address={ toData }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
</Address> </Address>
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
......
...@@ -62,7 +62,7 @@ const AddressIntTxsTableItem = ({ ...@@ -62,7 +62,7 @@ const AddressIntTxsTableItem = ({
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ isOut }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ isOut }/>
</Address> </Address>
</Td> </Td>
<Td px={ 0 } verticalAlign="middle"> <Td px={ 0 } verticalAlign="middle">
...@@ -74,7 +74,7 @@ const AddressIntTxsTableItem = ({ ...@@ -74,7 +74,7 @@ const AddressIntTxsTableItem = ({
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/> <AddressIcon address={ toData }/>
<AddressLink hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/> <AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
</Address> </Address>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
......
import { Flex, Tag, Text, HStack } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { AddressesItem } from 'types/api/addresses';
import appConfig from 'configs/app/config';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
type Props = {
item: AddressesItem;
index: number;
totalSupply: string;
}
const AddressesListItem = ({
item,
index,
totalSupply,
}: Props) => {
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** appConfig.network.currency.decimals));
return (
<ListItemMobile rowGap={ 3 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
<Address maxW="100%" mr={ 8 }>
<AddressIcon address={ item } mr={ 2 }/>
<AddressLink
fontWeight={ 700 }
flexGrow={ 1 }
w="calc(100% - 32px)"
hash={ item.hash }
alias={ item.name }
type="address"
/>
</Address>
<Text fontSize="sm" ml="auto" variant="secondary">{ index }</Text>
</Flex>
{ item.public_tags !== null && item.public_tags.length > 0 && item.public_tags.map(tag => (
<Tag key={ tag.label }>{ tag.display_name }</Tag>
)) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>{ `Balance ${ appConfig.network.currency.symbol }` }</Text>
<Text fontSize="sm" variant="secondary">{ addressBalance.dp(8).toFormat() }</Text>
</HStack>
{ totalSupply && totalSupply !== '0' && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Percentage</Text>
<Text fontSize="sm" variant="secondary">{ addressBalance.div(BigNumber(totalSupply)).multipliedBy(100).dp(8).toFormat() + '%' }</Text>
</HStack>
) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Txn count</Text>
<Text fontSize="sm" variant="secondary">{ Number(item.tx_count).toLocaleString('en') }</Text>
</HStack>
</ListItemMobile>
);
};
export default React.memo(AddressesListItem);
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { AddressesItem } from 'types/api/addresses';
import appConfig from 'configs/app/config';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressesTableItem from './AddressesTableItem';
interface Props {
items: Array<AddressesItem>;
totalSupply: string;
pageStartIndex: number;
}
const AddressesTable = ({ items, totalSupply, pageStartIndex }: Props) => {
const hasPercentage = Boolean(totalSupply && totalSupply !== '0');
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="64px">Rank</Th>
<Th width={ hasPercentage ? '30%' : '40%' }>Address</Th>
<Th width="20%" pl={ 10 }>Public tag</Th>
<Th width={ hasPercentage ? '20%' : '25%' } isNumeric>{ `Balance ${ appConfig.network.currency.symbol }` }</Th>
{ hasPercentage && <Th width="15%" isNumeric>Percentage</Th> }
<Th width="15%" isNumeric>Txn count</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<AddressesTableItem
key={ item.hash }
item={ item }
totalSupply={ totalSupply }
index={ pageStartIndex + index }
hasPercentage={ hasPercentage }
/>
)) }
</Tbody>
</Table>
);
};
export default AddressesTable;
import { Tr, Td, Tag, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { AddressesItem } from 'types/api/addresses';
import appConfig from 'configs/app/config';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
type Props = {
item: AddressesItem;
index: number;
totalSupply: string;
hasPercentage: boolean;
}
const AddressesTableItem = ({
item,
index,
totalSupply,
hasPercentage,
}: Props) => {
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** appConfig.network.currency.decimals));
const addressBalanceChunks = addressBalance.dp(8).toFormat().split('.');
return (
<Tr>
<Td>
<Text lineHeight="24px">{ index }</Text>
</Td>
<Td>
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ item } mr={ 2 }/>
<AddressLink
fontWeight={ 700 }
flexGrow={ 1 }
w="calc(100% - 32px)"
hash={ item.hash }
alias={ item.name }
type="address"
/>
</Address>
</Td>
<Td pl={ 10 }>
{ item.public_tags && item.public_tags.length ? item.public_tags.map(tag => (
<Tag key={ tag.label }>{ tag.display_name }</Tag>
)) : <Text lineHeight="24px">-</Text> }
</Td>
<Td isNumeric>
<Text lineHeight="24px" as="span">{ addressBalanceChunks[0] }</Text>
{ addressBalanceChunks[1] && <Text lineHeight="24px" as="span">.</Text> }
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
</Td>
{ hasPercentage && (
<Td isNumeric>
<Text lineHeight="24px">{ addressBalance.div(BigNumber(totalSupply)).multipliedBy(100).dp(8).toFormat() + '%' }</Text>
</Td>
) }
<Td isNumeric>
<Text lineHeight="24px">{ Number(item.tx_count).toLocaleString('en') }</Text>
</Td>
</Tr>
);
};
export default React.memo(AddressesTableItem);
...@@ -127,7 +127,7 @@ const BlockDetails = () => { ...@@ -127,7 +127,7 @@ const BlockDetails = () => {
hint="A block producer who successfully included the block onto the blockchain." hint="A block producer who successfully included the block onto the blockchain."
columnGap={ 1 } columnGap={ 1 }
> >
<AddressLink hash={ data.miner.hash }/> <AddressLink type="address" hash={ data.miner.hash }/>
{ data.miner.name && <Text>{ `(${ capitalize(validatorTitle) }: ${ data.miner.name })` }</Text> } { data.miner.name && <Text>{ `(${ capitalize(validatorTitle) }: ${ data.miner.name })` }</Text> }
{ /* api doesn't return the block processing time yet */ } { /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ } { /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
......
...@@ -48,7 +48,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -48,7 +48,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text> <Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
<AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/> <AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text> <Text fontWeight={ 500 }>Txn</Text>
......
...@@ -50,7 +50,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -50,7 +50,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') }</Td> <Td fontSize="sm">{ data.size.toLocaleString('en') }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/> <AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/>
</Td> </Td>
<Td isNumeric fontSize="sm">{ data.tx_count }</Td> <Td isNumeric fontSize="sm">{ data.tx_count }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
......
...@@ -84,11 +84,11 @@ const IndexingAlert = ({ className }: { className?: string }) => { ...@@ -84,11 +84,11 @@ const IndexingAlert = ({ className }: { className?: string }) => {
let content; let content;
if (data.finished_indexing_blocks === false) { if (data.finished_indexing_blocks === false) {
content = `${ data.indexed_blocks_ratio && `${ (Number(data.indexed_blocks_ratio) * 100).toFixed() }% Blocks Indexed${ nbsp }${ ndash } ` } content = `${ data.indexed_blocks_ratio && `${ Math.floor(Number(data.indexed_blocks_ratio) * 100) }% Blocks Indexed${ nbsp }${ ndash } ` }
We're indexing this chain right now. Some of the counts may be inaccurate.` ; We're indexing this chain right now. Some of the counts may be inaccurate.` ;
} else if (data.finished_indexing === false) { } else if (data.finished_indexing === false) {
content = `${ data.indexed_inernal_transactions_ratio && content = `${ data.indexed_inernal_transactions_ratio &&
`${ (Number(data.indexed_inernal_transactions_ratio) * 100).toFixed() }% Blocks With Internal Transactions Indexed${ nbsp }${ ndash } ` } `${ Math.floor(Number(data.indexed_inernal_transactions_ratio) * 100) }% Blocks With Internal Transactions Indexed${ nbsp }${ ndash } ` }
We're indexing this chain right now. Some of the counts may be inaccurate.`; We're indexing this chain right now. Some of the counts may be inaccurate.`;
} }
......
...@@ -60,7 +60,7 @@ const LatestBlocksItem = ({ block, h }: Props) => { ...@@ -60,7 +60,7 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<GridItem>Reward</GridItem> <GridItem>Reward</GridItem>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem> <GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem>
<GridItem>Miner</GridItem> <GridItem>Miner</GridItem>
<GridItem><AddressLink alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem> <GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem>
</Grid> </Grid>
</Box> </Box>
); );
......
...@@ -107,6 +107,7 @@ const LatestBlocksItem = ({ tx }: Props) => { ...@@ -107,6 +107,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
<Address> <Address>
<AddressIcon address={ tx.from }/> <AddressIcon address={ tx.from }/>
<AddressLink <AddressLink
type="address"
hash={ tx.from.hash } hash={ tx.from.hash }
alias={ tx.from.name } alias={ tx.from.name }
fontWeight="500" fontWeight="500"
...@@ -124,6 +125,7 @@ const LatestBlocksItem = ({ tx }: Props) => { ...@@ -124,6 +125,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
<Address> <Address>
<AddressIcon address={ dataTo }/> <AddressIcon address={ dataTo }/>
<AddressLink <AddressLink
type="address"
hash={ dataTo.hash } hash={ dataTo.hash }
alias={ dataTo.name } alias={ dataTo.name }
fontWeight="500" fontWeight="500"
......
...@@ -30,13 +30,14 @@ const Stats = () => { ...@@ -30,13 +30,14 @@ const Stats = () => {
let content; let content;
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
if (isLoading) { if (isLoading) {
content = Array.from(Array(itemsCount)).map((item, index) => <StatsItemSkeleton key={ index }/>); content = Array.from(Array(itemsCount)).map((item, index) => <StatsItemSkeleton key={ index } _last={ itemsCount % 2 ? lastItemTouchStyle : undefined }/>);
} }
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
if (data) { if (data) {
const isOdd = Boolean(hasGasTracker && !data.gas_prices ? (itemsCount - 1) % 2 : itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null; const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null;
content = ( content = (
<> <>
...@@ -63,14 +64,14 @@ const Stats = () => { ...@@ -63,14 +64,14 @@ const Stats = () => {
icon={ walletIcon } icon={ walletIcon }
title="Wallet addresses" title="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() } value={ Number(data.total_addresses).toLocaleString() }
_last={ itemsCount % 2 ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
/> />
{ hasGasTracker && data.gas_prices && ( { hasGasTracker && data.gas_prices && (
<StatsItem <StatsItem
icon={ gasIcon } icon={ gasIcon }
title="Gas tracker" title="Gas tracker"
value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` } value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` }
_last={ itemsCount % 2 ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel } tooltipLabel={ gasLabel }
/> />
) } ) }
......
import { Flex, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { Flex, Skeleton, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const StatsItemSkeleton = () => { const StatsItemSkeleton = ({ className }: {className?: string}) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return ( return (
...@@ -13,6 +13,7 @@ const StatsItemSkeleton = () => { ...@@ -13,6 +13,7 @@ const StatsItemSkeleton = () => {
alignItems="center" alignItems="center"
columnGap={ 3 } columnGap={ 3 }
rowGap={ 2 } rowGap={ 2 }
className={ className }
> >
<Skeleton <Skeleton
w="40px" w="40px"
...@@ -26,4 +27,4 @@ const StatsItemSkeleton = () => { ...@@ -26,4 +27,4 @@ const StatsItemSkeleton = () => {
); );
}; };
export default StatsItemSkeleton; export default chakra(StatsItemSkeleton);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import type { AddressesResponse } from 'types/api/addresses';
import * as addressMocks from 'mocks/address/address';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Accounts from './Accounts';
const ADDRESSES_API_URL = buildApiUrl('addresses');
const addresses: AddressesResponse = {
items: [
{
...addressMocks.withName,
tx_count: '1',
coin_balance: '12345678901234567890000',
}, {
...addressMocks.token,
tx_count: '109123890123',
coin_balance: '22222345678901234567890000',
}, {
...addressMocks.withoutName,
tx_count: '11',
coin_balance: '1000000000000000000',
},
],
total_supply: '25222000',
next_page_params: {
items_count: 50,
fetched_coin_balance: '123',
hash: 'aa',
},
};
test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(ADDRESSES_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addresses),
}));
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
const component = await mount(
<TestApp>
<Accounts/>
</TestApp>,
);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Hide, Show } from '@chakra-ui/react';
import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import AddressesListItem from 'ui/addresses/AddressesListItem';
import AddressesTable from 'ui/addresses/AddressesTable';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const PAGE_SIZE = 50;
const Accounts = () => {
const { isError, isLoading, data, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'addresses',
});
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
const bar = isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
if (isLoading) {
return (
<>
{ bar }
<Show below="lg">
<SkeletonList/>
</Show>
<Hide below="lg">
<SkeletonTable columns={ [ '64px', '30%', '20%', '20%', '15%', '15%' ] }/>
</Hide>
</>
);
}
const pageStartIndex = (pagination.page - 1) * PAGE_SIZE + 1;
return (
<>
{ bar }
<Hide below="lg" ssr={ false }>
<AddressesTable
items={ data.items }
totalSupply={ data.total_supply }
pageStartIndex={ pageStartIndex }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => {
return (
<AddressesListItem
key={ item.hash }
item={ item }
index={ pageStartIndex + index }
totalSupply={ data.total_supply }
/>
);
}) }
</Show>
</>
);
})();
return (
<Page>
<PageTitle text="Top accounts" withTextAd/>
{ content }
</Page>
);
};
export default Accounts;
...@@ -6,7 +6,7 @@ import PlusIcon from 'icons/plus.svg'; ...@@ -6,7 +6,7 @@ import PlusIcon from 'icons/plus.svg';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton'; import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu'; import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplaceApps from '../apps/useMarketplaceApps'; import useMarketplaceApps from '../apps/useMarketplaceApps';
......
...@@ -10,6 +10,7 @@ import Token from './Token'; ...@@ -10,6 +10,7 @@ import Token from './Token';
const TOKEN_API_URL = buildApiUrl('token', { hash: '1' }); const TOKEN_API_URL = buildApiUrl('token', { hash: '1' });
const TOKEN_COUNTERS_API_URL = buildApiUrl('token_counters', { hash: '1' }); const TOKEN_COUNTERS_API_URL = buildApiUrl('token_counters', { hash: '1' });
const TOKEN_TRANSFERS_API_URL = buildApiUrl('token_transfers', { hash: '1' });
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' }); const ADDRESS_API_URL = buildApiUrl('address', { id: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
...@@ -33,6 +34,10 @@ test('base view +@dark-mode', async({ mount, page }) => { ...@@ -33,6 +34,10 @@ test('base view +@dark-mode', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify(tokenCounters), body: JSON.stringify(tokenCounters),
})); }));
await page.route(TOKEN_TRANSFERS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({}),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
...@@ -41,5 +46,5 @@ test('base view +@dark-mode', async({ mount, page }) => { ...@@ -41,5 +46,5 @@ test('base view +@dark-mode', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot(); await expect(component.locator('main')).toHaveScreenshot();
}); });
...@@ -15,6 +15,7 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; ...@@ -15,6 +15,7 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders' export type TokenTabs = 'token_transfers' | 'holders'
...@@ -29,13 +30,14 @@ const TokenPageContent = () => { ...@@ -29,13 +30,14 @@ const TokenPageContent = () => {
queryOptions: { enabled: Boolean(router.query.hash) }, queryOptions: { enabled: Boolean(router.query.hash) },
}); });
// const transfersQuery = useQueryWithPages({ const transfersQuery = useQueryWithPages({
// resourceName: 'token_transfers', resourceName: 'token_transfers',
// pathParams: { hash: router.query.hash?.toString() }, pathParams: { hash: router.query.hash?.toString() },
// options: { scrollRef,
// enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data), options: {
// }, enabled: Boolean(router.query.hash && (!router.query.tab || router.query.tab === 'token_transfers') && tokenQuery.data),
// }); },
});
const holdersQuery = useQueryWithPages({ const holdersQuery = useQueryWithPages({
resourceName: 'token_holders', resourceName: 'token_holders',
...@@ -47,16 +49,18 @@ const TokenPageContent = () => { ...@@ -47,16 +49,18 @@ const TokenPageContent = () => {
}); });
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: null }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
]; ];
let hasPagination; let hasPagination;
let pagination; let pagination;
// if (router.query.tab === 'token_transfers') {
// hasPagination = transfersQuery.isPaginationVisible; if (!router.query.tab || router.query.tab === 'token_transfers') {
// pagination = transfersQuery.pagination; hasPagination = transfersQuery.isPaginationVisible;
// } pagination = transfersQuery.pagination;
}
if (router.query.tab === 'holders') { if (router.query.tab === 'holders') {
hasPagination = holdersQuery.isPaginationVisible; hasPagination = holdersQuery.isPaginationVisible;
pagination = holdersQuery.pagination; pagination = holdersQuery.pagination;
......
import React from 'react';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import TokensList from 'ui/tokens/Tokens';
const Tokens = () => {
return (
<Page>
<PageTitle text="Tokens" withTextAd/>
<TokensList/>
</Page>
);
};
export default Tokens;
...@@ -43,7 +43,7 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -43,7 +43,7 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
<Address> <Address>
<AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/> <AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/>
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block" whiteSpace="nowrap" overflow="hidden"> <Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block" whiteSpace="nowrap" overflow="hidden">
<AddressLink hash={ data.address } fontWeight={ 700 } display="block" w="100%"/> <AddressLink type="address" hash={ data.address } fontWeight={ 700 } display="block" w="100%"/>
</Box> </Box>
</Address> </Address>
); );
......
...@@ -18,6 +18,10 @@ const ActionBar = ({ children, className }: Props) => { ...@@ -18,6 +18,10 @@ const ActionBar = ({ children, className }: Props) => {
const isSticky = useIsSticky(ref, TOP_UP + 5); const isSticky = useIsSticky(ref, TOP_UP + 5);
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
if (!React.Children.toArray(children).filter(Boolean).length) {
return null;
}
return ( return (
<Flex <Flex
className={ className } className={ className }
......
...@@ -18,7 +18,7 @@ const AddressSnippet = ({ address, subtitle }: Props) => { ...@@ -18,7 +18,7 @@ const AddressSnippet = ({ address, subtitle }: Props) => {
<Box maxW="100%"> <Box maxW="100%">
<Address> <Address>
<AddressIcon address={ address }/> <AddressIcon address={ address }/>
<AddressLink hash={ address.hash } fontWeight="600" ml={ 2 }/> <AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 }/>
<CopyToClipboard text={ address.hash } ml={ 1 }/> <CopyToClipboard text={ address.hash } ml={ 1 }/>
</Address> </Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> } { subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> }
......
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system'; import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import type { RoutedTab } from './types'; import type { RoutedTab } from './types';
...@@ -43,6 +43,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -43,6 +43,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1); const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const tabsRef = useRef<HTMLDivElement>(null);
const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile); const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile);
const isSticky = useIsSticky(listRef, 5, stickyEnabled); const isSticky = useIsSticky(listRef, 5, stickyEnabled);
const listBgColor = useColorModeValue('white', 'black'); const listBgColor = useColorModeValue('white', 'black');
...@@ -57,6 +58,23 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -57,6 +58,23 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
); );
}, [ tabs, router ]); }, [ tabs, router ]);
useEffect(() => {
if (router.query.scroll_to_tabs) {
tabsRef?.current?.scrollIntoView(true);
delete router.query.scroll_to_tabs;
router.push(
{
pathname: router.pathname,
query: router.query,
},
undefined,
{ shallow: true },
);
}
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
let tabIndex = 0; let tabIndex = 0;
...@@ -104,6 +122,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -104,6 +122,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
index={ activeTabIndex } index={ activeTabIndex }
position="relative" position="relative"
size={ themeProps.size || 'md' } size={ themeProps.size || 'md' }
ref={ tabsRef }
> >
<TabList <TabList
marginBottom={{ base: 6, lg: 8 }} marginBottom={{ base: 6, lg: 8 }}
......
import { import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
CheckboxGroup,
Checkbox,
Text, Text,
useDisclosure,
Radio, Radio,
RadioGroup, RadioGroup,
Stack, Stack,
...@@ -16,9 +9,8 @@ import React from 'react'; ...@@ -16,9 +9,8 @@ import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/tokenInfo';
import FilterButton from 'ui/shared/FilterButton'; import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
import { TOKEN_TYPE } from './helpers';
interface Props { interface Props {
appliedFiltersNum?: number; appliedFiltersNum?: number;
...@@ -37,19 +29,9 @@ const TokenTransferFilter = ({ ...@@ -37,19 +29,9 @@ const TokenTransferFilter = ({
onAddressFilterChange, onAddressFilterChange,
defaultAddressFilter, defaultAddressFilter,
}: Props) => { }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }}>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent w="200px">
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
{ withAddressFilter && ( { withAddressFilter && (
<> <>
<Text variant="secondary" fontWeight={ 600 }>Address</Text> <Text variant="secondary" fontWeight={ 600 }>Address</Text>
...@@ -70,12 +52,8 @@ const TokenTransferFilter = ({ ...@@ -70,12 +52,8 @@ const TokenTransferFilter = ({
</> </>
) } ) }
<Text variant="secondary" fontWeight={ 600 }>Type</Text> <Text variant="secondary" fontWeight={ 600 }>Type</Text>
<CheckboxGroup size="lg" onChange={ onTypeFilterChange } defaultValue={ defaultTypeFilters }> <TokenTypeFilter onChange={ onTypeFilterChange } defaultValue={ defaultTypeFilters }/>
{ TOKEN_TYPE.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) } </PopoverFilter>
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
); );
}; };
......
...@@ -79,7 +79,7 @@ const TokenTransferListItem = ({ ...@@ -79,7 +79,7 @@ const TokenTransferListItem = ({
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/>
</Address> </Address>
{ baseAddress ? { baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> : <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
...@@ -87,7 +87,7 @@ const TokenTransferListItem = ({ ...@@ -87,7 +87,7 @@ const TokenTransferListItem = ({
} }
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ to }/> <AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/>
</Address> </Address>
</Flex> </Flex>
{ value && ( { value && (
......
import { Box, Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg'; import nftPlaceholder from 'icons/nft_shield.svg';
...@@ -8,11 +8,20 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -8,11 +8,20 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
hash: string; hash: string;
id: string; id: string;
className?: string;
} }
const TokenTransferNft = ({ hash, id }: Props) => { const TokenTransferNft = ({ hash, id, className }: Props) => {
return ( return (
<Link href={ link('token_instance_item', { hash, id }) } overflow="hidden" whiteSpace="nowrap" display="flex" alignItems="center" w="100%"> <Link
href={ link('token_instance_item', { hash, id }) }
overflow="hidden"
whiteSpace="nowrap"
display="flex"
alignItems="center"
w="100%"
className={ className }
>
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/> <Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/>
<Box maxW="calc(100% - 34px)"> <Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
...@@ -21,4 +30,4 @@ const TokenTransferNft = ({ hash, id }: Props) => { ...@@ -21,4 +30,4 @@ const TokenTransferNft = ({ hash, id }: Props) => {
); );
}; };
export default React.memo(TokenTransferNft); export default React.memo(chakra(TokenTransferNft));
...@@ -70,7 +70,7 @@ const TokenTransferTableItem = ({ ...@@ -70,7 +70,7 @@ const TokenTransferTableItem = ({
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ baseAddress === from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ baseAddress === from.hash }/>
</Address> </Address>
</Td> </Td>
{ baseAddress && ( { baseAddress && (
...@@ -81,7 +81,7 @@ const TokenTransferTableItem = ({ ...@@ -81,7 +81,7 @@ const TokenTransferTableItem = ({
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/> <AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } isDisabled={ baseAddress === to.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } isDisabled={ baseAddress === to.hash }/>
</Address> </Address>
</Td> </Td>
<Td isNumeric verticalAlign="top" lineHeight="30px"> <Td isNumeric verticalAlign="top" lineHeight="30px">
......
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => { export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
...@@ -25,9 +24,3 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => { ...@@ -25,9 +24,3 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
return 'Token transfer'; return 'Token transfer';
} }
}; };
export const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
...@@ -7,49 +7,76 @@ import link from 'lib/link/link'; ...@@ -7,49 +7,76 @@ import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { import TruncatedTextTooltip from '../TruncatedTextTooltip';
type?: 'address' | 'transaction' | 'token' | 'block' | 'token_instance_item';
alias?: string | null; type CommonProps = {
className?: string; className?: string;
hash: string;
truncation?: 'constant' | 'dynamic'| 'none'; truncation?: 'constant' | 'dynamic'| 'none';
fontWeight?: string;
id?: string;
target?: HTMLAttributeAnchorTarget; target?: HTMLAttributeAnchorTarget;
isDisabled?: boolean; isDisabled?: boolean;
fontWeight?: string;
alias?: string | null;
}
type AddressTokenTxProps = {
type: 'address' | 'token' | 'transaction';
hash: 'hash';
}
type BlockProps = {
type: 'block';
hash: string;
id: string;
}
type AddressTokenProps = {
type: 'address_token';
hash: string;
tokenHash: string;
} }
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target = '_self', isDisabled }: Props) => { type Props = CommonProps & (AddressTokenTxProps | BlockProps | AddressTokenProps);
const AddressLink = (props: Props) => {
const { alias, type, className, truncation = 'dynamic', hash, fontWeight, target = '_self', isDisabled } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
let url; let url;
if (type === 'transaction') { if (type === 'transaction') {
url = link('tx', { id: id || hash }); url = link('tx', { id: hash });
} else if (type === 'token') { } else if (type === 'token') {
url = link('token_index', { hash: id || hash }); url = link('token_index', { hash: hash });
} else if (type === 'token_instance_item') {
url = link('token_instance_item', { hash, id });
} else if (type === 'block') { } else if (type === 'block') {
url = link('block', { id: id || hash }); url = link('block', { id: props.id });
} else if (type === 'address_token') {
url = link('address_index', { id: hash }, { tab: 'token_transfers', token: props.tokenHash, scroll_to_tabs: 'true' });
} else { } else {
url = link('address_index', { id: id || hash }); url = link('address_index', { id: hash });
} }
const content = (() => { const content = (() => {
if (alias) { if (alias) {
const text = <Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box>;
if (type === 'token') {
return (
<TruncatedTextTooltip label={ alias }>
{ text }
</TruncatedTextTooltip>
);
}
return ( return (
<Tooltip label={ hash } isDisabled={ isMobile }> <Tooltip label={ hash } isDisabled={ isMobile }>
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box> { text }
</Tooltip> </Tooltip>
); );
} }
switch (truncation) { switch (truncation) {
case 'constant': case 'constant':
return <HashStringShorten hash={ id || hash } isTooltipDisabled={ isMobile }/>; return <HashStringShorten hash={ hash } isTooltipDisabled={ isMobile }/>;
case 'dynamic': case 'dynamic':
return <HashStringShortenDynamic hash={ id || hash } fontWeight={ fontWeight } isTooltipDisabled={ isMobile }/>; return <HashStringShortenDynamic hash={ hash } fontWeight={ fontWeight } isTooltipDisabled={ isMobile }/>;
case 'none': case 'none':
return <span>{ id || hash }</span>; return <span>{ hash }</span>;
} }
})(); })();
......
...@@ -10,10 +10,11 @@ type Props = { ...@@ -10,10 +10,11 @@ type Props = {
className?: string; className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg'; size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string; placeholder: string;
initialValue?: string;
} }
const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => { const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
......
import type { PopoverContentProps } from '@chakra-ui/react';
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import FilterButton from 'ui/shared/filters/FilterButton';
interface Props {
appliedFiltersNum?: number;
isActive?: boolean;
children: React.ReactNode;
contentProps?: PopoverContentProps;
}
const PopoverFilter = ({ appliedFiltersNum, children, contentProps, isActive }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || isActive || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent { ...contentProps }>
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
{ children }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(PopoverFilter);
import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import TOKEN_TYPE from 'lib/token/tokenTypes';
type Props = {
onChange: (nextValue: Array<TokenType>) => void;
defaultValue?: Array<TokenType>;
}
const TokenTypeFilter = ({ onChange, defaultValue }: Props) => {
return (
<CheckboxGroup size="lg" onChange={ onChange } defaultValue={ defaultValue }>
{ TOKEN_TYPE.map(({ title, id }) => (
<Checkbox key={ id } value={ id }>
<Text fontSize="md">{ title }</Text>
</Checkbox>
)) }
</CheckboxGroup>
);
};
export default TokenTypeFilter;
...@@ -159,7 +159,7 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -159,7 +159,7 @@ const LogDecodedInputData = ({ data }: Props) => {
<TableRow key={ name } name={ name } type={ type } isLast={ index === data.parameters.length - 1 } indexed={ indexed }> <TableRow key={ name } name={ name } type={ type } isLast={ index === data.parameters.length - 1 } indexed={ indexed }>
{ type === 'address' ? ( { type === 'address' ? (
<Address justifyContent="space-between"> <Address justifyContent="space-between">
<AddressLink hash={ value }/> <AddressLink type="address" hash={ value }/>
<CopyToClipboard text={ value }/> <CopyToClipboard text={ value }/>
</Address> </Address>
) : ( ) : (
......
...@@ -53,7 +53,7 @@ const TxLogItem = ({ address, index, topics, data, decoded, type }: Props) => { ...@@ -53,7 +53,7 @@ const TxLogItem = ({ address, index, topics, data, decoded, type }: Props) => {
<GridItem display="flex" alignItems="center"> <GridItem display="flex" alignItems="center">
<Address mr={{ base: 9, lg: 0 }}> <Address mr={{ base: 9, lg: 0 }}>
<AddressIcon address={ address }/> <AddressIcon address={ address }/>
<AddressLink hash={ address.hash } alias={ address.name } ml={ 2 }/> <AddressLink type="address" hash={ address.hash } alias={ address.name } ml={ 2 }/>
</Address> </Address>
{ /* api doesn't have find topic feature yet */ } { /* api doesn't have find topic feature yet */ }
{ /* <Tooltip label="Find matches topic"> { /* <Tooltip label="Find matches topic">
......
...@@ -51,7 +51,7 @@ const LogTopic = ({ hex, index }: Props) => { ...@@ -51,7 +51,7 @@ const LogTopic = ({ hex, index }: Props) => {
case 'address': { case 'address': {
return ( return (
<Address> <Address>
<AddressLink hash={ value }/> <AddressLink type="address" hash={ value }/>
<CopyToClipboard text={ value }/> <CopyToClipboard text={ value }/>
</Address> </Address>
); );
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import type { StatsChartsSection } from 'types/api/stats'; import type { StatsChartsSection } from 'types/api/stats';
import type { StatsInterval, StatsIntervalIds } from 'types/client/stats'; import type { StatsInterval, StatsIntervalIds } from 'types/client/stats';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import { STATS_INTERVALS } from './constants'; import { STATS_INTERVALS } from './constants';
import StatsDropdownMenu from './StatsDropdownMenu'; import StatsDropdownMenu from './StatsDropdownMenu';
......
...@@ -48,7 +48,7 @@ const TokenContractInfo = ({ tokenQuery }: Props) => { ...@@ -48,7 +48,7 @@ const TokenContractInfo = ({ tokenQuery }: Props) => {
return ( return (
<Flex alignItems="center"> <Flex alignItems="center">
<AddressContractIcon/> <AddressContractIcon/>
<AddressLink hash={ hash } ml={ 2 } truncation={ isMobile ? 'constant' : 'none' }/> <AddressLink type="address" hash={ hash } ml={ 2 } truncation={ isMobile ? 'constant' : 'none' }/>
<CopyToClipboard text={ hash } ml={ 1 }/> <CopyToClipboard text={ hash } ml={ 1 }/>
{ contractQuery.data?.token && <AddressAddToMetaMask token={ contractQuery.data?.token } ml={ 2 }/> } { contractQuery.data?.token && <AddressAddToMetaMask token={ contractQuery.data?.token } ml={ 2 }/> }
<AddressQrCode hash={ hash } ml={ 2 }/> <AddressQrCode hash={ hash } ml={ 2 }/>
......
...@@ -22,7 +22,7 @@ const TokenHoldersListItem = ({ holder, token }: Props) => { ...@@ -22,7 +22,7 @@ const TokenHoldersListItem = ({ holder, token }: Props) => {
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/> <AddressIcon address={ holder.address }/>
<AddressLink ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/> <AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
</Address> </Address>
<Flex justifyContent="space-between" alignItems="center" width="100%"> <Flex justifyContent="space-between" alignItems="center" width="100%">
{ quantity } { quantity }
......
...@@ -22,7 +22,7 @@ const TokenTransferTableItem = ({ holder, token }: Props) => { ...@@ -22,7 +22,7 @@ const TokenTransferTableItem = ({ holder, token }: Props) => {
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/> <AddressIcon address={ holder.address }/>
<AddressLink ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/> <AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
</Address> </Address>
</Td> </Td>
<Td isNumeric> <Td isNumeric>
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import TokenTransfer from './TokenTransfer';
test('erc20 +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={ tokenInfo }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
data: {
items: [ tokenTransferMock.erc20 ],
next_page_params: null,
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('erc721 +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={{ ...tokenInfo, type: 'ERC-721' }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
data: {
items: [ tokenTransferMock.erc721 ],
next_page_params: null,
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('erc1155 +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={{ ...tokenInfo, type: 'ERC-1155' }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
data: {
items: [ tokenTransferMock.erc1155, tokenTransferMock.erc1155multiple ],
next_page_params: null,
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
type Props = {
token?: TokenInfo;
transfersQuery: UseQueryResult<TokenTransferResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenTransfer = ({ transfersQuery, token }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery;
const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const handleNewTransfersMessage: SocketMessage.TokenTransfers['handler'] = (payload) => {
setNewItemsCount(payload.token_transfer);
};
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please refresh the page to load new token transfers.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new token transfers. Please refresh the page.');
}, []);
const channel = useSocketChannel({
topic: `tokens:${ router.query.hash?.toString().toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: isLoading || isError || pagination.page !== 1,
});
useSocketMessage({
channel,
event: 'token_transfer',
handler: handleNewTransfersMessage,
});
const content = (() => {
if (isLoading) {
return (
<>
<Hide below="lg">
<SkeletonTable columns={ [ '45%', '15%', '36px', '15%', '25%' ] }
/>
</Hide>
<Show below="lg">
<SkeletonList/>
</Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (!data.items?.length) {
return <Text as="span">There are no token transfers</Text>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<TokenTransferTable
data={ items }
top={ 80 }
// token transfers query depends on token data
// so if we are here, we definitely have token data
token={ token as TokenInfo }
showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
/>
</Hide>
<Show below="lg">
{ pagination.page === 1 && (
<SocketNewItemsNotice
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
/>
) }
<TokenTransferList data={ items }/>
</Show>
</>
);
})();
return (
<>
{ isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ content }
</>
);
};
export default React.memo(TokenTransfer);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem';
interface Props {
data: Array<TokenTransfer>;
}
const TokenTransferList = ({ data }: Props) => {
return (
<Box>
{ data.map((item, index) => (
<TokenTransferListItem
key={ index }
{ ...item }
/>
)) }
</Box>
);
};
export default TokenTransferList;
import { Text, Flex, Tag, Icon, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import eastArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer;
const TokenTransferListItem = ({
token,
total,
tx_hash: txHash,
from,
to,
method,
timestamp,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
return null;
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const timeAgo = useTimeAgoIncrement(timestamp, true);
return (
<ListItemMobile rowGap={ 3 } isAnimated>
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex>
<Icon
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
<Address width="100%">
<AddressLink
hash={ txHash }
type="transaction"
fontWeight="700"
truncation="constant"
/>
</Address>
</Flex>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
</Flex>
{ method && <Tag colorScheme="gray">{ method }</Tag> }
<Flex w="100%" columnGap={ 3 }>
<Address width="50%">
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address }/>
</Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Address width="50%">
<AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address }/>
</Address>
</Flex>
{ value && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text>
<Text variant="secondary">{ value }</Text>
<Text>{ token.symbol }</Text>
</Flex>
) }
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
</ListItemMobile>
);
};
export default React.memo(TokenTransferListItem);
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem';
interface Props {
data: Array<TokenTransfer>;
top: number;
token: TokenInfo;
showSocketInfo: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
}
const TokenTransferTable = ({ data, top, token, showSocketInfo, socketInfoAlert, socketInfoNum }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="40%">Txn hash</Th>
<Th width="164px">Method</Th>
<Th width="148px">From</Th>
<Th width="36px" px={ 0 }/>
<Th width="218px" >To</Th>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && <Th width="20%" isNumeric={ token.type === 'ERC-721' }>Token ID</Th> }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && <Th width="20%" isNumeric>Value { token.symbol }</Th> }
</Tr>
</Thead>
<Tbody>
{ showSocketInfo && (
<Tr>
<Td colSpan={ 10 } p={ 0 }>
<SocketNewItemsNotice
borderRadius={ 0 }
pl="10px"
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
type="token_transfer"
/>
</Td>
</Tr>
) }
{ data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(TokenTransferTable);
import { Tr, Td, Tag, Text, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import eastArrowIcon from 'icons/arrows/east.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer
const TokenTransferTableItem = ({
token,
total,
tx_hash: txHash,
from,
to,
method,
timestamp,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
return '-';
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const timeAgo = useTimeAgoIncrement(timestamp, true);
return (
<Tr alignItems="top">
<Td>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/>
</Address>
{ timestamp && <Text color="gray.500" fontWeight="400" mt="10px">{ timeAgo }</Text> }
</Td>
<Td>
{ method ? <Tag colorScheme="gray">{ method }</Tag> : '-' }
</Td>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/>
<AddressLink
ml={ 2 }
flexGrow={ 1 }
fontWeight="500"
type="address_token"
hash={ from.hash }
alias={ from.name }
tokenHash={ token.address }
truncation="constant"
/>
</Address>
</Td>
<Td px={ 0 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/>
<AddressLink
ml={ 2 }
flexGrow={ 1 }
fontWeight="500"
type="address_token"
hash={ to.hash }
alias={ to.name }
tokenHash={ token.address }
truncation="constant"
/>
</Address>
</Td>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<Td lineHeight="30px">
{ 'token_id' in total ? (
<TokenTransferNft
hash={ token.address }
id={ total.token_id }
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }/>
) : '-'
}
</Td>
) }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Td isNumeric verticalAlign="top" lineHeight="30px">
{ value || '-' }
</Td>
) }
</Tr>
);
};
export default React.memo(TokenTransferTableItem);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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