Commit 58df71fe authored by tom's avatar tom

base implementation

parent edad6594
...@@ -36,6 +36,7 @@ import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } fro ...@@ -36,6 +36,7 @@ import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } fro
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type ArrayElement from 'types/utils/ArrayElement'; import type ArrayElement from 'types/utils/ArrayElement';
...@@ -158,6 +159,10 @@ export const RESOURCES = { ...@@ -158,6 +159,10 @@ export const RESOURCES = {
path: '/api/v2/transactions/:hash/raw-trace', path: '/api/v2/transactions/:hash/raw-trace',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
}, },
tx_state_changes: {
path: '/api/v2/transactions/:hash/state-changes',
pathParams: [ 'hash' as const ],
},
// ADDRESSES // ADDRESSES
addresses: { addresses: {
...@@ -449,6 +454,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse : ...@@ -449,6 +454,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx : Q extends 'tx_logs' ? LogsResponseTx :
Q extends 'tx_token_transfers' ? TokenTransferResponse : Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse : Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'tx_state_changes' ? TxStateChanges :
Q extends 'addresses' ? AddressesResponse : Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address : Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters : Q extends 'address_counters' ? AddressCounters :
......
...@@ -31,6 +31,8 @@ export type TokenTransfer = ( ...@@ -31,6 +31,8 @@ export type TokenTransfer = (
} }
) & TokenTransferBase ) & TokenTransferBase
export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload | Array<Erc1155TotalPayload>;
interface TokenTransferBase { interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting'; type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
tx_hash: string; tx_hash: string;
......
import type { AddressParam } from './addressParams';
import type { TokenInfo } from './token';
import type { TokenTotal } from './tokenTransfer';
export type TxStateChange = (TxStateChangeCoin | TxStateChangeToken) & {
address: AddressParam;
token: TokenInfo | null;
is_miner: boolean;
balance_before: string | null;
balance_after: string | null;
storage: Array<unknown> | null;
}
export interface TxStateChangeCoin {
type: 'coin';
change: string;
}
export interface TxStateChangeToken {
type: 'token';
change: {
direction: 'from' | 'to';
total: TokenTotal;
};
}
export type TxStateChanges = Array<TxStateChange>;
...@@ -17,16 +17,15 @@ import TxDetails from 'ui/tx/TxDetails'; ...@@ -17,16 +17,15 @@ import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace'; import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
// import TxState from 'ui/tx/TxState';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> }, { id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> }, { id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
{ id: 'internal', title: 'Internal txns', component: <TxInternals/> }, { id: 'internal', title: 'Internal txns', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> }, { id: 'logs', title: 'Logs', component: <TxLogs/> },
// will be implemented later, api is not ready { id: 'state', title: 'State', component: <TxState/> },
// { id: 'state', title: 'State', component: <TxState/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> }, { id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
]; ];
......
import { Accordion, Text } from '@chakra-ui/react'; import { Accordion, Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TxStateList from 'ui/tx/state/TxStateList'; import TxStateList from 'ui/tx/state/TxStateList';
import TxStateTable from 'ui/tx/state/TxStateTable'; import TxStateTable from 'ui/tx/state/TxStateTable';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
import TxPendingAlert from './TxPendingAlert';
import TxSocketAlert from './TxSocketAlert';
const TxState = () => { const TxState = () => {
const isMobile = useIsMobile(); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const list = isMobile ? <TxStateList/> : <TxStateTable/>; const { data, isLoading, isError } = useApiQuery('tx_state_changes', {
pathParams: { hash: txInfo.data?.hash },
queryOptions: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
},
});
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
if (isError || txInfo.isError) {
return <DataFetchAlert/>;
}
if (isLoading || txInfo.isLoading) {
return (
<>
<Show below="lg" ssr={ false }>
<Skeleton h={ 4 } borderRadius="full" w="100%"/>
<Skeleton h={ 4 } borderRadius="full" w="100%" mt={ 2 }/>
<Skeleton h={ 4 } borderRadius="full" w="100%" mt={ 2 }/>
<Skeleton h={ 4 } borderRadius="full" w="50%" mt={ 2 } mb={ 6 }/>
<SkeletonList/>
</Show>
<Hide below="lg" ssr={ false }>
<Skeleton h={ 6 } borderRadius="full" w="90%" mb={ 6 }/>
<SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
</Hide>
</>
);
}
return ( return (
<> <>
...@@ -15,7 +54,12 @@ const TxState = () => { ...@@ -15,7 +54,12 @@ const TxState = () => {
A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes
</Text> </Text>
<Accordion allowMultiple defaultIndex={ [] }> <Accordion allowMultiple defaultIndex={ [] }>
{ list } <Hide below="lg" ssr={ false }>
<TxStateTable data={ data }/>
</Hide>
<Show below="lg" ssr={ false }>
<TxStateList data={ data }/>
</Show>
</Accordion> </Accordion>
</> </>
); );
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { data } from 'data/txState'; import type { TxStateChanges } from 'types/api/txStateChanges';
import TxStateListItem from 'ui/tx/state/TxStateListItem'; import TxStateListItem from 'ui/tx/state/TxStateListItem';
const TxStateList = () => { interface Props {
data: TxStateChanges;
}
const TxStateList = ({ data }: Props) => {
return ( return (
<Box mt={ 6 }> <Box mt={ 6 }>
{ data.map((item, index) => <TxStateListItem key={ index } { ...item }/>) } { data.map((item, index) => <TxStateListItem key={ index } data={ item }/>) }
</Box> </Box>
); );
}; };
......
import { AccordionItem, AccordionButton, AccordionIcon, Button, Box, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react'; import { AccordionItem, AccordionButton, AccordionIcon, Button, Box, Flex, Text, StatArrow, Stat } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement'; import type { TxStateChange } from 'types/api/txStateChanges';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import type { data } from 'data/txState'; // import { nbsp } from 'lib/html-entities';
import { nbsp } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
// import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile';
import TxStateStorageItem from './TxStateStorageItem'; // import TxStateStorageItem from './TxStateStorageItem';
import { formatData } from './utils';
type Props = ArrayElement<typeof data>; interface Props {
data: TxStateChange;
}
const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props) => { const TxStateListItem = ({ data }: Props) => {
const hasStorageData = Boolean(storage?.length); const hasStorageData = false;
const { balanceBefore, balanceAfter, difference, isIncrease } = formatData(data);
return ( return (
<ListItemMobile> <ListItemMobile>
...@@ -46,50 +49,51 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props ...@@ -46,50 +49,51 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
// AccordionButton has its own opacity rule when disabled // AccordionButton has its own opacity rule when disabled
_disabled={{ opacity: 1 }} _disabled={{ opacity: 1 }}
> >
{ storage?.length || '0' } { data.storage?.length || '0' }
</Button> </Button>
<AccordionIcon color="blue.600" width="30px"/> <AccordionIcon color="blue.600" width="30px"/>
</AccordionButton> </AccordionButton>
<Address flexGrow={ 1 }> <Address flexGrow={ 1 }>
{ /* ??? */ } <AddressIcon address={ data.address }/>
{ /* <AddressIcon hash={ address }/> */ } <AddressLink type="address" hash={ data.address.hash } ml={ 2 }/>
<AddressLink type="address" hash={ address } ml={ 2 }/>
</Address> </Address>
</Flex> </Flex>
{ hasStorageData && ( { /* { hasStorageData && (
<AccordionPanel fontWeight={ 500 } p={ 0 }> <AccordionPanel fontWeight={ 500 } p={ 0 }>
{ storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) } { data.storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel> </AccordionPanel>
) } ) } */ }
<Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }> <Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }>
<Box> { data.is_miner && (
<Text as="span">{ capitalize(getNetworkValidatorTitle()) }</Text> <Box>
<Link>{ miner }</Link> <Text as="span">{ capitalize(getNetworkValidatorTitle()) }</Text>
</Box> <span> -</span>
</Box>
) }
<Box> <Box>
<Text as="span">Before { appConfig.network.currency.symbol } </Text> <Text as="span">Before { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ before.balance }</Text> <Text as="span" variant="secondary">{ balanceBefore }</Text>
</Box> </Box>
{ typeof before.nonce !== 'undefined' && ( { /* { typeof before.nonce !== 'undefined' && (
<Box> <Box>
<Text as="span">Nonce:</Text> <Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ before.nonce }</Text> <Text as="span" fontWeight={ 600 }>{ nbsp }{ before.nonce }</Text>
</Box> </Box>
) } ) } */ }
<Box> <Box>
<Text as="span">After { appConfig.network.currency.symbol } </Text> <Text as="span">After { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ after.balance }</Text> <Text as="span" variant="secondary">{ balanceAfter }</Text>
</Box> </Box>
{ typeof after.nonce !== 'undefined' && ( { /* { typeof after.nonce !== 'undefined' && (
<Box> <Box>
<Text as="span">Nonce:</Text> <Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text> <Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text>
</Box> </Box>
) } ) } */ }
<Text>State difference { appConfig.network.currency.symbol }</Text> <Text>State difference { appConfig.network.currency.symbol }</Text>
<Stat> <Stat>
{ diff } { difference }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/> <StatArrow ml={ 2 } type={ isIncrease ? 'increase' : 'decrease' }/>
</Stat> </Stat>
</Flex> </Flex>
</> </>
......
...@@ -7,13 +7,18 @@ import { ...@@ -7,13 +7,18 @@ import {
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type { TxStateChanges } from 'types/api/txStateChanges';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { data } from 'data/txState';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import TxStateTableItem from 'ui/tx/state/TxStateTableItem'; import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
const TxStateTable = () => { interface Props {
data: TxStateChanges;
}
const TxStateTable = ({ data }: Props) => {
return ( return (
<Table variant="simple" minWidth="950px" size="sm" w="auto" mt={ 6 }> <Table variant="simple" minWidth="950px" size="sm" w="auto" mt={ 6 }>
<Thead top={ 0 }> <Thead top={ 0 }>
...@@ -21,13 +26,13 @@ const TxStateTable = () => { ...@@ -21,13 +26,13 @@ const TxStateTable = () => {
<Th width="92px">Storage</Th> <Th width="92px">Storage</Th>
<Th width="146px">Address</Th> <Th width="146px">Address</Th>
<Th width="120px">{ capitalize(getNetworkValidatorTitle()) }</Th> <Th width="120px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency.symbol }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency.symbol }` }</Th> <Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency.symbol }` }</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency.symbol }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency.symbol }` }</Th> <Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency.symbol }` }</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) } { data.map((item, index) => <TxStateTableItem data={ item } key={ index }/>) }
</Tbody> </Tbody>
</Table> </Table>
); );
......
import { import {
AccordionItem, AccordionItem,
AccordionButton, AccordionButton,
AccordionPanel, // AccordionPanel,
AccordionIcon, AccordionIcon,
Text,
Box, Box,
Tr, Tr,
Td, Td,
Stat, Stat,
StatArrow, StatArrow,
Portal, // Portal,
Link,
Button, Button,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import type { TTxStateItem } from 'data/txState'; import type { TxStateChange } from 'types/api/txStateChanges';
import { nbsp } from 'lib/html-entities';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
// import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
// import { nbsp } from 'lib/html-entities';
// import TxStateStorageItem from './TxStateStorageItem';
import { formatData } from './utils';
import TxStateStorageItem from './TxStateStorageItem'; interface Props {
data: TxStateChange;
}
const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => { const TxStateTableItem = ({ data }: Props) => {
const ref = useRef<HTMLTableDataCellElement>(null); const ref = useRef<HTMLTableDataCellElement>(null);
const hasStorageData = Boolean(txStateItem.storage?.length); const hasStorageData = false;
const { balanceBefore, balanceAfter, difference, isIncrease } = formatData(data);
return ( return (
<> <>
...@@ -51,39 +57,40 @@ const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => { ...@@ -51,39 +57,40 @@ const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => {
// AccordionButton has its own opacity rule when disabled // AccordionButton has its own opacity rule when disabled
_disabled={{ opacity: 1 }} _disabled={{ opacity: 1 }}
> >
{ txStateItem.storage?.length || '0' } { data.storage?.length || '0' }
</Button> </Button>
<AccordionIcon color="blue.600" width="30px"/> <AccordionIcon color="blue.600" width="30px"/>
</AccordionButton> </AccordionButton>
</Td> </Td>
<Td border={ 0 }> <Td border={ 0 }>
<Address height="30px"> <Address height="30px">
{ /* ??? */ } <AddressIcon address={ data.address }/>
{ /* <AddressIcon hash={ txStateItem.address }/> */ } <AddressLink type="address" hash={ data.address.hash } fontWeight="500" truncation="constant" ml={ 2 }/>
<AddressLink type="address" hash={ txStateItem.address } fontWeight="500" truncation="constant" ml={ 2 }/>
</Address> </Address>
</Td> </Td>
<Td border={ 0 } lineHeight="30px"><Link>{ txStateItem.miner }</Link></Td> <Td border={ 0 } lineHeight="30px">
{ data.is_miner ? 'Validator' : '-' }
</Td>
<Td border={ 0 } isNumeric lineHeight="30px"> <Td border={ 0 } isNumeric lineHeight="30px">
<Box>{ txStateItem.after.balance }</Box> <Box>{ balanceBefore }</Box>
{ typeof txStateItem.after.nonce !== 'undefined' && ( { /* { typeof txStateItem.after.nonce !== 'undefined' && (
<Box justifyContent="end" display="inline-flex">Nonce: <Text fontWeight={ 600 }>{ nbsp + txStateItem.after.nonce }</Text></Box> <Box justifyContent="end" display="inline-flex">Nonce: <Text fontWeight={ 600 }>{ nbsp + txStateItem.after.nonce }</Text></Box>
) } ) } */ }
</Td> </Td>
<Td border={ 0 } isNumeric lineHeight="30px">{ txStateItem.before.balance }</Td> <Td border={ 0 } isNumeric lineHeight="30px">{ balanceAfter }</Td>
<Td border={ 0 } isNumeric lineHeight="30px"> <Td border={ 0 } isNumeric lineHeight="30px">
<Stat> <Stat>
{ txStateItem.diff } { difference }
<StatArrow ml={ 2 } type={ Number(txStateItem.diff) > 0 ? 'increase' : 'decrease' }/> <StatArrow ml={ 2 } type={ isIncrease ? 'increase' : 'decrease' }/>
</Stat> </Stat>
</Td> </Td>
{ hasStorageData && ( { /* { hasStorageData && (
<Portal containerRef={ ref }> <Portal containerRef={ ref }>
<AccordionPanel fontWeight={ 500 }> <AccordionPanel fontWeight={ 500 }>
{ txStateItem.storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) } { data.storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel> </AccordionPanel>
</Portal> </Portal>
) } ) } */ }
</> </>
) } ) }
</AccordionItem> </AccordionItem>
......
import BigNumber from 'bignumber.js';
import type { TxStateChange } from 'types/api/txStateChanges';
import appConfig from 'configs/app/config';
export function formatData(data: TxStateChange) {
if (data.type === 'coin') {
const beforeBn = BigNumber(data.balance_before || '0').div(10 ** appConfig.network.currency.decimals);
const afterBn = BigNumber(data.balance_after || '0').div(10 ** appConfig.network.currency.decimals);
const differenceBn = afterBn.minus(beforeBn);
return {
balanceBefore: beforeBn.toFormat(),
balanceAfter: afterBn.toFormat(),
difference: differenceBn.toFormat(),
isIncrease: beforeBn.lte(afterBn),
};
}
const difference = Number(data.balance_after) - Number(data.balance_before);
return {
balanceBefore: data.balance_before,
balanceAfter: data.balance_after,
difference,
isIncrease: difference > 0,
};
}
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