Commit 758af1c4 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #560 from blockscout/address-404

404 for address
parents daf36687 7eca8dce
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters'; import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokenBalanceMock from 'mocks/address/tokenBalance';
...@@ -36,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => { ...@@ -36,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, unknown>}/> <AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, ResourceError>}/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -65,7 +66,7 @@ test('token', async({ mount, page }) => { ...@@ -65,7 +66,7 @@ test('token', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<MockAddressPage> <MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, unknown>}/> <AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, ResourceError>}/>
</MockAddressPage> </MockAddressPage>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
...@@ -86,7 +87,7 @@ test('validator +@mobile', async({ mount, page }) => { ...@@ -86,7 +87,7 @@ test('validator +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, unknown>}/> <AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, ResourceError>}/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
...@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address'; ...@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
...@@ -29,7 +30,7 @@ import AddressQrCode from './details/AddressQrCode'; ...@@ -29,7 +30,7 @@ import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
interface Props { interface Props {
addressQuery: UseQueryResult<TAddress>; addressQuery: UseQueryResult<TAddress, ResourceError>;
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
} }
...@@ -37,8 +38,10 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -37,8 +38,10 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const addressHash = router.query.id?.toString();
const countersQuery = useApiQuery('address_counters', { const countersQuery = useApiQuery('address_counters', {
pathParams: { id: router.query.id?.toString() }, pathParams: { id: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
}, },
...@@ -51,33 +54,54 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -51,33 +54,54 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
}, 500); }, 500);
}, [ scrollRef ]); }, [ scrollRef ]);
if (addressQuery.isError) { const errorData = React.useMemo(() => ({
hash: addressHash || '',
is_contract: false,
implementation_name: null,
implementation_address: null,
token: null,
watchlist_names: null,
creation_tx_hash: null,
block_number_balance_updated_at: null,
name: null,
exchange_rate: null,
coin_balance: null,
has_tokens: true,
has_token_transfers: true,
has_validated_blocks: false,
}), [ addressHash ]);
const is404Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 404;
const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422;
if (addressQuery.isError && is422Error) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error }); throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
} }
if (addressQuery.isLoading) { if (addressQuery.isError && !is404Error) {
return <AddressDetailsSkeleton/>; return <DataFetchAlert/>;
} }
if (addressQuery.isError) { if (addressQuery.isLoading) {
return <DataFetchAlert/>; return <AddressDetailsSkeleton/>;
} }
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address); const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const data = addressQuery.isError ? errorData : addressQuery.data;
return ( return (
<Box> <Box>
<Flex alignItems="center"> <Flex alignItems="center">
<AddressIcon address={ addressQuery.data }/> <AddressIcon address={ data }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }> <Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash } { isMobile ? <HashStringShorten hash={ data.hash }/> : data.hash }
</Text> </Text>
<CopyToClipboard text={ addressQuery.data.hash }/> <CopyToClipboard text={ data.hash }/>
{ addressQuery.data.is_contract && addressQuery.data.token && <AddressAddToMetaMask ml={ 2 } token={ addressQuery.data.token }/> } { data.is_contract && data.token && <AddressAddToMetaMask ml={ 2 } token={ data.token }/> }
{ !addressQuery.data.is_contract && ( { !data.is_contract && (
<AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/> <AddressFavoriteButton hash={ data.hash } isAdded={ Boolean(data.watchlist_names?.length) } ml={ 3 }/>
) } ) }
<AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/> <AddressQrCode hash={ data.hash } ml={ 2 }/>
</Flex> </Flex>
{ explorers.length > 0 && ( { explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap"> <Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
...@@ -94,69 +118,77 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -94,69 +118,77 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
rowGap={{ base: 1, lg: 3 }} rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden" templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
> >
<AddressNameInfo data={ addressQuery.data }/> <AddressNameInfo data={ data }/>
{ addressQuery.data.is_contract && addressQuery.data.creation_tx_hash && addressQuery.data.creator_address_hash && ( { data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem <DetailsInfoItem
title="Creator" title="Creator"
hint="Transaction and address of creation." hint="Transaction and address of creation."
> >
<AddressLink type="address" hash={ addressQuery.data.creator_address_hash } truncation="constant"/> <AddressLink type="address" hash={ 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={ data.creation_tx_hash } type="transaction" truncation="constant"/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ addressQuery.data.is_contract && addressQuery.data.implementation_address && ( { data.is_contract && data.implementation_address && (
<DetailsInfoItem <DetailsInfoItem
title="Implementation" title="Implementation"
hint="Implementation address of the proxy contract." hint="Implementation address of the proxy contract."
columnGap={ 1 } columnGap={ 1 }
> >
<Link href={ link('address_index', { id: addressQuery.data.implementation_address }) }>{ addressQuery.data.implementation_name }</Link> <Link href={ link('address_index', { id: data.implementation_address }) }>{ data.implementation_name }</Link>
<Text variant="secondary" overflow="hidden"> <Text variant="secondary" overflow="hidden">
<HashStringShortenDynamic hash={ `(${ addressQuery.data.implementation_address })` }/> <HashStringShortenDynamic hash={ `(${ data.implementation_address })` }/>
</Text> </Text>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<AddressBalance data={ addressQuery.data }/> <AddressBalance data={ data }/>
{ addressQuery.data.has_tokens && ( { data.has_tokens && (
<DetailsInfoItem <DetailsInfoItem
title="Tokens" title="Tokens"
hint="All tokens in the account and total value." hint="All tokens in the account and total value."
alignSelf="center" alignSelf="center"
py={ 0 } py={ 0 }
> >
<TokenSelect/> { addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsInfoItem <DetailsInfoItem
title="Transactions" title="Transactions"
hint="Number of transactions related to this address." hint="Number of transactions related to this address."
> >
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/> { addressQuery.data ?
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem> </DetailsInfoItem>
{ addressQuery.data.has_token_transfers && ( { data.has_token_transfers && (
<DetailsInfoItem <DetailsInfoItem
title="Transfers" title="Transfers"
hint="Number of transfers to/from this address." hint="Number of transfers to/from this address."
> >
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/> { addressQuery.data ?
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsInfoItem <DetailsInfoItem
title="Gas used" title="Gas used"
hint="Gas used by the address." hint="Gas used by the address."
> >
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/> { addressQuery.data ?
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem> </DetailsInfoItem>
{ addressQuery.data.has_validated_blocks && ( { data.has_validated_blocks && (
<DetailsInfoItem <DetailsInfoItem
title="Blocks validated" title="Blocks validated"
hint="Number of blocks validated by this validator." hint="Number of blocks validated by this validator."
> >
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/> { addressQuery.data ?
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ addressQuery.data.block_number_balance_updated_at && ( { data.block_number_balance_updated_at && (
<DetailsInfoItem <DetailsInfoItem
title="Last balance update" title="Last balance update"
hint="Block number in which the address was updated." hint="Block number in which the address was updated."
...@@ -164,12 +196,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -164,12 +196,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
py={{ base: '2px', lg: 1 }} py={{ base: '2px', lg: 1 }}
> >
<Link <Link
href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) } href={ link('block', { id: String(data.block_number_balance_updated_at) }) }
display="flex" display="flex"
alignItems="center" alignItems="center"
> >
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/> <Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/>
{ addressQuery.data.block_number_balance_updated_at } { data.block_number_balance_updated_at }
</Link> </Link>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
......
...@@ -13,7 +13,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; ...@@ -13,7 +13,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: Address; data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate'>;
} }
const AddressBalance = ({ data }: Props) => { const AddressBalance = ({ data }: Props) => {
...@@ -63,10 +63,6 @@ const AddressBalance = ({ data }: Props) => { ...@@ -63,10 +63,6 @@ const AddressBalance = ({ data }: Props) => {
handler: handleNewCoinBalanceMessage, handler: handleNewCoinBalanceMessage,
}); });
if (!data.coin_balance) {
return null;
}
return ( return (
<DetailsInfoItem <DetailsInfoItem
title="Balance" title="Balance"
...@@ -82,7 +78,7 @@ const AddressBalance = ({ data }: Props) => { ...@@ -82,7 +78,7 @@ const AddressBalance = ({ data }: Props) => {
fontSize="sm" fontSize="sm"
/> />
<CurrencyValue <CurrencyValue
value={ data.coin_balance } value={ data.coin_balance || '0' }
exchangeRate={ data.exchange_rate } exchangeRate={ data.exchange_rate }
decimals={ String(appConfig.network.currency.decimals) } decimals={ String(appConfig.network.currency.decimals) }
currency={ appConfig.network.currency.symbol } currency={ appConfig.network.currency.symbol }
......
...@@ -29,7 +29,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => { ...@@ -29,7 +29,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
const data = query.data?.[prop]; const data = query.data?.[prop];
if (query.isError || data === null || data === undefined) { if (query.isError || data === null || data === undefined) {
return <span>no data</span>; return <span>0</span>;
} }
switch (prop) { switch (prop) {
......
...@@ -7,7 +7,7 @@ import link from 'lib/link/link'; ...@@ -7,7 +7,7 @@ import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
interface Props { interface Props {
data: Address; data: Pick<Address, 'name' | 'token' | 'is_contract'>;
} }
const AddressNameInfo = ({ data }: Props) => { const AddressNameInfo = ({ data }: Props) => {
......
...@@ -17,7 +17,11 @@ import useSocketMessage from 'lib/socket/useSocketMessage'; ...@@ -17,7 +17,11 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import TokenSelectDesktop from './TokenSelectDesktop'; import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile'; import TokenSelectMobile from './TokenSelectMobile';
const TokenSelect = () => { interface Props {
onClick?: () => void;
}
const TokenSelect = ({ onClick }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -88,6 +92,7 @@ const TokenSelect = () => { ...@@ -88,6 +92,7 @@ const TokenSelect = () => {
pr="6px" pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> } icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a" as="a"
onClick={ onClick }
/> />
</NextLink> </NextLink>
</Box> </Box>
......
...@@ -122,6 +122,8 @@ const AddressPageContent = () => { ...@@ -122,6 +122,8 @@ const AddressPageContent = () => {
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null; const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
const content = addressQuery.isError ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>;
return ( return (
<Page> <Page>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
...@@ -138,7 +140,7 @@ const AddressPageContent = () => { ...@@ -138,7 +140,7 @@ const AddressPageContent = () => {
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> } { addressQuery.isLoading ? <SkeletonTabs/> : content }
</Page> </Page>
); );
}; };
......
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