Commit 701cd8eb authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into contract-verification-form

parents eb10da29 42f60f7a
......@@ -61,6 +61,10 @@ const oldUrls = [
oldPath: '/address/:id/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`,
},
{
oldPath: '/address/:id/tokens/:hash/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers&token=:hash`,
},
// contract verification
{
oldPath: '/address/:id/contract_verifications/new',
......
......@@ -473,6 +473,8 @@ frontend:
# - "/address"
- "/stats"
- "/search-results"
- "/tokens"
- "/accounts"
resources:
limits:
memory:
......
......@@ -322,6 +322,8 @@ frontend:
- "/stats"
- "/search-results"
- "/token"
- "/tokens"
- "/accounts"
resources:
limits:
......
......@@ -14,6 +14,7 @@ import type {
AddressTokensFilter,
AddressTokensResponse,
} from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract';
......@@ -24,6 +25,7 @@ import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
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 { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
......@@ -127,6 +129,13 @@ export const RESOURCES = {
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: {
path: '/api/v2/addresses/:id',
......@@ -150,7 +159,7 @@ export const RESOURCES = {
address_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 ],
filterFields: [ 'filter' as const, 'type' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
},
address_blocks_validated: {
path: '/api/v2/addresses/:id/blocks-validated',
......@@ -208,6 +217,16 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'value' as const ],
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_stats: {
......@@ -275,10 +294,11 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' |
'address_logs' | 'address_tokens' |
'token_holders';
'token_transfers' | 'token_holders' | 'tokens';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -310,6 +330,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx :
Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters :
Q extends 'address_token_balances' ? Array<AddressTokenBalance> :
......@@ -323,7 +344,9 @@ Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
......@@ -338,9 +361,11 @@ export type PaginationFilters<Q extends PaginatedResources> =
Q extends 'blocks' ? BlockFilters :
Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'search' ? SearchResultFilters :
Q extends 'tokens' ? TokensFilters :
never;
/* eslint-enable @typescript-eslint/indent */
......@@ -12,6 +12,7 @@ import publicTagIcon from 'icons/publictags.svg';
import statsIcon from 'icons/stats.svg';
import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute';
......@@ -27,6 +28,7 @@ export default function useNavItems() {
{ 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: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: false },
isMarketplaceFilled ?
{ 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 },
......
......@@ -15,6 +15,7 @@
"token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verification",
"accounts": "/accounts",
"apps": "/apps",
"app_index": "/apps/:id",
"search_results": "/search-results",
......
......@@ -77,6 +77,12 @@ export const ROUTES = {
crossNetworkNavigation: true,
},
// ACCOUNTS
accounts: {
pattern: PATHS.accounts,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: PATHS.apps,
......
......@@ -18,6 +18,7 @@ SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
......@@ -42,5 +43,6 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>
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 * as tokens from 'mocks/tokens/tokenInfo';
export const erc20a: AddressTokenBalance = {
token: {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
},
token: tokens.tokenInfoERC20a,
token_id: null,
value: '1169320000000000000000000',
};
export const erc20b: AddressTokenBalance = {
token: {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
},
token: tokens.tokenInfoERC20b,
token_id: null,
value: '872500000000',
};
export const erc20c: AddressTokenBalance = {
token: {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
},
token: tokens.tokenInfoERC20c,
token_id: null,
value: '9852000000000000000000',
};
export const erc20d: AddressTokenBalance = {
token: {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
},
token: tokens.tokenInfoERC20d,
token_id: null,
value: '39000000000000000000',
};
export const erc721a: AddressTokenBalance = {
token: {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
},
token: tokens.tokenInfoERC721a,
token_id: null,
value: '51',
};
export const erc721b: AddressTokenBalance = {
token: {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
},
token: tokens.tokenInfoERC721b,
token_id: null,
value: '1',
};
export const erc721c: AddressTokenBalance = {
token: {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
},
token: tokens.tokenInfoERC721c,
token_id: null,
value: '5',
};
export const erc1155a: AddressTokenBalance = {
token: {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
},
token: tokens.tokenInfoERC1155a,
token_id: '42',
value: '24',
};
export const erc1155b: AddressTokenBalance = {
token: {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
},
token: tokens.tokenInfoERC1155b,
token_id: '100010000000001',
value: '11',
};
export const erc1155withoutName: AddressTokenBalance = {
token: {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
},
token: tokens.tokenInfoERC1155WithoutName,
token_id: '64532245',
value: '42',
};
......
......@@ -15,3 +15,113 @@ export const tokenCounters: TokenCounters = {
token_holders_count: '8838883',
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 = {
timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1',
log_index: '1',
method: 'updateSmartAsset',
};
export const erc721: TokenTransfer = {
......@@ -81,6 +82,7 @@ export const erc721: TokenTransfer = {
timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1',
log_index: '1',
method: 'updateSmartAsset',
};
export const erc1155: TokenTransfer = {
......
......@@ -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' }/>;
};
......
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';
......@@ -70,15 +70,15 @@ const variantOutline = defineStyle((props) => {
borderColor,
bg: 'transparent',
_hover: {
color: 'blue.400',
borderColor: 'blue.400',
color: 'link_hovered',
borderColor: 'link_hovered',
bg: 'transparent',
_active: {
bg: props.isActive ? activeBg : 'transparent',
borderColor: props.isActive ? activeBg : 'blue.400',
color: props.isActive ? activeColor : 'blue.400',
borderColor: props.isActive ? activeBg : 'link_hovered',
color: props.isActive ? activeColor : 'link_hovered',
p: {
color: 'blue.400',
color: 'link_hovered',
},
},
_disabled: {
......@@ -86,7 +86,7 @@ const variantOutline = defineStyle((props) => {
borderColor,
},
p: {
color: 'blue.400',
color: 'link_hovered',
},
},
_disabled: {
......@@ -147,7 +147,7 @@ const variantSubtle = defineStyle((props) => {
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
_hover: {
color: 'blue.400',
color: 'link_hovered',
_disabled: {
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
......@@ -160,7 +160,7 @@ const variantSubtle = defineStyle((props) => {
bg: `${ c }.100`,
color: `${ c }.600`,
_hover: {
color: 'blue.400',
color: 'link_hovered',
},
};
});
......
......@@ -8,9 +8,9 @@ const baseStyle = defineStyle(getDefaultTransitionProps());
const variantPrimary = defineStyle((props) => {
return {
color: mode('blue.600', 'blue.300')(props),
color: 'link',
_hover: {
color: mode('blue.400', 'blue.200')(props),
color: 'link_hovered',
textDecorationStyle: props.textDecorationStyle || 'solid',
},
};
......
......@@ -51,7 +51,7 @@ const baseStyleCloseButton = defineStyle((props) => {
height: 10,
width: 10,
color: mode('gray.700', 'gray.500')(props),
_hover: { color: 'blue.400' },
_hover: { color: 'link_hovered' },
_active: { bg: 'none' },
};
});
......
......@@ -23,7 +23,7 @@ const variantSimple = definePartsStyle((props) => {
...transitionProps,
},
td: {
borderColor: mode('blackAlpha.200', 'whiteAlpha.200')(props),
borderColor: 'divider',
...transitionProps,
},
};
......
......@@ -22,7 +22,7 @@ const variantSoftRounded = definePartsStyle((props) => {
},
},
_hover: {
color: 'blue.400',
color: 'link_hovered',
},
},
};
......
......@@ -10,10 +10,6 @@ const variantSecondary = defineStyle((props) => ({
color: mode('gray.500', 'gray.400')(props),
}));
const variantAlternate = defineStyle((props) => ({
color: mode('blue.600', 'blue.300')(props),
}));
const variantInherit = {
color: 'inherit',
};
......@@ -21,7 +17,6 @@ const variantInherit = {
const variants: Record<string, SystemStyleInterpolation> = {
primary: variantPrimary,
secondary: variantSecondary,
alternate: variantAlternate,
inherit: variantInherit,
};
......
const semanticTokens = {
colors: {
divider: {
'default': 'blackAlpha.200',
_dark: 'whiteAlpha.200',
},
link: {
'default': 'blue.600',
_dark: 'blue.300',
},
link_hovered: {
'default': 'blue.400',
},
},
};
export default semanticTokens;
......@@ -5,6 +5,7 @@ import config from './config';
import borders from './foundations/borders';
import breakpoints from './foundations/breakpoints';
import colors from './foundations/colors';
import semanticTokens from './foundations/semanticTokens';
import transition from './foundations/transition';
import typography from './foundations/typography';
import zIndices from './foundations/zIndices';
......@@ -22,6 +23,7 @@ const overrides = {
breakpoints,
transition,
zIndices,
semanticTokens,
};
export default extendTheme(overrides);
......@@ -81,8 +81,9 @@ export interface AddressTokenTransferResponse {
}
export type AddressTokenTransferFilters = {
filter: AddressFromToFilter;
type: Array<TokenType>;
filter?: AddressFromToFilter;
type?: Array<TokenType>;
token?: string;
}
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 {
timestamp: string;
block_hash: string;
log_index: string;
method?: string;
}
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) => {
title="Creator"
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>
<AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/>
</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 { useRouter } from 'next/router';
import React from 'react';
......@@ -9,13 +9,16 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -23,7 +26,8 @@ import Pagination 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 { 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 TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......@@ -61,20 +65,26 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?:
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString();
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const tokenFilter = router.query.token ? router.query.token.toString() : undefined;
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({
resourceName: 'address_token_transfers',
pathParams: { id: currentAddress },
filters: filters,
filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef,
});
......@@ -89,6 +99,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
setFilters((prevState) => ({ ...prevState, filter: filterVal }));
}, [ 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) => {
setSocketAlert('');
......@@ -131,7 +148,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
topic: `addresses:${ (router.query.id as string).toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: pagination.page !== 1,
isDisabled: pagination.page !== 1 || Boolean(tokenFilter),
});
useSocketMessage({
......@@ -141,7 +158,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
});
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 = (() => {
if (isLoading) {
......@@ -179,13 +196,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
showTxInfo
top={ 80 }
enableTimeIncrement
showSocketInfo={ pagination.page === 1 }
showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
/>
</Hide>
<Show below="lg">
{ pagination.page === 1 && (
{ pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice
url={ window.location.href }
num={ newItemsCount }
......@@ -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 (
<>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
{ !isMobile && tokenFilterComponent }
{ !tokenFilter && (
<TokenTransferFilter
defaultTypeFilters={ filters.type }
onTypeFilterChange={ handleTypeFilterChange }
......@@ -217,6 +258,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter }
/>
) }
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
) }
......
......@@ -10,7 +10,7 @@ import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import FilterButton from 'ui/shared/FilterButton';
import FilterButton from 'ui/shared/filters/FilterButton';
interface Props {
isActive: boolean;
......
......@@ -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>
<Address>
<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>
<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>
......@@ -135,7 +135,7 @@ const ContractCode = () => {
<span>Minimal Proxy Contract for </span>
<Address>
<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>
<span>. </span>
<Box>
......
......@@ -36,7 +36,7 @@ const ContractConnectWallet = () => {
<Flex alignItems="center">
<span>Connected to </span>
<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>
<Button onClick={ handleDisconnect } size="sm" variant="outline">Disconnect</Button>
</Flex>
......
......@@ -24,7 +24,7 @@ const ContractImplementationAddress = ({ hash }: Props) => {
return (
<Address whiteSpace="pre-wrap" flexWrap="wrap" mb={ 6 }>
<span>Implementation address: </span>
<AddressLink hash={ data.implementation_address }/>
<AddressLink type="address" hash={ data.implementation_address }/>
</Address>
);
};
......
......@@ -34,7 +34,7 @@ const ContractMethodStatic = ({ data }: Props) => {
const content = (() => {
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>;
......
......@@ -50,7 +50,7 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
}
}, [ toast, token ]);
if (token.type !== 'ERC-20' || !('ethereum' in window)) {
if (!('ethereum' in window)) {
return null;
}
......
......@@ -54,7 +54,7 @@ const TxInternalsListItem = ({
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<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>
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
......@@ -62,7 +62,7 @@ const TxInternalsListItem = ({
}
<Address width="calc((100% - 48px) / 2)">
<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>
</Box>
<HStack spacing={ 3 }>
......
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import { Skeleton, SkeletonCircle, Flex, Box } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
......@@ -13,7 +11,7 @@ const TxInternalsSkeletonMobile = () => {
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
borderColor="divider"
_last={{
borderBottomWidth: '1px',
}}
......
......@@ -62,7 +62,7 @@ const AddressIntTxsTableItem = ({
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<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>
</Td>
<Td px={ 0 } verticalAlign="middle">
......@@ -74,7 +74,7 @@ const AddressIntTxsTableItem = ({
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<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>
</Td>
<Td isNumeric verticalAlign="middle">
......
......@@ -49,7 +49,7 @@ const TokenSelectItem = ({ data }: Props) => {
display="flex"
flexDir="column"
rowGap={ 2 }
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderColor="divider"
borderBottomWidth="1px"
_hover={{
bgColor: useColorModeValue('blue.50', 'gray.800'),
......
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);
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import NextLink from 'next/link';
......@@ -52,8 +52,6 @@ const BlockDetails = () => {
router.push(url, undefined);
}, [ router ]);
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
if (isLoading) {
return <BlockDetailsSkeleton/>;
}
......@@ -76,7 +74,7 @@ const BlockDetails = () => {
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor={ borderColor }
borderColor="divider"
/>
);
const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data);
......@@ -129,7 +127,7 @@ const BlockDetails = () => {
hint="A block producer who successfully included the block onto the blockchain."
columnGap={ 1 }
>
<AddressLink hash={ data.miner.hash }/>
<AddressLink type="address" hash={ data.miner.hash }/>
{ data.miner.name && <Text>{ `(${ capitalize(validatorTitle) }: ${ data.miner.name })` }</Text> }
{ /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
......
import { Grid, GridItem, Skeleton, useColorModeValue } from '@chakra-ui/react';
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const BlockDetailsSkeleton = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const sectionGap = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor={ borderColor }
borderColor="divider"
/>
);
......
......@@ -48,7 +48,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex>
<Flex columnGap={ 2 }>
<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 columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text>
......
......@@ -50,7 +50,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') }</Td>
<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 isNumeric fontSize="sm">{ data.tx_count }</Td>
<Td fontSize="sm">
......
......@@ -84,11 +84,11 @@ const IndexingAlert = ({ className }: { className?: string }) => {
let content;
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.` ;
} else if (data.finished_indexing === false) {
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.`;
}
......
......@@ -7,7 +7,6 @@ import {
Icon,
Link,
Text,
useColorModeValue,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
......@@ -36,14 +35,14 @@ const LatestBlocksItem = ({ block, h }: Props) => {
transitionTimingFunction="linear"
borderRadius="12px"
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderColor="divider"
p={ 6 }
h={ `${ h }px` }
minWidth={{ base: '100%', lg: '280px' }}
>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<HStack spacing={ 2 }>
<Icon as={ blockIcon } boxSize="30px" color={ useColorModeValue('blue.600', 'blue.300') }/>
<Icon as={ blockIcon } boxSize="30px" color="link"/>
<Link
href={ link('block', { id: String(block.height) }) }
fontSize="xl"
......@@ -61,7 +60,7 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<GridItem>Reward</GridItem>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></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>
</Box>
);
......
......@@ -5,7 +5,6 @@ import {
GridItem,
HStack,
Skeleton,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
......@@ -15,7 +14,7 @@ const LatestBlocksItemSkeleton = () => {
minWidth={{ base: '100%', lg: '280px' }}
borderRadius="12px"
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderColor="divider"
p={ 6 }
>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
......
......@@ -11,7 +11,6 @@ import {
PopoverTrigger,
PopoverContent,
PopoverBody,
useColorModeValue,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
......@@ -37,9 +36,6 @@ type Props = {
}
const LatestBlocksItem = ({ tx }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const iconColor = useColorModeValue('blue.600', 'blue.300');
const dataTo = tx.to ? tx.to : tx.created_contract;
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
......@@ -50,10 +46,10 @@ const LatestBlocksItem = ({ tx }: Props) => {
<Box
width="100%"
borderTop="1px solid"
borderColor={ borderColor }
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
<Flex justifyContent="space-between" width="100%" alignItems="start" flexDirection={{ base: 'column', lg: 'row' }}>
{ !isMobile && (
......@@ -63,7 +59,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
<PopoverTrigger>
<AdditionalInfoButton isOpen={ isOpen } mr={ 3 }/>
</PopoverTrigger>
<PopoverContent border="1px solid" borderColor={ borderColor }>
<PopoverContent border="1px solid" borderColor="divider">
<PopoverBody>
<TxAdditionalInfo tx={ tx }/>
</PopoverBody>
......@@ -92,7 +88,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
color="link"
/>
<Address width="100%">
<AddressLink
......@@ -111,6 +107,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
<Address>
<AddressIcon address={ tx.from }/>
<AddressLink
type="address"
hash={ tx.from.hash }
alias={ tx.from.name }
fontWeight="500"
......@@ -128,6 +125,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
<Address>
<AddressIcon address={ dataTo }/>
<AddressLink
type="address"
hash={ dataTo.hash }
alias={ dataTo.name }
fontWeight="500"
......
......@@ -4,21 +4,18 @@ import {
HStack,
Skeleton,
SkeletonCircle,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
const LatestTxsItemSkeleton = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box
width="100%"
borderTop="1px solid"
borderColor={ borderColor }
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
<Flex justifyContent="space-between" width="100%" alignItems="start" flexDirection={{ base: 'column', lg: 'row' }}>
<Box width="100%">
......
......@@ -30,13 +30,14 @@ const Stats = () => {
let content;
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
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) {
const isOdd = Boolean(hasGasTracker && !data.gas_prices ? (itemsCount - 1) % 2 : itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null;
content = (
<>
......@@ -63,14 +64,14 @@ const Stats = () => {
icon={ walletIcon }
title="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() }
_last={ itemsCount % 2 ? lastItemTouchStyle : undefined }
_last={ isOdd ? lastItemTouchStyle : undefined }
/>
{ hasGasTracker && data.gas_prices && (
<StatsItem
icon={ gasIcon }
title="Gas tracker"
value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` }
_last={ itemsCount % 2 ? lastItemTouchStyle : undefined }
_last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel }
/>
) }
......
import { Flex, Skeleton, useColorModeValue } from '@chakra-ui/react';
import { Flex, Skeleton, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
const StatsItemSkeleton = () => {
const StatsItemSkeleton = ({ className }: {className?: string}) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
......@@ -13,6 +13,7 @@ const StatsItemSkeleton = () => {
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
className={ className }
>
<Skeleton
w="40px"
......@@ -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';
import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput';
import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplaceApps from '../apps/useMarketplaceApps';
......
......@@ -10,6 +10,7 @@ import Token from './Token';
const TOKEN_API_URL = buildApiUrl('token', { 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 hooksConfig = {
router: {
......@@ -33,6 +34,10 @@ test('base view +@dark-mode', async({ mount, page }) => {
status: 200,
body: JSON.stringify(tokenCounters),
}));
await page.route(TOKEN_TRANSFERS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({}),
}));
const component = await mount(
<TestApp>
......@@ -41,5 +46,5 @@ test('base view +@dark-mode', async({ mount, page }) => {
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
await expect(component.locator('main')).toHaveScreenshot();
});
......@@ -15,6 +15,7 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders'
......@@ -29,13 +30,14 @@ const TokenPageContent = () => {
queryOptions: { enabled: Boolean(router.query.hash) },
});
// const transfersQuery = useQueryWithPages({
// resourceName: 'token_transfers',
// pathParams: { hash: router.query.hash?.toString() },
// options: {
// enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
// },
// });
const transfersQuery = useQueryWithPages({
resourceName: 'token_transfers',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && (!router.query.tab || router.query.tab === 'token_transfers') && tokenQuery.data),
},
});
const holdersQuery = useQueryWithPages({
resourceName: 'token_holders',
......@@ -47,16 +49,18 @@ const TokenPageContent = () => {
});
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 }/> },
];
let hasPagination;
let pagination;
// if (router.query.tab === 'token_transfers') {
// hasPagination = transfersQuery.isPaginationVisible;
// pagination = transfersQuery.pagination;
// }
if (!router.query.tab || router.query.tab === 'token_transfers') {
hasPagination = transfersQuery.isPaginationVisible;
pagination = transfersQuery.pagination;
}
if (router.query.tab === 'holders') {
hasPagination = holdersQuery.isPaginationVisible;
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) => {
<Address>
<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">
<AddressLink hash={ data.address } fontWeight={ 700 } display="block" w="100%"/>
<AddressLink type="address" hash={ data.address } fontWeight={ 700 } display="block" w="100%"/>
</Box>
</Address>
);
......
......@@ -18,6 +18,10 @@ const ActionBar = ({ children, className }: Props) => {
const isSticky = useIsSticky(ref, TOP_UP + 5);
const bgColor = useColorModeValue('white', 'black');
if (!React.Children.toArray(children).filter(Boolean).length) {
return null;
}
return (
<Flex
className={ className }
......
......@@ -17,7 +17,6 @@ interface Props {
const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600');
const infoColor = useColorModeValue('blue.600', 'blue.300');
return (
<Button
......@@ -36,8 +35,8 @@ const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.
<Icon
as={ infoIcon }
boxSize={ 5 }
color={ infoColor }
_hover={{ color: 'blue.400' }}
color="link"
_hover={{ color: 'link_hovered' }}
/>
</Button>
);
......
......@@ -18,7 +18,7 @@ const AddressSnippet = ({ address, subtitle }: Props) => {
<Box maxW="100%">
<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 }/>
</Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> }
......
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import { Flex, chakra } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
......@@ -20,7 +20,7 @@ const ListItemMobile = ({ children, className, isAnimated }: Props) => {
alignItems="flex-start"
flexDirection="column"
paddingY={ 6 }
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderColor="divider"
borderTopWidth="1px"
_last={{
borderBottomWidth: '1px',
......
......@@ -11,7 +11,7 @@ import {
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import type { RoutedTab } from './types';
......@@ -43,6 +43,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
const isMobile = useIsMobile();
const tabsRef = useRef<HTMLDivElement>(null);
const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile);
const isSticky = useIsSticky(listRef, 5, stickyEnabled);
const listBgColor = useColorModeValue('white', 'black');
......@@ -57,6 +58,23 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
);
}, [ 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(() => {
if (router.isReady) {
let tabIndex = 0;
......@@ -104,6 +122,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
index={ activeTabIndex }
position="relative"
size={ themeProps.size || 'md' }
ref={ tabsRef }
>
<TabList
marginBottom={{ base: 6, lg: 8 }}
......
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
CheckboxGroup,
Checkbox,
Text,
useDisclosure,
Radio,
RadioGroup,
Stack,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo';
import FilterButton from 'ui/shared/FilterButton';
import { TOKEN_TYPE } from './helpers';
import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
interface Props {
appliedFiltersNum?: number;
......@@ -38,21 +29,9 @@ const TokenTransferFilter = ({
onAddressFilterChange,
defaultAddressFilter,
}: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<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 }>
<PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }}>
{ withAddressFilter && (
<>
<Text variant="secondary" fontWeight={ 600 }>Address</Text>
......@@ -62,7 +41,7 @@ const TokenTransferFilter = ({
defaultValue={ defaultAddressFilter || 'all' }
paddingBottom={ 4 }
borderBottom="1px solid"
borderColor={ borderColor }
borderColor="divider"
>
<Stack spacing={ 4 }>
<Radio value="all"><Text fontSize="md">All</Text></Radio>
......@@ -73,12 +52,8 @@ const TokenTransferFilter = ({
</>
) }
<Text variant="secondary" fontWeight={ 600 }>Type</Text>
<CheckboxGroup size="lg" onChange={ onTypeFilterChange } defaultValue={ defaultTypeFilters }>
{ TOKEN_TYPE.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
<TokenTypeFilter onChange={ onTypeFilterChange } defaultValue={ defaultTypeFilters }/>
</PopoverFilter>
);
};
......
import { Text, Flex, Tag, Icon, useColorModeValue } from '@chakra-ui/react';
import { Text, Flex, Tag, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -43,8 +43,6 @@ const TokenTransferListItem = ({
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`;
......@@ -64,7 +62,7 @@ const TokenTransferListItem = ({
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
color="link"
/>
<Address width="100%">
<AddressLink
......@@ -81,7 +79,7 @@ const TokenTransferListItem = ({
<Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }>
<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>
{ baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
......@@ -89,7 +87,7 @@ const TokenTransferListItem = ({
}
<Address width={ addressWidth }>
<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>
</Flex>
{ value && (
......
import { Box, Icon, Link } from '@chakra-ui/react';
import { Box, Icon, Link, chakra } from '@chakra-ui/react';
import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg';
......@@ -8,11 +8,20 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
hash: string;
id: string;
className?: string;
}
const TokenTransferNft = ({ hash, id }: Props) => {
const TokenTransferNft = ({ hash, id, className }: Props) => {
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"/>
<Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
......@@ -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 = ({
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<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>
</Td>
{ baseAddress && (
......@@ -81,7 +81,7 @@ const TokenTransferTableItem = ({
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<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>
</Td>
<Td isNumeric verticalAlign="top" lineHeight="30px">
......
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
......@@ -25,9 +24,3 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
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';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
type?: 'address' | 'transaction' | 'token' | 'block' | 'token_instance_item';
alias?: string | null;
import TruncatedTextTooltip from '../TruncatedTextTooltip';
type CommonProps = {
className?: string;
hash: string;
truncation?: 'constant' | 'dynamic'| 'none';
fontWeight?: string;
id?: string;
target?: HTMLAttributeAnchorTarget;
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();
let url;
if (type === 'transaction') {
url = link('tx', { id: id || hash });
url = link('tx', { id: hash });
} else if (type === 'token') {
url = link('token_index', { hash: id || hash });
} else if (type === 'token_instance_item') {
url = link('token_instance_item', { hash, id });
url = link('token_index', { hash: hash });
} 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 {
url = link('address_index', { id: id || hash });
url = link('address_index', { id: hash });
}
const content = (() => {
if (alias) {
const text = <Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box>;
if (type === 'token') {
return (
<TruncatedTextTooltip label={ alias }>
{ text }
</TruncatedTextTooltip>
);
}
return (
<Tooltip label={ hash } isDisabled={ isMobile }>
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box>
{ text }
</Tooltip>
);
}
switch (truncation) {
case 'constant':
return <HashStringShorten hash={ id || hash } isTooltipDisabled={ isMobile }/>;
return <HashStringShorten hash={ hash } isTooltipDisabled={ isMobile }/>;
case 'dynamic':
return <HashStringShortenDynamic hash={ id || hash } fontWeight={ fontWeight } isTooltipDisabled={ isMobile }/>;
return <HashStringShortenDynamic hash={ hash } fontWeight={ fontWeight } isTooltipDisabled={ isMobile }/>;
case 'none':
return <span>{ id || hash }</span>;
return <span>{ hash }</span>;
}
})();
......
import { useColorModeValue, useToken } from '@chakra-ui/react';
import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
......@@ -13,8 +13,7 @@ interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> {
const ChartGridLine = ({ type, scale, ticks, size, disableAnimation, ...props }: Props) => {
const ref = React.useRef<SVGGElement>(null);
const strokeColorToken = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const strokeColor = useToken('colors', strokeColorToken);
const strokeColor = useToken('colors', 'divider');
React.useEffect(() => {
if (!ref.current) {
......
......@@ -10,10 +10,11 @@ type Props = {
className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string;
initialValue?: string;
}
const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => {
const [ filterQuery, setFilterQuery ] = useState('');
const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null);
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) => {
<TableRow key={ name } name={ name } type={ type } isLast={ index === data.parameters.length - 1 } indexed={ indexed }>
{ type === 'address' ? (
<Address justifyContent="space-between">
<AddressLink hash={ value }/>
<AddressLink type="address" hash={ value }/>
<CopyToClipboard text={ value }/>
</Address>
) : (
......
......@@ -53,7 +53,7 @@ const TxLogItem = ({ address, index, topics, data, decoded, type }: Props) => {
<GridItem display="flex" alignItems="center">
<Address mr={{ base: 9, lg: 0 }}>
<AddressIcon address={ address }/>
<AddressLink hash={ address.hash } alias={ address.name } ml={ 2 }/>
<AddressLink type="address" hash={ address.hash } alias={ address.name } ml={ 2 }/>
</Address>
{ /* api doesn't have find topic feature yet */ }
{ /* <Tooltip label="Find matches topic">
......
......@@ -51,7 +51,7 @@ const LogTopic = ({ hex, index }: Props) => {
case 'address': {
return (
<Address>
<AddressLink hash={ value }/>
<AddressLink type="address" hash={ value }/>
<CopyToClipboard text={ value }/>
</Address>
);
......
import { Box, Flex, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import { Box, Flex, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
const SkeletonList = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
......@@ -13,7 +11,7 @@ const SkeletonList = () => {
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
borderColor="divider"
_last={{
borderBottomWidth: '0px',
}}
......
import { Box, Flex, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import { Box, Flex, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
interface Props {
......@@ -6,8 +6,6 @@ interface Props {
}
const SkeletonListAccount = ({ showFooterSlot }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
......@@ -17,7 +15,7 @@ const SkeletonListAccount = ({ showFooterSlot }: Props) => {
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
borderColor="divider"
_last={{
borderBottomWidth: '0px',
}}
......
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.
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