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

Merge pull request #421 from blockscout/address-balance

address page: balance and name
parents 94ed0ff7 d9ecae7c
...@@ -7,6 +7,8 @@ SocketMessage.BlocksIndexStatus | ...@@ -7,6 +7,8 @@ SocketMessage.BlocksIndexStatus |
SocketMessage.TxStatusUpdate | SocketMessage.TxStatusUpdate |
SocketMessage.NewTx | SocketMessage.NewTx |
SocketMessage.NewPendingTx | SocketMessage.NewPendingTx |
SocketMessage.AddressBalance |
SocketMessage.AddressCurrentCoinBalance |
SocketMessage.AddressTokenBalance | SocketMessage.AddressTokenBalance |
SocketMessage.AddressCoinBalance | SocketMessage.AddressCoinBalance |
SocketMessage.Unknown; SocketMessage.Unknown;
...@@ -34,6 +36,9 @@ export namespace SocketMessage { ...@@ -34,6 +36,9 @@ export namespace SocketMessage {
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>; export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
export type AddressCurrentCoinBalance =
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>; export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', AddressCoinBalancePayload>; export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', AddressCoinBalancePayload>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
......
...@@ -15,7 +15,7 @@ export interface Address { ...@@ -15,7 +15,7 @@ export interface Address {
name: string | null; name: string | null;
private_tags: Array<AddressTag> | null; private_tags: Array<AddressTag> | null;
public_tags: Array<AddressTag> | null; public_tags: Array<AddressTag> | null;
tokenInfo: TokenInfo | null; token: TokenInfo | null;
watchlist_names: Array<WatchlistName> | null; watchlist_names: Array<WatchlistName> | null;
} }
......
import { Box, Flex, Text, Icon, Grid } from '@chakra-ui/react'; import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
...@@ -9,18 +9,23 @@ import type { Address as TAddress, AddressCounters, AddressTokenBalance } from ' ...@@ -9,18 +9,23 @@ import type { Address as TAddress, AddressCounters, AddressTokenBalance } from '
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import metamaskIcon from 'icons/metamask.svg'; import metamaskIcon from 'icons/metamask.svg';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink'; import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton'; import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressFavoriteButton from './details/AddressFavoriteButton'; import AddressFavoriteButton from './details/AddressFavoriteButton';
import AddressNameInfo from './details/AddressNameInfo';
import AddressQrCode from './details/AddressQrCode'; import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
...@@ -87,6 +92,18 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -87,6 +92,18 @@ const AddressDetails = ({ addressQuery }: Props) => {
rowGap={{ base: 3, lg: 3 }} rowGap={{ base: 3, 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 }/>
{ addressQuery.data.is_contract && addressQuery.data.creation_tx_hash && addressQuery.data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation."
>
<AddressLink hash={ addressQuery.data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at </Text>
<AddressLink hash={ addressQuery.data.creation_tx_hash } truncation="constant"/>
</DetailsInfoItem>
) }
<AddressBalance data={ addressQuery.data }/>
<DetailsInfoItem <DetailsInfoItem
title="Tokens" title="Tokens"
hint="All tokens in the account and total value." hint="All tokens in the account and total value."
...@@ -121,6 +138,23 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -121,6 +138,23 @@ const AddressDetails = ({ addressQuery }: Props) => {
{ validationsCount.toLocaleString() } { validationsCount.toLocaleString() }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ addressQuery.data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated."
alignSelf="center"
py={{ base: 0, lg: 1 }}
>
<Link
href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) }
display="flex"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/>
{ addressQuery.data.block_number_balance_updated_at }
</Link>
</DetailsInfoItem>
) }
</Grid> </Grid>
</Box> </Box>
); );
......
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import CurrencyValue from 'ui/shared/CurrencyValue';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: Address;
}
const AddressBalance = ({ data }: Props) => {
const [ lastBlockNumber, setLastBlockNumber ] = React.useState<number>(data.block_number_balance_updated_at || 0);
const queryClient = useQueryClient();
const updateData = React.useCallback((balance: string, exchangeRate: string, blockNumber: number) => {
if (blockNumber < lastBlockNumber) {
return;
}
setLastBlockNumber(blockNumber);
queryClient.setQueryData([ QueryKeys.address, data.hash ], (prevData: Address | undefined) => {
if (!prevData) {
return;
}
return {
...prevData,
coin_balance: balance,
exchange_rate: exchangeRate,
block_number_balance_updated_at: blockNumber,
};
});
}, [ data.hash, lastBlockNumber, queryClient ]);
const handleNewBalanceMessage: SocketMessage.AddressBalance['handler'] = React.useCallback((payload) => {
updateData(payload.balance, payload.exchange_rate, payload.block_number);
}, [ updateData ]);
const handleNewCoinBalanceMessage: SocketMessage.AddressCurrentCoinBalance['handler'] = React.useCallback((payload) => {
updateData(payload.coin_balance, payload.exchange_rate, payload.block_number);
}, [ updateData ]);
const channel = useSocketChannel({
topic: `addresses:${ data.hash.toLowerCase() }`,
isDisabled: !data.coin_balance,
});
useSocketMessage({
channel,
event: 'balance',
handler: handleNewBalanceMessage,
});
useSocketMessage({
channel,
event: 'current_coin_balance',
handler: handleNewCoinBalanceMessage,
});
if (!data.coin_balance) {
return null;
}
return (
<DetailsInfoItem
title="Balance"
hint={ `Address balance in ${ appConfig.network.currency.symbol }. Doesn't include ERC20, ERC721 and ERC1155 tokens.` }
>
<TokenLogo
hash={ appConfig.network.currency.address }
name={ appConfig.network.currency.name }
boxSize={ 5 }
mr={ 2 }
fontSize="sm"
/>
<CurrencyValue
value={ data.coin_balance }
exchangeRate={ data.exchange_rate }
decimals={ String(appConfig.network.currency.decimals) }
currency={ appConfig.network.currency.symbol }
accuracyUsd={ 2 }
accuracy={ 8 }
/>
</DetailsInfoItem>
);
};
export default React.memo(AddressBalance);
...@@ -18,10 +18,12 @@ const AddressDetailsSkeleton = () => { ...@@ -18,10 +18,12 @@ const AddressDetailsSkeleton = () => {
<Skeleton h={ 6 } w="80px"/> <Skeleton h={ 6 } w="80px"/>
</Flex> </Flex>
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '150px 1fr' }} maxW="1000px" mt={ 8 }> <Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '150px 1fr' }} maxW="1000px" mt={ 8 }>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="30%"/> <DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="10%"/> <DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/> <DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="20%"/> <DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="20%"/>
</Grid> </Grid>
</Box> </Box>
); );
......
import { Link } from '@chakra-ui/react';
import React from 'react';
import type { Address } from 'types/api/address';
import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
interface Props {
data: Address;
}
const AddressNameInfo = ({ data }: Props) => {
if (data.token) {
return (
<DetailsInfoItem
title="Token name"
hint="Token name and symbol"
>
<Link href={ link('token_index', { hash: data.token.address }) }>
{ data.token.name } ({ data.token.symbol })
</Link>
</DetailsInfoItem>
);
}
if (data.is_contract && data.name) {
return (
<DetailsInfoItem
title="Contract name"
hint="The name found in the source code of the Contract"
>
{ data.name }
</DetailsInfoItem>
);
}
if (data.name) {
return (
<DetailsInfoItem
title="Validator name"
hint="The name of the validator"
>
{ data.name }
</DetailsInfoItem>
);
}
return null;
};
export default React.memo(AddressNameInfo);
...@@ -21,13 +21,13 @@ const EmptyElement = ({ className, letter }: { className?: string; letter: strin ...@@ -21,13 +21,13 @@ const EmptyElement = ({ className, letter }: { className?: string; letter: strin
}; };
interface Props { interface Props {
hash: string; hash?: string;
name?: string | null; name?: string | null;
className?: string; className?: string;
} }
const TokenLogo = ({ hash, name, className }: Props) => { const TokenLogo = ({ hash, name, className }: Props) => {
const logoSrc = appConfig.network.assetsPathname ? [ const logoSrc = appConfig.network.assetsPathname && hash ? [
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/', 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/',
appConfig.network.assetsPathname, appConfig.network.assetsPathname,
'/assets/', '/assets/',
......
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