Commit 1d6236ad authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #693 from blockscout/tx-state-api

tx state changes
parents 8fee1bad ac558374
export const data = [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
nonce: '4',
},
before: {
balance: '0.008350264867549483',
nonce: '5',
},
diff: '0.003842645503636562',
storage: [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
],
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
nonce: '4',
},
before: {
balance: '0.008350264867549483',
nonce: '5',
},
diff: '0.003842645503636562',
storage: [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
],
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
},
before: {
balance: '0.008350264867549483',
},
diff: '-0.003842645503636562',
},
];
export type TTxState = Array<TTxStateItem>;
export type TTxStateItem = {
address: string;
miner: string;
after: {
balance: string;
nonce?: string;
};
before: {
balance: string;
nonce?: string;
};
diff: string;
storage?: Array<TTxStateItemStorage>;
}
export type TTxStateItemStorage = {
address: string;
before: string;
after: string;
}
...@@ -39,6 +39,7 @@ import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/toke ...@@ -39,6 +39,7 @@ import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/toke
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TxnBatchesResponse } from 'types/api/txnBatches'; import type { TxnBatchesResponse } from 'types/api/txnBatches';
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 { WithdrawalsResponse } from 'types/api/withdrawals'; import type { WithdrawalsResponse } from 'types/api/withdrawals';
import type ArrayElement from 'types/utils/ArrayElement'; import type ArrayElement from 'types/utils/ArrayElement';
...@@ -162,6 +163,10 @@ export const RESOURCES = { ...@@ -162,6 +163,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: {
...@@ -498,6 +503,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse : ...@@ -498,6 +503,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 :
......
...@@ -15,3 +15,5 @@ export const YEAR = 365 * DAY; ...@@ -15,3 +15,5 @@ export const YEAR = 365 * DAY;
export const Kb = 1_000; export const Kb = 1_000;
export const Mb = 1_000 * Kb; export const Mb = 1_000 * Kb;
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
import type { TxStateChange } from 'types/api/txStateChanges';
export const mintToken: TxStateChange = {
address: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
balance_after: null,
balance_before: null,
change: [
{
direction: 'from',
total: {
token_id: '15077554365819457090226168288698582604878106156134383525616269766016907608065',
},
},
],
is_miner: false,
token: {
address: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543',
decimals: null,
exchange_rate: null,
holders: '9191',
name: 'ParaSpace Derivative Token MOONBIRD',
symbol: 'nMOONBIRD',
total_supply: '10645',
type: 'ERC-721',
},
type: 'token',
};
export const receiveMintedToken: TxStateChange = {
address: {
hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
balance_after: '1',
balance_before: '0',
change: [
{
direction: 'to',
total: {
token_id: '15077554365819457090226168288698582604878106156134383525616269766016907608065',
},
},
],
is_miner: false,
token: {
address: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543',
decimals: null,
exchange_rate: null,
holders: '9191',
name: 'ParaSpace Derivative Token MOONBIRD',
symbol: 'nMOONBIRD',
total_supply: '10645',
type: 'ERC-721',
},
type: 'token',
};
export const receiveCoin: TxStateChange = {
address: {
hash: '0x8dC847Af872947Ac18d5d63fA646EB65d4D99560',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
balance_after: '443787514723917012805',
balance_before: '443787484997510408745',
change: '29726406604060',
is_miner: true,
token: null,
type: 'coin',
};
export const sendCoin: TxStateChange = {
address: {
hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
balance_after: '828282622733717191',
balance_before: '832127467556437753',
change: '-3844844822720562',
is_miner: false,
token: null,
type: 'coin',
};
export const baseResponse = [
mintToken,
receiveMintedToken,
sendCoin,
receiveCoin,
];
...@@ -4,6 +4,10 @@ const semanticTokens = { ...@@ -4,6 +4,10 @@ const semanticTokens = {
'default': 'blackAlpha.200', 'default': 'blackAlpha.200',
_dark: 'whiteAlpha.200', _dark: 'whiteAlpha.200',
}, },
text: {
'default': 'blackAlpha.800',
_dark: 'whiteAlpha.800',
},
text_secondary: { text_secondary: {
'default': 'gray.500', 'default': 'gray.500',
_dark: 'gray.400', _dark: 'gray.400',
......
...@@ -2,9 +2,9 @@ import type { AddressParam } from './addressParams'; ...@@ -2,9 +2,9 @@ import type { AddressParam } from './addressParams';
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155'; export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
export interface TokenInfo { export interface TokenInfo<T extends TokenType = TokenType> {
address: string; address: string;
type: TokenType; type: T;
symbol: string | null; symbol: string | null;
name: string | null; name: string | null;
decimals: string | null; decimals: string | null;
......
...@@ -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 { Erc1155TotalPayload, Erc721TotalPayload } from './tokenTransfer';
export type TxStateChange = (TxStateChangeCoin | TxStateChangeToken) & {
address: AddressParam;
is_miner: boolean;
balance_before: string | null;
balance_after: string | null;
}
export interface TxStateChangeCoin {
type: 'coin';
change: string;
token: null;
}
export type TxStateChangeToken = TxStateChangeTokenErc20 | TxStateChangeTokenErc721 | TxStateChangeTokenErc1155;
type ChangeDirection = 'from' | 'to';
export interface TxStateChangeTokenErc20 {
type: 'token';
token: TokenInfo<'ERC-20'>;
change: string;
}
export interface TxStateChangeTokenErc721 {
type: 'token';
token: TokenInfo<'ERC-721'>;
change: Array<{
direction: ChangeDirection;
total: Erc721TotalPayload;
}>;
}
export type TxStateChangeTokenErc1155 = TxStateChangeTokenErc1155Single | TxStateChangeTokenErc1155Batch;
export interface TxStateChangeTokenErc1155Single {
type: 'token';
token: TokenInfo<'ERC-1155'>;
change: Array<{
direction: ChangeDirection;
total: Erc1155TotalPayload;
}>;
}
export interface TxStateChangeTokenErc1155Batch {
type: 'token';
token: TokenInfo<'ERC-1155'>;
change: Array<{
direction: ChangeDirection;
total: Array<Erc1155TotalPayload>;
}>;
}
export type TxStateChanges = Array<TxStateChange>;
...@@ -20,10 +20,11 @@ type Props = { item: DepositsItem }; ...@@ -20,10 +20,11 @@ type Props = { item: DepositsItem };
const DepositsListItem = ({ item }: Props) => { const DepositsListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const items = [ return (
{ <ListItemMobileGrid.Container>
name: 'L1 block No',
value: ( <ListItemMobileGrid.Label>L1 block No</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
...@@ -32,11 +33,10 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -32,11 +33,10 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l1_block_number } { item.l1_block_number }
</LinkExternal> </LinkExternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>L2 txn hash</ListItemMobileGrid.Label>
name: 'L2 txn hash', <ListItemMobileGrid.Value>
value: (
<LinkInternal <LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) } href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex" display="flex"
...@@ -48,15 +48,13 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -48,15 +48,13 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box> <Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
</LinkInternal> </LinkInternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
name: 'Age', <ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
value: timeAgo,
}, <ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
{ <ListItemMobileGrid.Value>
name: 'L1 txn hash',
value: (
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
...@@ -66,11 +64,10 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -66,11 +64,10 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal> </LinkExternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>L1 txn origin</ListItemMobileGrid.Label>
name: 'L1 txn origin', <ListItemMobileGrid.Value>
value: (
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
maxW="100%" maxW="100%"
...@@ -80,15 +77,13 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -80,15 +77,13 @@ const DepositsListItem = ({ item }: Props) => {
<AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} mr={ 2 }/> <AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} mr={ 2 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_origin }/></Box> <Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_origin }/></Box>
</LinkExternal> </LinkExternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>Gas limit</ListItemMobileGrid.Label>
name: 'Gas limit', <ListItemMobileGrid.Value>{ BigNumber(item.l2_tx_gas_limit).toFormat() }</ListItemMobileGrid.Value>
value: BigNumber(item.l2_tx_gas_limit).toFormat(),
},
];
return <ListItemMobileGrid items={ items } gridTemplateColumns="92px auto"/>; </ListItemMobileGrid.Container>
);
}; };
export default DepositsListItem; export default DepositsListItem;
...@@ -18,18 +18,19 @@ type Props = { item: OutputRootsItem }; ...@@ -18,18 +18,19 @@ type Props = { item: OutputRootsItem };
const OutputRootsListItem = ({ item }: Props) => { const OutputRootsListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
const items = [ return (
{ <ListItemMobileGrid.Container>
name: 'L2 output index',
value: item.l2_output_index, <ListItemMobileGrid.Label>L2 output index</ListItemMobileGrid.Label>
}, <ListItemMobileGrid.Value fontWeight={ 600 } color="text">
{ { item.l2_output_index }
name: 'Age', </ListItemMobileGrid.Value>
value: timeAgo,
}, <ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
{ <ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
name: 'L2 block #',
value: ( <ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal <LinkInternal
display="flex" display="flex"
width="fit-content" width="fit-content"
...@@ -38,11 +39,10 @@ const OutputRootsListItem = ({ item }: Props) => { ...@@ -38,11 +39,10 @@ const OutputRootsListItem = ({ item }: Props) => {
> >
{ item.l2_block_number } { item.l2_block_number }
</LinkInternal> </LinkInternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
name: 'L1 txn hash', <ListItemMobileGrid.Value>
value: (
<LinkExternal <LinkExternal
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
...@@ -52,20 +52,18 @@ const OutputRootsListItem = ({ item }: Props) => { ...@@ -52,20 +52,18 @@ const OutputRootsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal> </LinkExternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>Output root</ListItemMobileGrid.Label>
name: 'Output root', <ListItemMobileGrid.Value>
value: (
<Flex overflow="hidden" whiteSpace="nowrap" alignItems="center" w="100%" justifyContent="space-between"> <Flex overflow="hidden" whiteSpace="nowrap" alignItems="center" w="100%" justifyContent="space-between">
<Text variant="secondary" w="calc(100% - 24px)"><HashStringShortenDynamic hash={ item.output_root }/></Text> <Text variant="secondary" w="calc(100% - 24px)"><HashStringShortenDynamic hash={ item.output_root }/></Text>
<CopyToClipboard text={ item.output_root }/> <CopyToClipboard text={ item.output_root }/>
</Flex> </Flex>
), </ListItemMobileGrid.Value>
},
];
return <ListItemMobileGrid items={ items } gridTemplateColumns="100px auto"/>; </ListItemMobileGrid.Container>
);
}; };
export default OutputRootsListItem; export default OutputRootsListItem;
...@@ -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/> },
]; ];
......
...@@ -23,8 +23,7 @@ type FilterProps = { ...@@ -23,8 +23,7 @@ type FilterProps = {
type Props = { type Props = {
isError: boolean; isError: boolean;
isLoading: boolean; isLoading: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any items?: Array<unknown>;
items?: Array<any>;
emptyText: string; emptyText: string;
actionBar?: React.ReactNode; actionBar?: React.ReactNode;
content: React.ReactNode; content: React.ReactNode;
......
...@@ -7,6 +7,10 @@ interface Props { ...@@ -7,6 +7,10 @@ interface Props {
} }
const HashStringShorten = ({ hash, isTooltipDisabled }: Props) => { const HashStringShorten = ({ hash, isTooltipDisabled }: Props) => {
if (hash.length <= 8) {
return <span>{ hash }</span>;
}
return ( return (
<Tooltip label={ hash } isDisabled={ isTooltipDisabled }> <Tooltip label={ hash } isDisabled={ isTooltipDisabled }>
{ hash.slice(0, 4) + '...' + hash.slice(-4) } { hash.slice(0, 4) + '...' + hash.slice(-4) }
......
import { Grid, Text, chakra } from '@chakra-ui/react'; import { Grid, chakra, GridItem } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import React from 'react'; import React from 'react';
type Item = { interface ContainerProps {
name: string;
value: string | React.ReactNode;
}
interface Props {
items: Array<Item>;
className?: string; className?: string;
isAnimated?: boolean; isAnimated?: boolean;
children: React.ReactNode;
} }
const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => { const Container = chakra(({ isAnimated, children, className }: ContainerProps) => {
return ( return (
<Grid <Grid
as={ motion.div } as={ motion.div }
...@@ -22,10 +17,11 @@ const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => { ...@@ -22,10 +17,11 @@ const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => {
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal" transitionDuration="normal"
transitionTimingFunction="linear" transitionTimingFunction="linear"
rowGap={ 4 } rowGap={ 2 }
columnGap={ 2 } columnGap={ 2 }
gridTemplateColumns="max-content auto" gridTemplateColumns="86px auto"
paddingY={ 6 } gridTemplateRows="minmax(30px, max-content)"
paddingY={ 4 }
borderColor="divider" borderColor="divider"
borderTopWidth="1px" borderTopWidth="1px"
_last={{ _last={{
...@@ -34,14 +30,46 @@ const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => { ...@@ -34,14 +30,46 @@ const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => {
className={ className } className={ className }
fontSize="sm" fontSize="sm"
> >
{ items.map(item => Boolean(item.value) && ( { children }
<>
<Text >{ item.name }</Text>
{ typeof item.value === 'string' ? <Text variant="secondary">{ item.value }</Text> : item.value }
</>
)) }
</Grid> </Grid>
); );
});
interface LabelProps {
className?: string;
children: React.ReactNode;
}
const Label = chakra(({ children, className }: LabelProps) => {
return (
<GridItem className={ className } fontWeight={ 500 } lineHeight="20px" py="5px">
{ children }
</GridItem>
);
});
interface ValueProps {
className?: string;
children: React.ReactNode;
}
const Value = chakra(({ children, className }: ValueProps) => {
return (
<GridItem
className={ className }
py="5px"
color="text_secondary"
overflow="hidden"
>
{ children }
</GridItem>
);
});
const ListItemMobileGrid = {
Container,
Label,
Value,
}; };
export default chakra(ListItemMobileGrid); export default ListItemMobileGrid;
...@@ -3,6 +3,7 @@ import { route } from 'nextjs-routes'; ...@@ -3,6 +3,7 @@ import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg'; import nftPlaceholder from 'icons/nft_shield.svg';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -11,9 +12,10 @@ interface Props { ...@@ -11,9 +12,10 @@ interface Props {
id: string; id: string;
className?: string; className?: string;
isDisabled?: boolean; isDisabled?: boolean;
truncation?: 'dynamic' | 'constant';
} }
const TokenTransferNft = ({ hash, id, className, isDisabled }: Props) => { const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynamic' }: Props) => {
const Component = isDisabled ? Box : LinkInternal; const Component = isDisabled ? Box : LinkInternal;
return ( return (
...@@ -28,7 +30,7 @@ const TokenTransferNft = ({ hash, id, className, isDisabled }: Props) => { ...@@ -28,7 +30,7 @@ const TokenTransferNft = ({ hash, id, className, isDisabled }: Props) => {
> >
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/> <Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/>
<Box maxW="calc(100% - 34px)"> <Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> { truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> }
</Box> </Box>
</Component> </Component>
); );
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as txStateMock from 'mocks/txs/state';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxState from './TxState';
const TX_INFO_API_URL = buildApiUrl('tx', { hash: txMock.base.hash });
const TX_STATE_API_URL = buildApiUrl('tx_state_changes', { hash: txMock.base.hash });
const hooksConfig = {
router: {
query: { hash: txMock.base.hash },
},
};
test('base view +@mobile', async({ mount, page }) => {
await page.route(TX_INFO_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.base),
}));
await page.route(TX_STATE_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txStateMock.baseResponse),
}));
const component = await mount(
<TestApp>
<TxState/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
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 DataListDisplay from 'ui/shared/DataListDisplay';
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),
},
});
return ( if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
const skeleton = (
<>
<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={ [ '140px', '146px', '33%', '33%', '33%', '150px' ] }/>
</Hide>
</>
);
const content = data ? (
<> <>
<Text> <Text>
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>
</> </>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data }
emptyText="There are no state changes for this transaction."
content={ content }
skeletonProps={{ customSkeleton: skeleton }}
/>
); );
}; };
......
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 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 type { data } from 'data/txState';
import { nbsp } from 'lib/html-entities';
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/ListItemMobile'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import TxStateStorageItem from './TxStateStorageItem'; import { getStateElements } 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 { before, after, change, tag, tokenId } = getStateElements(data);
return ( return (
<ListItemMobile> <ListItemMobileGrid.Container>
<AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column">
{ ({ isExpanded }) => ( <ListItemMobileGrid.Label>Address</ListItemMobileGrid.Label>
<> <ListItemMobileGrid.Value py="3px">
<Flex mb={ 6 }> <Address flexGrow={ 1 } w="100%" alignSelf="center">
<AccordionButton <AddressIcon address={ data.address }/>
_hover={{ background: 'unset' }} <AddressLink type="address" hash={ data.address.hash } ml={ 2 } truncation="constant" mr={ 3 }/>
padding="0" { tag }
mr={ 5 }
w="auto"
>
<Button
variant="outline"
borderWidth="1px"
// button can't be inside button (AccordionButton)
as="div"
isActive={ isExpanded }
size="sm"
fontWeight={ 400 }
isDisabled={ !hasStorageData }
colorScheme="gray"
// AccordionButton has its own opacity rule when disabled
_disabled={{ opacity: 1 }}
>
{ storage?.length || '0' }
</Button>
<AccordionIcon color="blue.600" width="30px"/>
</AccordionButton>
<Address flexGrow={ 1 }>
{ /* ??? */ }
{ /* <AddressIcon hash={ address }/> */ }
<AddressLink type="address" hash={ address } ml={ 2 }/>
</Address> </Address>
</Flex> </ListItemMobileGrid.Value>
{ hasStorageData && (
<AccordionPanel fontWeight={ 500 } p={ 0 }> { before && (
{ storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) } <>
</AccordionPanel> <ListItemMobileGrid.Label>Before</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ before }</ListItemMobileGrid.Value>
</>
) } ) }
<Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }>
<Box> { after && (
<Text as="span">{ capitalize(getNetworkValidatorTitle()) }</Text> <>
<Link>{ miner }</Link> <ListItemMobileGrid.Label>After</ListItemMobileGrid.Label>
</Box> <ListItemMobileGrid.Value>{ after }</ListItemMobileGrid.Value>
<Box> </>
<Text as="span">Before { appConfig.network.currency.symbol } </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 { appConfig.network.currency.symbol } </Text> { change && (
<Text as="span" variant="secondary">{ after.balance }</Text> <>
</Box> <ListItemMobileGrid.Label>Change</ListItemMobileGrid.Label>
{ typeof after.nonce !== 'undefined' && ( <ListItemMobileGrid.Value>{ change }</ListItemMobileGrid.Value>
<Box> </>
<Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text>
</Box>
) } ) }
<Text>State difference { appConfig.network.currency.symbol }</Text>
<Stat> { tokenId && (
{ diff } <>
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/> <ListItemMobileGrid.Label>Token ID</ListItemMobileGrid.Label>
</Stat> <ListItemMobileGrid.Value py="0">{ tokenId }</ListItemMobileGrid.Value>
</Flex>
</> </>
) } ) }
</AccordionItem>
</ListItemMobile> </ListItemMobileGrid.Container>
); );
}; };
......
import {
Grid,
GridItem,
Select,
Box,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import type { TTxStateItemStorage } from 'data/txState';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) => {
const gridData = [
{ name: 'Storage Address:', value: storageItem.address },
{ name: 'Before:', value: storageItem.before, select: true },
{ name: 'After:', value: storageItem.after, select: true },
];
const backgroundColor = useColorModeValue('white', 'gray.900');
const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ];
return (
<Grid
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}
columnGap={ 3 }
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="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="xs"
borderRadius="base"
focusBorderColor="none"
display="inline-block"
w="auto"
flexShrink={ 0 }
background={ backgroundColor }
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
) }
<Box fontWeight={ 500 } whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic fontWeight="500" hash={ item.value }/>
</Box>
</GridItem>
</React.Fragment>
)) }
</Grid>
);
};
export default TxStateStorageItem;
...@@ -4,33 +4,35 @@ import { ...@@ -4,33 +4,35 @@ import {
Tr, Tr,
Th, Th,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import type { TxStateChanges } from 'types/api/txStateChanges';
import { data } from 'data/txState';
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="1000px" size="sm" w="auto" mt={ 6 }>
<Thead top={ 0 }> <Thead top={ 0 }>
<Tr> <Tr>
<Th width="92px">Storage</Th> <Th width="140px">Type</Th>
<Th width="146px">Address</Th> <Th width="146px">Address</Th>
<Th width="120px">{ capitalize(getNetworkValidatorTitle()) }</Th> <Th width="33%" isNumeric>Before</Th>
<Th width="33%" isNumeric>{ `After ${ appConfig.network.currency.symbol }` }</Th> <Th width="33%" isNumeric>After</Th>
<Th width="33%" isNumeric>{ `Before ${ appConfig.network.currency.symbol }` }</Th> <Th width="33%" isNumeric>Change</Th>
<Th width="33%" isNumeric>{ `State difference ${ appConfig.network.currency.symbol }` }</Th> <Th width="150px" minW="80px" maxW="150px">Token ID</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>
); );
}; };
export default TxStateTable; export default React.memo(TxStateTable);
import { import { Tr, Td } from '@chakra-ui/react';
AccordionItem, import React from 'react';
AccordionButton,
AccordionPanel, import type { TxStateChange } from 'types/api/txStateChanges';
AccordionIcon,
Text,
Box,
Tr,
Td,
Stat,
StatArrow,
Portal,
Link,
Button,
} from '@chakra-ui/react';
import React, { useRef } from 'react';
import type { TTxStateItem } from 'data/txState';
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 TxStateStorageItem from './TxStateStorageItem'; import { getStateElements } from './utils';
const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => { interface Props {
const ref = useRef<HTMLTableDataCellElement>(null); data: TxStateChange;
}
const hasStorageData = Boolean(txStateItem.storage?.length); const TxStateTableItem = ({ data }: Props) => {
const { before, after, change, tag, tokenId } = getStateElements(data);
return ( return (
<> <Tr>
<AccordionItem as="tr" isDisabled={ !hasStorageData } fontWeight={ 500 } border={ 0 }> <Td lineHeight="30px">
{ ({ isExpanded }) => ( { tag }
<>
<Td border={ 0 }>
<AccordionButton
_hover={{ background: 'unset' }}
padding="0"
>
<Button
variant="outline"
borderWidth="1px"
// button can't be inside button (AccordionButton)
as="div"
isActive={ isExpanded }
size="sm"
fontWeight={ 400 }
isDisabled={ !hasStorageData }
colorScheme="gray"
// AccordionButton has its own opacity rule when disabled
_disabled={{ opacity: 1 }}
>
{ txStateItem.storage?.length || '0' }
</Button>
<AccordionIcon color="blue.600" width="30px"/>
</AccordionButton>
</Td> </Td>
<Td border={ 0 }> <Td>
<Address height="30px"> <Address height="30px">
{ /* ??? */ } <AddressIcon address={ data.address }/>
{ /* <AddressIcon hash={ txStateItem.address }/> */ } <AddressLink type="address" hash={ data.address.hash } alias={ data.address.name } 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 isNumeric lineHeight="30px">{ before }</Td>
<Td border={ 0 } isNumeric lineHeight="30px"> <Td isNumeric lineHeight="30px">{ after }</Td>
<Box>{ txStateItem.after.balance }</Box> <Td isNumeric lineHeight="30px"> { change } </Td>
{ typeof txStateItem.after.nonce !== 'undefined' && ( <Td lineHeight="30px">{ tokenId }</Td>
<Box justifyContent="end" display="inline-flex">Nonce: <Text fontWeight={ 600 }>{ nbsp + txStateItem.after.nonce }</Text></Box> </Tr>
) }
</Td>
<Td border={ 0 } isNumeric lineHeight="30px">{ txStateItem.before.balance }</Td>
<Td border={ 0 } isNumeric lineHeight="30px">
<Stat>
{ txStateItem.diff }
<StatArrow ml={ 2 } type={ Number(txStateItem.diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Td>
{ hasStorageData && (
<Portal containerRef={ ref }>
<AccordionPanel fontWeight={ 500 }>
{ txStateItem.storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel>
</Portal>
) }
</>
) }
</AccordionItem>
<Tr><Td colSpan={ 6 } ref={ ref } padding={ 0 }></Td></Tr>
</>
); );
}; };
export default TxStateTableItem; export default React.memo(TxStateTableItem);
import { Flex, Link, useBoolean } from '@chakra-ui/react';
import React from 'react';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import type { TxStateChangeNftItemFlatten } from './utils';
interface Props {
items: Array<TxStateChangeNftItemFlatten>;
tokenAddress: string;
}
const TxStateTokenIdList = ({ items, tokenAddress }: Props) => {
const [ isCut, setIsCut ] = useBoolean(true);
return (
<Flex flexDir="column" rowGap={ 2 }>
{ items.slice(0, isCut ? 3 : items.length).map((item, index) => (
<TokenTransferNft
key={ index }
hash={ tokenAddress }
id={ item.total.token_id }
w="auto"
truncation="constant"
/>
)) }
{ items.length > 3 && (
<Link
fontWeight={ 400 }
textDecoration="underline dashed"
_hover={{ textDecoration: 'underline dashed', color: 'link_hovered' }}
onClick={ setIsCut.toggle }
pb={{ base: '5px', md: 0 }}
>
View { isCut ? 'more' : 'less' }
</Link>
) }
</Flex>
);
};
export default React.memo(TxStateTokenIdList);
import { Box, Flex, Tag, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TxStateChange, TxStateChangeTokenErc1155, TxStateChangeTokenErc1155Single, TxStateChangeTokenErc721 } from 'types/api/txStateChanges';
import type ArrayElement from 'types/utils/ArrayElement';
import appConfig from 'configs/app/config';
import { ZERO_ADDRESS } from 'lib/consts';
import { nbsp } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStateTokenIdList from './TxStateTokenIdList';
export function getStateElements(data: TxStateChange) {
const tag = (() => {
if (data.is_miner) {
return (
<Tooltip label="A block producer who successfully included the block into the blockchain">
<Tag textTransform="capitalize" colorScheme="yellow">{ getNetworkValidatorTitle() }</Tag>
</Tooltip>
);
}
if (data.address.hash === ZERO_ADDRESS) {
const changeDirection = (() => {
if (Array.isArray(data.change)) {
const firstChange = data.change[0];
return firstChange.direction;
}
return Number(data.change) > 0 ? 'to' : 'from';
})();
if (changeDirection) {
const text = changeDirection === 'from' ? 'Mint' : 'Burn';
return (
<Tooltip label="Address used in tokens mintings and burnings">
<Tag textTransform="capitalize" colorScheme="yellow">{ text } address</Tag>
</Tooltip>
);
}
}
return null;
})();
switch (data.type) {
case '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);
const changeColor = beforeBn.lte(afterBn) ? 'green.500' : 'red.500';
const changeSign = beforeBn.lte(afterBn) ? '+' : '-';
return {
before: <Box>{ beforeBn.toFormat() } { appConfig.network.currency.symbol }</Box>,
after: <Box>{ afterBn.toFormat() } { appConfig.network.currency.symbol }</Box>,
change: <Box color={ changeColor }>{ changeSign }{ nbsp }{ differenceBn.abs().toFormat() }</Box>,
tag,
};
}
case 'token': {
const tokenLink = <AddressLink type="token" hash={ data.token.address } alias={ trimTokenSymbol(data.token?.symbol || data.token.address) }/>;
const before = Number(data.balance_before);
const after = Number(data.balance_after);
const change = (() => {
const difference = typeof data.change === 'string' ? Number(data.change) : after - before;
if (!difference) {
return null;
}
const changeColor = difference >= 0 ? 'green.500' : 'red.500';
const changeSign = difference >= 0 ? '+' : '-';
return <Box color={ changeColor }>{ changeSign }{ nbsp }{ Math.abs(difference).toLocaleString() }</Box>;
})();
const tokenId = (() => {
if (!Array.isArray(data.change)) {
return null;
}
const items = (data.change as Array<TxStateChangeNftItem>).reduce(flattenTotal, []);
return <TxStateTokenIdList items={ items } tokenAddress={ data.token.address }/>;
})();
return {
before: data.balance_before ? (
<Flex whiteSpace="pre-wrap" justifyContent={{ base: 'flex-start', lg: 'flex-end' }}>
<span>{ before.toLocaleString() } </span>
{ tokenLink }
</Flex>
) : null,
after: data.balance_after ? (
<Flex whiteSpace="pre-wrap" justifyContent={{ base: 'flex-start', lg: 'flex-end' }}>
<span>{ after.toLocaleString() } </span>
{ tokenLink }
</Flex>
) : null,
change,
tag,
tokenId,
};
}
}
}
export type TxStateChangeNftItem = ArrayElement<TxStateChangeTokenErc721['change'] | TxStateChangeTokenErc1155['change']>;
export type TxStateChangeNftItemFlatten = ArrayElement<TxStateChangeTokenErc721['change'] | TxStateChangeTokenErc1155Single['change']>;
function flattenTotal(result: Array<TxStateChangeNftItemFlatten>, item: TxStateChangeNftItem): Array<TxStateChangeNftItemFlatten> {
if (Array.isArray(item.total)) {
result.push(...item.total.map((total) => ({ ...item, total })));
} else {
result.push({ ...item, total: item.total });
}
return result;
}
...@@ -18,10 +18,11 @@ type Props = { item: TxnBatchesItem }; ...@@ -18,10 +18,11 @@ type Props = { item: TxnBatchesItem };
const TxnBatchesListItem = ({ item }: Props) => { const TxnBatchesListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
const items = [ return (
{ <ListItemMobileGrid.Container>
name: 'L2 block #',
value: ( <ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal <LinkInternal
fontWeight={ 600 } fontWeight={ 600 }
display="flex" display="flex"
...@@ -32,19 +33,17 @@ const TxnBatchesListItem = ({ item }: Props) => { ...@@ -32,19 +33,17 @@ const TxnBatchesListItem = ({ item }: Props) => {
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l2_block_number } { item.l2_block_number }
</LinkInternal> </LinkInternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>L2 block txn count</ListItemMobileGrid.Label>
name: 'L2 block txn count', <ListItemMobileGrid.Value>
value: (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString(), tab: 'txs' } }) }> <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString(), tab: 'txs' } }) }>
{ item.tx_count } { item.tx_count }
</LinkInternal> </LinkInternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>Epoch number</ListItemMobileGrid.Label>
name: 'Epoch number', <ListItemMobileGrid.Value>
value: (
<LinkExternal <LinkExternal
fontWeight={ 600 } fontWeight={ 600 }
display="inline-flex" display="inline-flex"
...@@ -52,11 +51,10 @@ const TxnBatchesListItem = ({ item }: Props) => { ...@@ -52,11 +51,10 @@ const TxnBatchesListItem = ({ item }: Props) => {
> >
{ item.epoch_number } { item.epoch_number }
</LinkExternal> </LinkExternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
name: 'L1 txn hash', <ListItemMobileGrid.Value>
value: (
<VStack spacing={ 3 } w="100%" overflow="hidden"> <VStack spacing={ 3 } w="100%" overflow="hidden">
{ item.l1_tx_hashes.map(hash => ( { item.l1_tx_hashes.map(hash => (
<LinkExternal <LinkExternal
...@@ -70,15 +68,13 @@ const TxnBatchesListItem = ({ item }: Props) => { ...@@ -70,15 +68,13 @@ const TxnBatchesListItem = ({ item }: Props) => {
</LinkExternal> </LinkExternal>
)) } )) }
</VStack> </VStack>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
name: 'Age', <ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
value: timeAgo,
},
];
return <ListItemMobileGrid items={ items } gridTemplateColumns="100px auto"/>; </ListItemMobileGrid.Container>
);
}; };
export default TxnBatchesListItem; export default TxnBatchesListItem;
...@@ -18,26 +18,31 @@ import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; ...@@ -18,26 +18,31 @@ import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: WithdrawalsItem }; type Props = { item: WithdrawalsItem };
const WithdrawalsListItem = ({ item }: Props) => { const WithdrawalsListItem = ({ item }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : ''; const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : null;
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : ''; const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : null;
const items = [ return (
{ <ListItemMobileGrid.Container>
name: 'Msg nonce',
value: item.msg_nonce_version + '-' + item.msg_nonce, <ListItemMobileGrid.Label>Msg nonce</ListItemMobileGrid.Label>
}, <ListItemMobileGrid.Value>
{ { item.msg_nonce_version + '-' + item.msg_nonce }
name: 'From', </ListItemMobileGrid.Value>
value: item.from ? (
{ item.from && (
<>
<ListItemMobileGrid.Label>From</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Address> <Address>
<AddressIcon address={ item.from }/> <AddressIcon address={ item.from }/>
<AddressLink hash={ item.from?.hash } type="address" truncation="dynamic" ml={ 2 }/> <AddressLink hash={ item.from.hash } type="address" truncation="dynamic" ml={ 2 }/>
</Address> </Address>
) : null, </ListItemMobileGrid.Value>
}, </>
{ ) }
name: 'L2 txn hash',
value: ( <ListItemMobileGrid.Label>L2 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal <LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) } href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex" display="flex"
...@@ -49,21 +54,26 @@ const WithdrawalsListItem = ({ item }: Props) => { ...@@ -49,21 +54,26 @@ const WithdrawalsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box> <Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
</LinkInternal> </LinkInternal>
), </ListItemMobileGrid.Value>
},
{ { timeAgo && (
name: 'Age', <>
value: timeAgo, <ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
}, <ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
{ </>
name: 'Status', ) }
value: item.status === 'Ready for relay' ?
<ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.status === 'Ready for relay' ?
<LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> : <LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> :
item.status, item.status }
}, </ListItemMobileGrid.Value>
{
name: 'L1 txn hash', { item.l1_tx_hash && (
value: item.l1_tx_hash ? ( <>
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
...@@ -73,15 +83,19 @@ const WithdrawalsListItem = ({ item }: Props) => { ...@@ -73,15 +83,19 @@ const WithdrawalsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal> </LinkExternal>
) : null, </ListItemMobileGrid.Value>
}, </>
{ ) }
name: 'Time left',
value: timeToEnd, { timeToEnd && (
}, <>
]; <ListItemMobileGrid.Label>Time left</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ timeToEnd }</ListItemMobileGrid.Value>
</>
) }
return <ListItemMobileGrid items={ items } gridTemplateColumns="92px auto"/>; </ListItemMobileGrid.Container>
);
}; };
export default WithdrawalsListItem; export default WithdrawalsListItem;
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