Commit b2b7616c authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tx-page-api

parents 9c45721c cadf8340
......@@ -45,7 +45,7 @@ export const tx = {
input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b',
transferred_tokens: [
{ from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: { symbol: 'VIK', hash: '0xADFE00d92e5A16e773891F59780e6e54f40B532e', name: 'Viktor Coin' }, amount: 192.7, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: { symbol: 'PAO', hash: '0xC98a06220239818B086CD96756d4E3bC41EC848E', name: 'POA Candy' }, amount: 76.1851851851846, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: { symbol: 'PAO', hash: '0xC98a06220239818B086CD96756d4E3bC41EC848f', name: 'POA Candy' }, amount: 76.1851851851846, usd: 194.05 },
],
txType: 'transaction' as TxType,
};
......
......@@ -36,37 +36,16 @@ dayjs.updateLocale('en', {
ss: '%d secs',
future: 'in %s',
past: '%s ago',
m: 'a min',
m: '1 min',
mm: '%d mins',
h: 'an hour',
hh: '%d hours',
d: 'a day',
dd: '%d days',
M: 'a month',
MM: '%d months',
y: 'a year',
yy: '%d years',
},
});
dayjs.locale('en-short', {
name: 'en-short',
relativeTime: {
s: '1s',
future: 'in %s',
past: '%s ago',
m: '1m',
mm: '%dm',
h: '1h',
hh: '%dh',
d: '1d',
dd: '%dd',
M: '1mo',
MM: '%dmo',
y: '1y',
yy: '%dy',
// have to trick typescript 🎩 🐇
...{ ss: '%ds' },
h: '1 h',
hh: '%d h',
d: '1 d',
dd: '%d d',
M: '1 mo',
MM: '%d mo',
y: '1 y',
yy: '%d y',
},
});
......
......@@ -35,7 +35,7 @@ const BlocksListItem = ({ data, isPending }: Props) => {
{ data.height }
</Link>
</Flex>
<Text variant="secondary"fontWeight={ 400 }>{ dayjs(data.timestamp).locale('en-short').fromNow() }</Text>
<Text variant="secondary"fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text>
......
......@@ -33,7 +33,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => {
{ data.height }
</Link>
</Flex>
<Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).locale('en-short').fromNow() }</Text>
<Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
</Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td>
<Td fontSize="sm">
......
......@@ -38,11 +38,21 @@ const TransactionPageContent = ({ tab }: Props) => {
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions
</Link>
<PageTitle text="Transaction details"/>
<Flex marginLeft="auto" alignItems="center" flexWrap="wrap" columnGap={ 6 } rowGap={ 3 } mb={ 6 }>
<ExternalLink title="Open in Tenderly" href="#"/>
<ExternalLink title="Open in Blockchair" href="#"/>
<ExternalLink title="Open in Etherscan" href="#"/>
<Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}>
<PageTitle text="Transaction details"/>
<Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
ml={{ base: 'initial', lg: 'auto' }}
mb={{ base: 6, lg: 'initial' }}
py={ 2.5 }
>
<ExternalLink title="Open in Tenderly" href="#"/>
<ExternalLink title="Open in Blockchair" href="#"/>
<ExternalLink title="Open in Etherscan" href="#"/>
</Flex>
</Flex>
<RoutedTabs
tabs={ TABS }
......
......@@ -241,7 +241,7 @@ const TxDetails = () => {
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ data.type }</Text>
{ /* todo_tom waiting for Nikita's reply */ }
{ /* <Text fontWeight="400" as="span" ml={ 1 }>({ tx.type.eip })</Text> */ }
{ /* <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">({ tx.type.eip })</Text> */ }
<TextSeparator/>
</Box>
) }
......
......@@ -12,6 +12,46 @@ import FilterInput from 'ui/shared/FilterInput';
import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
import type { Sort, SortField } from 'ui/tx/internals/utils';
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
value: [ 'value-desc', 'value-asc', undefined ],
'gas-limit': [ 'gas-limit-desc', 'gas-limit-asc', undefined ],
};
const getNextSortValue = (field: SortField) => (prevValue: Sort | undefined) => {
const sequence = SORT_SEQUENCE[field];
const curIndex = sequence.findIndex((sort) => sort === prevValue);
const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1;
return sequence[nextIndex];
};
const sortFn = (sort: Sort | undefined) => (a: ArrayElement<typeof data>, b: ArrayElement<typeof data>) => {
switch (sort) {
case 'value-desc': {
const result = a.value > b.value ? -1 : 1;
return a.value === b.value ? 0 : result;
}
case 'value-asc': {
const result = a.value > b.value ? 1 : -1;
return a.value === b.value ? 0 : result;
}
case 'gas-limit-desc': {
const result = a.gasLimit > b.gasLimit ? -1 : 1;
return a.gasLimit === b.gasLimit ? 0 : result;
}
case 'gas-limit-asc': {
const result = a.gasLimit > b.gasLimit ? 1 : -1;
return a.gasLimit === b.gasLimit ? 0 : result;
}
default:
return 0;
}
};
const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase();
......@@ -23,22 +63,33 @@ const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): bool
const TxInternals = () => {
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>();
const isMobile = useIsMobile();
const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
setFilters(nextValue);
}, []);
const handleSortToggle = React.useCallback((field: SortField) => {
return () => {
setSort(getNextSortValue(field));
};
}, []);
const content = (() => {
const filteredData = data
.filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
.filter(searchFn(searchTerm));
.filter(searchFn(searchTerm))
.sort(sortFn(sort));
if (filteredData.length === 0) {
return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
}
return isMobile ? <TxInternalsList data={ filteredData }/> : <TxInternalsTable data={ filteredData }/>;
return isMobile ?
<TxInternalsList data={ filteredData }/> :
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle }/>;
})();
return (
......
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import { Table, Thead, Tbody, Tr, Th, TableContainer, Link, Icon } from '@chakra-ui/react';
import React from 'react';
import type { data as txData } from 'data/txInternal';
import arrowIcon from 'icons/arrows/east.svg';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { Sort, SortField } from 'ui/tx/internals/utils';
const TxInternalsTable = ({ data }: { data: typeof txData}) => {
interface Props {
data: typeof txData;
sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void;
}
const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
const selectedNetwork = useNetwork();
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return (
<TableContainer width="100%" mt={ 6 }>
......@@ -17,8 +26,18 @@ const TxInternalsTable = ({ data }: { data: typeof txData}) => {
<Th width="20%">From</Th>
<Th width="24px" px={ 0 }/>
<Th width="20%">To</Th>
<Th width="16%" isNumeric>Value { selectedNetwork?.currency }</Th>
<Th width="16%" isNumeric>Gas limit</Th>
<Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Value { selectedNetwork?.currency }
</Link>
</Th>
<Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }>
{ sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Gas limit
</Link>
</Th>
</Tr>
</Thead>
<Tbody>
......
import type { TxInternalsType } from 'types/api/tx';
export type Sort = 'value-asc' | 'value-desc' | 'gas-limit-asc' | 'gas-limit-desc';
export type SortField = 'value' | 'gas-limit';
interface TxInternalsTypeItem {
title: string;
id: TxInternalsType;
......
import { AccordionItem, AccordionButton, AccordionIcon, Button, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react';
import { AccordionItem, AccordionButton, AccordionIcon, Button, Box, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
......@@ -10,7 +10,6 @@ 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 TextSeparator from 'ui/shared/TextSeparator';
import TxStateStorageItem from './TxStateStorageItem';
......@@ -23,18 +22,14 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
return (
<AccountListItemMobile>
<AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column" rowGap={ 3 }>
<AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column">
{ ({ isExpanded }) => (
<>
<Flex>
<Address flexGrow={ 1 }>
<AddressIcon hash={ address }/>
<AddressLink hash={ address } fontWeight="500" ml={ 2 }/>
</Address>
<Flex mb={ 6 }>
<AccordionButton
_hover={{ background: 'unset' }}
padding="0"
ml={ 4 }
mr={ 5 }
w="auto"
>
<Button
......@@ -54,36 +49,47 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
</Button>
<AccordionIcon color="blue.600" width="30px"/>
</AccordionButton>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>Miner</Text>
<Link>{ miner }</Link>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>Before</Text>
<Flex>
<Text>{ before.balance } { selectedNetwork?.currency }</Text>
<TextSeparator/>
{ typeof before.nonce !== 'undefined' && <Text>Nonce:{ nbsp }{ before.nonce }</Text> }
</Flex>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>After</Text>
<Text>{ after.balance } { selectedNetwork?.currency }</Text>
{ typeof after.nonce !== 'undefined' && <Text>Nonce:{ nbsp }{ after.nonce }</Text> }
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>State difference</Text>
<Stat>
{ diff } { selectedNetwork?.currency }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
<Address flexGrow={ 1 }>
<AddressIcon hash={ address }/>
<AddressLink hash={ address } ml={ 2 }/>
</Address>
</Flex>
{ hasStorageData && (
<AccordionPanel fontWeight={ 500 } p={ 0 }>
{ storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel>
) }
<Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }>
<Box>
<Text as="span">Miner </Text>
<Link>{ miner }</Link>
</Box>
<Box>
<Text as="span">Before { selectedNetwork?.currency } </Text>
<Text as="span" variant="secondary">{ before.balance }</Text>
</Box>
{ typeof before.nonce !== 'undefined' && (
<Box>
<Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ before.nonce }</Text>
</Box>
) }
<Box>
<Text as="span">After { selectedNetwork?.currency } </Text>
<Text as="span" variant="secondary">{ after.balance }</Text>
</Box>
{ typeof after.nonce !== 'undefined' && (
<Box>
<Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text>
</Box>
) }
<Text>State difference { selectedNetwork?.currency }</Text>
<Stat>
{ diff }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Flex>
</>
) }
</AccordionItem>
......
......@@ -8,6 +8,7 @@ import {
import React from 'react';
import type { TTxStateItemStorage } from 'data/txState';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) => {
const gridData = [
......@@ -21,20 +22,27 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ];
return (
<Grid
gridTemplateColumns={{ base: '70px minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}
columnGap={ 3 }
rowGap={ 4 }
px={ 6 }
py={ 4 }
background="blackAlpha.50"
rowGap={{ base: 2.5, lg: 4 }}
px={{ base: 3, lg: 6 }}
py={{ base: 3, lg: 4 }}
backgroundColor={ useColorModeValue('blackAlpha.50', 'whiteAlpha.100') }
borderRadius="12px"
mb={ 4 }
fontSize="sm"
>
{ gridData.map((item) => (
<React.Fragment key={ item.name }>
<GridItem alignSelf={{ base: 'start', lg: 'center' }} fontWeight={{ base: 500, lg: 600 }} textAlign="end">{ item.name }</GridItem>
<GridItem display="flex" flexDir={{ base: 'column', lg: 'row' }} rowGap={ 2 } alignItems={{ base: 'flex-start', lg: 'center' }} >
<GridItem
alignSelf="center"
fontWeight={ 600 }
textAlign={{ base: 'start', lg: 'end' }}
_notFirst={{ mt: { base: 0.5, lg: 0 } }}
>
{ item.name }
</GridItem>
<GridItem display="flex" flexDir="row" columnGap={ 3 } alignItems="center" >
{ item.select && (
<Select
size="sm"
......@@ -42,14 +50,14 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
focusBorderColor="none"
display="inline-block"
w="auto"
mr={ 3 }
flexShrink={ 0 }
background={ backgroundColor }
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
) }
<Box fontWeight={{ base: 400, lg: 500 }} maxW="100%">
{ item.value }
<Box fontWeight={ 500 } whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic fontWeight="500" hash={ item.value }/>
</Box>
</GridItem>
</React.Fragment>
......
......@@ -17,7 +17,7 @@ const TxStateTable = () => {
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm">
<Table variant="simple" minWidth="950px" size="sm" w="auto">
<Thead>
<Tr>
<Th width="92px">Storage</Th>
......
......@@ -13,7 +13,7 @@ const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?
const infoColor = useColorModeValue('blue.600', 'blue.300');
return (
<Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="24px" h="24px" onClick={ onClick }>
<Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="24px" h="24px" onClick={ onClick } cursor="pointer">
<Icon
as={ infoIcon }
boxSize={ 5 }
......
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