Commit a83b5bd8 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #171 from blockscout/txs-page

Txs page
parents 8c6c542b e44a371a
/* eslint-disable max-len */
export const tx = {
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
status: 'success',
status: 'success' as TxStatus,
block_num: 15006918,
confirmation_num: 283,
confirmation_duration: 30,
timestamp: 1662623567695,
address_from: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
address_to: '0x35317007D203b8a86CA727ad44E473E40450E378',
address_from: {
hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
type: 'Address',
alias: '',
},
address_to: {
hash: '0x35317007D203b8a86CA727ad44E473E40450E378',
type: 'Contract',
alias: '',
},
amount: {
value: 0.03,
value_usd: 35.5,
......@@ -39,4 +47,9 @@ export const tx = {
{ from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: 'USDT', amount: 192.7, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: 'TOKE', amount: 76.1851851851846, usd: 194.05 },
],
txType: 'transaction' as TxType,
};
export type TxType = 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
export type TxStatus = 'success' | 'failed' | 'pending';
......@@ -5,8 +5,8 @@ export const data = [
id: 1,
type: 'call' as TxInternalsType,
status: 'success' as const,
from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e',
from: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
to: { hash: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e' },
value: 0.25207646303,
gasLimit: 369472,
},
......@@ -14,17 +14,17 @@ export const data = [
id: 2,
type: 'delegate_call' as TxInternalsType,
status: 'success' as const,
from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
from: { hash: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
to: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
value: 0.5633333,
gasLimit: 340022,
},
{
id: 3,
type: 'static_call' as TxInternalsType,
status: 'error' as const,
from: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
to: '0x35317007D203b8a86CA727ad44E473E40450E378',
status: 'failed' as const,
from: { hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830' },
to: { hash: '0x35317007D203b8a86CA727ad44E473E40450E378' },
value: 0.421152366,
gasLimit: 509333,
},
......
import { tx } from './tx';
import type { TxType, TxStatus } from './tx';
export const txs = [
{
...tx,
method: 'Withdraw',
txType: 'transaction' as TxType,
errorText: '',
},
{
...tx,
status: 'failed' as TxStatus,
errorText: 'Error: (Awaiting internal transactions for reason)',
txType: 'contract-call' as TxType,
method: 'CommitHash CommitHash CommitHash CommitHash',
amount: {
value: 0.04,
value_usd: 35.5,
},
fee: {
value: 0.002295904453623692,
value_usd: 2.84,
},
},
{
...tx,
status: 'pending' as TxStatus,
txType: 'token-transfer' as TxType,
method: 'Multicall',
address_from: {
hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
alias: 'tkdkdkdkdkdkdkdkdkdkdkdkdkdkd.eth',
type: 'ENS name',
},
amount: {
value: 0.02,
value_usd: 35.5,
},
fee: {
value: 0.002495904453623692,
value_usd: 2.84,
},
errorText: '',
},
];
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.774 13.088a.936.936 0 0 0 0 1.325l2.814 2.812a.935.935 0 0 0 1.324 0l2.813-2.812A.935.935 0 1 0 8.4 13.088l-1.213 1.21v-9.95a.954.954 0 0 0-.938-.937.954.954 0 0 0-.937.938v9.95L4.1 13.088a.937.937 0 0 0-1.327 0ZM12.813 15.626a.937.937 0 1 0 1.875 0V5.702l1.213 1.21a.935.935 0 1 0 1.324-1.324l-2.812-2.813a.936.936 0 0 0-1.325 0l-2.812 2.813A.935.935 0 1 0 11.6 6.912l1.213-1.21v9.924Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="5" fill="#D9DBE0"/>
<circle cx="5" cy="5" r="2.5" fill="#707886"/>
</svg>
......@@ -21,7 +21,7 @@ export default function useNavItems() {
return React.useMemo(() => {
const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute === 'blocks' },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Transactions', url: link('txs_validated'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' },
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
......
......@@ -45,10 +45,14 @@ export const ROUTES = {
},
// TRANSACTIONS
txs: {
txs_validated: {
pattern: `${ BASE_PATH }/txs`,
crossNetworkNavigation: true,
},
txs_pending: {
pattern: `${ BASE_PATH }/pending-transactions`,
crossNetworkNavigation: true,
},
tx_index: {
pattern: `${ BASE_PATH }/tx/[id]`,
},
......@@ -70,6 +74,9 @@ export const ROUTES = {
pattern: `${ BASE_PATH }/blocks`,
crossNetworkNavigation: true,
},
block: {
pattern: `${ BASE_PATH }/block/[id]`,
},
// TOKENS
tokens: {
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams);
return (
<>
<Head><title>{ title }</title></Head>
<Transactions tab="txs_pending"/>
</>
);
};
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams);
return (
<>
<Head><title>{ title }</title></Head>
<Transactions tab="txs_validated"/>
</>
);
};
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
......@@ -53,6 +53,19 @@ const sizes = {
fontWeight: 500,
},
}),
xs: definePartsStyle({
th: {
px: '6px',
py: '10px',
fontSize: 'sm',
},
td: {
px: '6px',
py: 6,
fontSize: 'sm',
fontWeight: 500,
},
}),
};
const variants = {
......
export type Sort = 'val-desc' | 'val-asc' | 'fee-desc' | 'fee-asc' | undefined;
import { Box } from '@chakra-ui/react'; import React from 'react';
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
......
......@@ -33,7 +33,7 @@ const TransactionPageContent = ({ tab }: Props) => {
return (
<Page>
{ /* TODO should be shown only when navigating from txs list */ }
<Link mb={ 6 } display="inline-flex" href={ link('txs') }>
<Link mb={ 6 } display="inline-flex" href={ link('txs_validated') }>
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions
</Link>
......
import {
Box,
} from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxsPending from 'ui/txs/TxsPending';
import TxsValidated from 'ui/txs/TxsValidated';
const TABS: Array<RoutedTab> = [
{ routeName: 'txs_validated', title: 'Validated', component: <TxsValidated/> },
{ routeName: 'txs_pending', title: 'Pending', component: <TxsPending/> },
];
type Props = {
tab: RoutedTab['routeName'];
}
const Transactions = ({ tab }: Props) => {
return (
<Page>
<Box h="100%">
<PageHeader text="Transactions"/>
<RoutedTabs tabs={ TABS } defaultActiveTab={ tab }/>
</Box>
</Page>
);
};
export default Transactions;
// DEPRECATED
// migrate to separate components
// ui/shared/FilterButton.tsx + custom filter
// ui/shared/FilterInput.tsx
import { Flex, Icon, Button, Circle, InputGroup, InputLeftElement, Input, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React from 'react';
import filterIcon from 'icons/filter.svg';
import searchIcon from 'icons/search.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 }/>;
const Filters = () => {
const [ isActive, setIsActive ] = React.useState(false);
const [ value, setValue ] = React.useState('');
const handleClick = React.useCallback(() => {
setIsActive(flag => !flag);
}, []);
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
}, []);
const badgeColor = useColorModeValue('white', 'black');
const badgeBgColor = useColorModeValue('blue.700', 'gray.50');
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
return (
<Flex>
<Button
leftIcon={ FilterIcon }
rightIcon={ isActive ? <Circle bg={ badgeBgColor } size={ 5 } color={ badgeColor }>2</Circle> : undefined }
size="sm"
variant="outline"
colorScheme="gray-dark"
borderWidth="1px"
onClick={ handleClick }
isActive={ isActive }
px={ 1.5 }
>
Filter
</Button>
<InputGroup size="xs" ml={ 3 } maxW="360px">
<InputLeftElement ml={ 1 }>
<Icon as={ searchIcon } boxSize={ 5 } color={ searchIconColor }/>
</InputLeftElement>
<Input
paddingInlineStart="38px"
placeholder="Search by addresses, hash, method..."
ml="1px"
borderWidth="2px"
textOverflow="ellipsis"
onChange={ handleChange }
borderColor={ inputBorderColor }
value={ value }
size="xs"
/>
</InputGroup>
</Flex>
);
};
export default Filters;
import { Button, Flex, Input, Icon, IconButton } from '@chakra-ui/react';
import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg';
type Props = {
currentPage: number;
maxPage?: number;
isMobile?: boolean;
}
const MAX_PAGE_DEFAULT = 50;
const Pagination = ({ currentPage, maxPage, isMobile }: Props) => {
const pageNumber = (
<Flex alignItems="center">
<Button
variant="outline"
colorScheme="gray"
size="sm"
isActive
borderWidth="1px"
fontWeight={ 400 }
mr={ 3 }
h={ 8 }
>
{ currentPage }
</Button>
of
<Button
variant="outline"
colorScheme="gray"
size="sm"
width={ 8 }
borderWidth="1px"
fontWeight={ 400 }
ml={ 3 }
>
{ maxPage || MAX_PAGE_DEFAULT }
</Button>
</Flex>
);
if (isMobile) {
return (
<Flex
fontSize="sm"
width="100%"
justifyContent="space-between"
alignItems="center"
>
<IconButton
variant="outline"
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
/>
{ pageNumber }
<IconButton
variant="outline"
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
/>
</Flex>
);
}
return (
<Flex
fontSize="sm"
>
<Flex alignItems="center" justifyContent="space-between">
<Button
variant="outline"
size="sm"
aria-label="Next page"
leftIcon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 8 }
pl={ 1 }
>
Previous
</Button>
{ pageNumber }
<Button
variant="outline"
size="sm"
aria-label="Next page"
rightIcon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 8 }
pr={ 1 }
>
Next
</Button>
</Flex>
<Flex alignItems="center" width="132px" ml={ 16 }>
Go to <Input w="84px" size="xs" ml={ 2 }/>
</Flex>
</Flex>
);
};
export default Pagination;
import { Icon, IconButton } from '@chakra-ui/react';
import React from 'react';
import upDownArrow from 'icons/arrows/up-down.svg';
type Props = {
handleSort: () => void;
isSortActive: boolean;
}
const SortButton = ({ handleSort, isSortActive }: Props) => {
return (
<IconButton
icon={ <Icon as={ upDownArrow } boxSize={ 5 }/> }
aria-label="sort"
size="sm"
variant="outline"
colorScheme="gray-dark"
ml={ 2 }
minWidth="36px"
onClick={ handleSort }
isActive={ isSortActive }
/>
);
};
export default SortButton;
import { Tag, TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react';
import React from 'react';
import errorIcon from 'icons/status/error.svg';
import pendingIcon from 'icons/status/pending.svg';
import successIcon from 'icons/status/success.svg';
export interface Props {
status: 'success' | 'failed' | 'pending';
errorText?: string;
}
const TxStatus = ({ status, errorText }: Props) => {
let label;
let icon;
let colorScheme;
switch (status) {
case 'success':
label = 'Success';
icon = successIcon;
colorScheme = 'green';
break;
case 'failed':
label = 'Failed';
icon = errorIcon;
colorScheme = 'red';
break;
case 'pending':
label = 'Pending';
icon = pendingIcon;
// FIXME: it's not gray on mockups
// need to implement new color scheme or redefine colors here
colorScheme = 'gray';
break;
}
return (
<Tooltip label={ errorText }>
<Tag colorScheme={ colorScheme } display="inline-flex">
<TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel>
</Tag>
</Tooltip>
);
};
export default TxStatus;
import { Link, chakra, shouldForwardProp } from '@chakra-ui/react';
import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import React from 'react';
import useLink from 'lib/link/useLink';
......@@ -7,13 +7,14 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
type?: 'address' | 'transaction' | 'token';
alias?: string;
className?: string;
hash: string;
truncation?: 'constant' | 'dynamic'| 'none';
fontWeight?: string;
}
const AddressLink = ({ type, className, truncation = 'dynamic', hash, fontWeight }: Props) => {
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, fontWeight }: Props) => {
const link = useLink();
let url;
if (type === 'transaction') {
......@@ -25,6 +26,13 @@ const AddressLink = ({ type, className, truncation = 'dynamic', hash, fontWeight
}
const content = (() => {
if (alias) {
return (
<Tooltip label={ hash }>
<Box overflow="hidden" textOverflow="ellipsis">{ alias }</Box>
</Tooltip>
);
}
switch (truncation) {
case 'constant':
return <HashStringShorten hash={ hash }/>;
......
......@@ -18,11 +18,11 @@ import PrevNext from 'ui/shared/PrevNext';
import RawInputData from 'ui/shared/RawInputData';
import TextSeparator from 'ui/shared/TextSeparator';
import Token from 'ui/shared/Token';
import type { Props as TxStatusProps } from 'ui/shared/TxStatus';
import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization';
import TokenTransfer from 'ui/tx/TokenTransfer';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import type { Props as TxStatusProps } from 'ui/tx/TxStatus';
import TxStatus from 'ui/tx/TxStatus';
const TxDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
......@@ -82,9 +82,9 @@ const TxDetails = () => {
hint="Address (external or contract) sending the transaction."
>
<Address>
<AddressIcon hash={ tx.address_from }/>
<AddressLink ml={ 2 } hash={ tx.address_from }/>
<CopyToClipboard text={ tx.address_from }/>
<AddressIcon hash={ tx.address_from.hash }/>
<AddressLink ml={ 2 } hash={ tx.address_from.hash }/>
<CopyToClipboard text={ tx.address_from.hash }/>
</Address>
</DetailsInfoItem>
<DetailsInfoItem
......@@ -93,9 +93,9 @@ const TxDetails = () => {
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
>
<Address mr={ 3 }>
<AddressIcon hash={ tx.address_to }/>
<AddressLink ml={ 2 } hash={ tx.address_to }/>
<CopyToClipboard text={ tx.address_to }/>
<AddressIcon hash={ tx.address_to.hash }/>
<AddressLink ml={ 2 } hash={ tx.address_to.hash }/>
<CopyToClipboard text={ tx.address_to.hash }/>
</Address>
<Tag colorScheme="orange" variant="solid" flexShrink={ 0 }>SANA</Tag>
<Tooltip label="Contract execution completed">
......
......@@ -16,8 +16,8 @@ import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase();
return item.type.toLowerCase().includes(formattedSearchTerm) ||
item.from.toLowerCase().includes(formattedSearchTerm) ||
item.to.toLowerCase().includes(formattedSearchTerm);
item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
item.to.hash.toLowerCase().includes(formattedSearchTerm);
};
const TxInternals = () => {
......
import { Tag, TagLabel, TagLeftIcon } from '@chakra-ui/react';
import React from 'react';
import errorIcon from 'icons/status/error.svg';
import successIcon from 'icons/status/success.svg';
export interface Props {
status: 'success' | 'error';
}
const TxStatus = ({ status }: Props) => {
const label = status === 'success' ? 'Success' : 'Error';
const icon = status === 'success' ? successIcon : errorIcon;
const colorScheme = status === 'success' ? 'green' : 'red';
return (
<Tag colorScheme={ colorScheme } display="inline-flex">
<TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel>
</Tag>
);
};
export default TxStatus;
......@@ -10,7 +10,7 @@ import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/tx/TxStatus';
import TxStatus from 'ui/shared/TxStatus';
type Props = ArrayElement<typeof data>;
......@@ -23,13 +23,13 @@ const TxInternalsListItem = ({ type, status, from, to, value, gasLimit }: Props)
</Flex>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from }/>
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to }/>
<AddressIcon hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash }/>
</Address>
</Box>
<HStack spacing={ 3 }>
......
......@@ -5,14 +5,14 @@ import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
import TxStatus from 'ui/tx/TxStatus';
interface Props {
type: string;
status: 'success' | 'error';
from: string;
to: string;
status: 'success' | 'failed' | 'pending';
from: { hash: string; alias?: string};
to: { hash: string; alias?: string};
value: number;
gasLimit: number;
}
......@@ -32,8 +32,8 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props)
</Td>
<Td>
<Address>
<AddressIcon hash={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from } flexGrow={ 1 }/>
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.alias } flexGrow={ 1 }/>
</Address>
</Td>
<Td px={ 0 }>
......@@ -41,8 +41,8 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props)
</Td>
<Td>
<Address>
<AddressIcon hash={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to }/>
<AddressIcon hash={ to.hash }/>
<AddressLink hash={ to.hash } alias={ to.alias } fontWeight="500" ml={ 2 }/>
</Address>
</Td>
<Td isNumeric>
......
import { Box, Heading, Text, Flex, Link, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { txs } from 'data/txs';
import useLink from 'lib/link/useLink';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization';
const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
const sectionBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const sectionProps = {
borderBottom: '1px solid',
borderColor: sectionBorderColor,
paddingBottom: 4,
};
const sectionTitleProps = {
color: 'gray.500',
fontWeight: 600,
marginBottom: 3,
};
const link = useLink();
return (
<>
<Heading as="h4" fontSize="18px" mb={ 6 }>Additional info </Heading>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Transaction fee</Text>
<Flex>
<Text>{ tx.fee.value } Ether</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
</Flex>
</Box>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas limit & usage by transaction</Text>
<Flex>
<Text>{ tx.gas_used.toLocaleString('en') }</Text>
<TextSeparator/>
<Text>{ tx.gas_limit.toLocaleString('en') }</Text>
<Utilization ml={ 4 } value={ tx.gas_used / tx.gas_limit }/>
</Flex>
</Box>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas fees (Gwei)</Text>
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.base }</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max }</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max_priority }</Text>
</Box>
</Box>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Others</Text>
<Box>
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ tx.type.value }</Text>
<Text fontWeight="400" as="span" ml={ 1 }>({ tx.type.eip })</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ tx.nonce }</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ tx.position }</Text>
</Box>
</Box>
<Link href={ link('tx_index', { id: tx.hash }) }>More details</Link>
</>
);
};
export default TxAdditionalInfo;
import {
Icon,
Center,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import infoIcon from 'icons/info.svg';
const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?: () => void}, ref: React.ForwardedRef<HTMLDivElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600');
const infoColor = useColorModeValue('blue.600', 'blue.300');
return (
<Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="30px" h="30px" onClick={ onClick }>
<Icon
as={ infoIcon }
boxSize={ 5 }
color={ infoColor }
_hover={{ color: 'blue.400' }}
/>
</Center>
);
};
export default React.forwardRef(TxAdditionalInfoButton);
import { Tag, TagLabel } from '@chakra-ui/react';
import React from 'react';
export interface Props {
type: 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
}
const TxStatus = ({ type }: Props) => {
let label;
let colorScheme;
switch (type) {
case 'contract-call':
label = 'Contract call';
colorScheme = 'blue';
break;
case 'transaction':
label = 'Transaction';
colorScheme = 'purple';
break;
case 'token-transfer':
label = 'Token transfer';
colorScheme = 'orange';
break;
case 'internal-tx':
label = 'Internal txn';
colorScheme = 'cyan';
break;
case 'multicall':
label = 'Multicall';
colorScheme = 'teal';
break;
}
return (
<Tag colorScheme={ colorScheme }>
<TagLabel>{ label }</TagLabel>
</Tag>
);
};
export default TxStatus;
import { Box, HStack } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import type { Sort } from 'types/client/txs-sort';
import { txs } from 'data/txs';
import useIsMobile from 'lib/hooks/useIsMobile';
import FilterButton from 'ui/shared/FilterButton';
import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination';
import SortButton from 'ui/shared/SortButton';
import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
type Props = {
isPending?: boolean;
}
const TxsContent = ({ isPending }: Props) => {
const isMobile = useIsMobile();
const [ sorting, setSorting ] = useState<Sort>();
const [ sortedTxs, setSortedTxs ] = useState(txs);
// sorting should be preserved with pagination!
const sort = useCallback((field: 'val' | 'fee') => () => {
if (field === 'val') {
setSorting((prevVal => {
if (prevVal === 'val-asc') {
return undefined;
}
if (prevVal === 'val-desc') {
return 'val-asc';
}
return 'val-desc';
}));
}
if (field === 'fee') {
setSorting((prevVal => {
if (prevVal === 'fee-asc') {
return undefined;
}
if (prevVal === 'fee-desc') {
return 'fee-asc';
}
return 'fee-desc';
}));
}
}, []);
useEffect(() => {
switch (sorting) {
case 'val-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx1.amount.value - tx2.amount.value));
break;
case 'val-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx2.amount.value - tx1.amount.value));
break;
case 'fee-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx1.fee.value - tx2.fee.value));
break;
case 'fee-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx2.fee.value - tx1.fee.value));
break;
default:
setSortedTxs(txs);
}
}, [ sorting ]);
return (
<>
{ !isPending && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> }
<HStack mb={ 6 }>
{ /* TODO */ }
<FilterButton
isActive={ false }
isCollapsed={ isMobile }
// eslint-disable-next-line react/jsx-no-bind
onClick={ () => {} }
appliedFiltersNum={ 0 }
/>
<SortButton
// eslint-disable-next-line react/jsx-no-bind
handleSort={ () => {} }
isSortActive={ Boolean(sorting) }
/>
<FilterInput
// eslint-disable-next-line react/jsx-no-bind
onChange={ () => {} }
maxW="360px"
size="xs"
placeholder="Search by addresses, hash, method..."
/>
</HStack>
{ isMobile ?
sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) :
<TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/> }
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 } isMobile={ isMobile }/>
</Box>
</>
);
};
export default TxsContent;
import {
HStack,
Box,
Flex,
Icon,
Link,
Modal,
ModalContent,
ModalCloseButton,
Text,
useColorModeValue,
useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import useLink from 'lib/link/useLink';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const link = useLink();
return (
<>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor: { borderColor } }}>
<Flex justifyContent="space-between" mt={ 4 }>
<HStack>
<TxType type={ tx.txType }/>
<TxStatus status={ tx.status } errorText={ tx.errorText }/>
</HStack>
<TxAdditionalInfoButton onClick={ onOpen }/>
</Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }>
<Flex>
<Icon
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
<Address width="100%">
<AddressLink
hash={ tx.hash }
type="transaction"
fontWeight="700"
truncation="constant"
/>
</Address>
</Flex>
<Text variant="secondary" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</Flex>
<Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text>
<Text
as="span"
variant="secondary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ tx.method }
</Text>
</Flex>
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block_num.toString() }) }>{ tx.block_num }</Link>
</Box>
<Flex alignItems="center" height={ 6 } mt={ 6 }>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.address_from.hash }/>
<AddressLink
hash={ tx.address_from.hash }
alias={ tx.address_from.alias }
fontWeight="500"
ml={ 2 }
/>
</Address>
<Icon
as={ rightArrowIcon }
boxSize={ 6 }
mx={ 2 }
color="gray.500"
/>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.address_to.hash }/>
<AddressLink
hash={ tx.address_to.hash }
alias={ tx.address_to.alias }
fontWeight="500"
ml={ 2 }
/>
</Address>
</Flex>
<Box mt={ 2 }>
<Text as="span">Value xDAI </Text>
<Text as="span" variant="secondary">{ tx.amount.value.toFixed(8) }</Text>
</Box>
<Box mt={ 2 } mb={ 3 }>
<Text as="span">Fee xDAI </Text>
<Text as="span" variant="secondary">{ tx.fee.value.toFixed(8) }</Text>
</Box>
</Box>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent paddingTop={ 4 }>
<ModalCloseButton/>
<TxAdditionalInfo tx={ tx }/>
</ModalContent>
</Modal>
</>
);
};
export default TxsListItem;
import React from 'react';
import TxsContent from './TxsContent';
const TxsPending = () => {
return <TxsContent isPending/>;
};
export default TxsPending;
import { Link, Table, Thead, Tbody, Tr, Th, TableContainer, Icon } from '@chakra-ui/react';
import React from 'react';
import type { Sort } from 'types/client/txs-sort';
import type { txs as data } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import TxsTableItem from './TxsTableItem';
const CURRENCY = 'xDAI';
type Props = {
txs: typeof data;
sort: (field: 'val' | 'fee') => () => void;
sorting: Sort;
}
const TxsTable = ({ txs, sort, sorting }: Props) => {
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="810px" size="xs">
<Thead>
<Tr>
<Th width="54px"></Th>
<Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th>
<Th width="15%">Method</Th>
<Th width="11%">Block</Th>
<Th width={{ xl: '128px', base: '58px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '58px' }}>To</Th>
<Th width="18%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ CURRENCY }` }
</Link>
</Th>
<Th width="18%" isNumeric pr={ 5 }>
<Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Fee ${ CURRENCY }` }
</Link>
</Th>
</Tr>
</Thead>
<Tbody>
{ txs.map((item) => (
<TxsTableItem
key={ item.hash }
tx={ item }
/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default TxsTable;
import {
Box,
Tr,
Td,
Tag,
Link,
Icon,
VStack,
Text,
Tooltip,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Portal,
useBreakpointValue,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import useLink from 'lib/link/useLink';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from './TxType';
const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const link = useLink();
const isLargeScreen = useBreakpointValue({ base: false, xl: true });
const addressFrom = (
<Address>
<Tooltip label={ tx.address_from.type }>
<Box display="flex"><AddressIcon hash={ tx.address_from.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.address_from.hash } alias={ tx.address_from.alias } fontWeight="500" ml={ 2 }/>
</Address>
);
const addressTo = (
<Address>
<Tooltip label={ tx.address_to.type }>
<Box display="flex"> <AddressIcon hash={ tx.address_to.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.address_to.hash } alias={ tx.address_to.alias } fontWeight="500" ml={ 2 }/>
</Address>
);
const infoBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
return (
<Tr>
<Td pl={ 4 }>
<Popover placement="right-start" openDelay={ 300 }>
{ ({ isOpen }) => (
<>
<PopoverTrigger>
<TxAdditionalInfoButton isOpen={ isOpen }/>
</PopoverTrigger>
<Portal>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverBody>
<TxAdditionalInfo tx={ tx }/>
</PopoverBody>
</PopoverContent>
</Portal>
</>
) }
</Popover>
</Td>
<Td>
<VStack alignItems="start">
<TxType type={ tx.txType }/>
<TxStatus status={ tx.status } errorText={ tx.errorText }/>
</VStack>
</Td>
<Td>
<VStack alignItems="start" lineHeight="24px">
<Address width="100%">
<AddressLink
hash={ tx.hash }
type="transaction"
fontWeight="700"
/>
</Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</VStack>
</Td>
<Td>
<TruncatedTextTooltip label={ tx.method }>
<Tag
colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }
>
{ tx.method }
</Tag>
</TruncatedTextTooltip>
</Td>
<Td>
<Link href={ link('block', { id: tx.block_num.toString() }) }>{ tx.block_num }</Link>
</Td>
{ isLargeScreen ? (
<>
<Td>
{ addressFrom }
</Td>
<Td>
<Icon as={ rightArrowIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
</Td>
<Td>
{ addressTo }
</Td>
</>
) : (
<Td colSpan={ 3 }>
<Box>
{ addressFrom }
<Icon
as={ rightArrowIcon }
boxSize={ 6 }
mt={ 2 }
mb={ 1 }
color="gray.500"
transform="rotate(90deg)"
/>
{ addressTo }
</Box>
</Td>
) }
<Td isNumeric>
{ tx.amount.value.toFixed(8) }
</Td>
<Td isNumeric>
{ tx.fee.value.toFixed(8) }
</Td>
</Tr>
);
};
export default TxsTableItem;
import React from 'react';
import TxsContent from './TxsContent';
const TxsValidated = () => {
return <TxsContent/>;
};
export default TxsValidated;
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