@@ -1,86 +0,0 @@
|
||||
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
|
||||
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
|
||||
import type { TxnBatchesResponse } from 'types/api/txnBatches';
|
||||
import type { TTxsFilters } from 'types/api/txsFilters';
|
||||
import type { TxStateChanges } from 'types/api/txStateChanges';
|
||||
import type { VisualizedContract } from 'types/api/visualization';
|
||||
import type { WithdrawalsResponse } from 'types/api/withdrawals';
|
||||
import type ArrayElement from 'types/utils/ArrayElement';
|
||||
@@ -162,6 +163,10 @@ export const RESOURCES = {
|
||||
path: '/api/v2/transactions/:hash/raw-trace',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
tx_state_changes: {
|
||||
path: '/api/v2/transactions/:hash/state-changes',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
|
||||
// ADDRESSES
|
||||
addresses: {
|
||||
@@ -498,6 +503,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
|
||||
Q extends 'tx_logs' ? LogsResponseTx :
|
||||
Q extends 'tx_token_transfers' ? TokenTransferResponse :
|
||||
Q extends 'tx_raw_trace' ? RawTracesResponse :
|
||||
Q extends 'tx_state_changes' ? TxStateChanges :
|
||||
Q extends 'addresses' ? AddressesResponse :
|
||||
Q extends 'address' ? Address :
|
||||
Q extends 'address_counters' ? AddressCounters :
|
||||
|
||||
@@ -15,3 +15,5 @@ export const YEAR = 365 * DAY;
|
||||
|
||||
export const Kb = 1_000;
|
||||
export const Mb = 1_000 * Kb;
|
||||
|
||||
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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 = {
|
||||
'default': 'blackAlpha.200',
|
||||
_dark: 'whiteAlpha.200',
|
||||
},
|
||||
text: {
|
||||
'default': 'blackAlpha.800',
|
||||
_dark: 'whiteAlpha.800',
|
||||
},
|
||||
text_secondary: {
|
||||
'default': 'gray.500',
|
||||
_dark: 'gray.400',
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { AddressParam } from './addressParams';
|
||||
|
||||
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
|
||||
|
||||
export interface TokenInfo {
|
||||
export interface TokenInfo<T extends TokenType = TokenType> {
|
||||
address: string;
|
||||
type: TokenType;
|
||||
type: T;
|
||||
symbol: string | null;
|
||||
name: string | null;
|
||||
decimals: string | null;
|
||||
|
||||
@@ -31,6 +31,8 @@ export type TokenTransfer = (
|
||||
}
|
||||
) & TokenTransferBase
|
||||
|
||||
export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload | Array<Erc1155TotalPayload>;
|
||||
|
||||
interface TokenTransferBase {
|
||||
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
|
||||
tx_hash: string;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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 };
|
||||
const DepositsListItem = ({ item }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: 'L1 block No',
|
||||
value: (
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
|
||||
<ListItemMobileGrid.Label>L1 block No</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkExternal
|
||||
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) }
|
||||
fontWeight={ 600 }
|
||||
@@ -32,11 +33,10 @@ const DepositsListItem = ({ item }: Props) => {
|
||||
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
{ item.l1_block_number }
|
||||
</LinkExternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'L2 txn hash',
|
||||
value: (
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>L2 txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkInternal
|
||||
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
|
||||
display="flex"
|
||||
@@ -48,15 +48,13 @@ const DepositsListItem = ({ item }: Props) => {
|
||||
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
|
||||
</LinkInternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
value: timeAgo,
|
||||
},
|
||||
{
|
||||
name: 'L1 txn hash',
|
||||
value: (
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkExternal
|
||||
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
|
||||
maxW="100%"
|
||||
@@ -66,11 +64,10 @@ const DepositsListItem = ({ item }: Props) => {
|
||||
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
|
||||
</LinkExternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'L1 txn origin',
|
||||
value: (
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>L1 txn origin</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkExternal
|
||||
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
|
||||
maxW="100%"
|
||||
@@ -80,15 +77,13 @@ const DepositsListItem = ({ item }: Props) => {
|
||||
<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>
|
||||
</LinkExternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Gas limit',
|
||||
value: BigNumber(item.l2_tx_gas_limit).toFormat(),
|
||||
},
|
||||
];
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
return <ListItemMobileGrid items={ items } gridTemplateColumns="92px auto"/>;
|
||||
<ListItemMobileGrid.Label>Gas limit</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ BigNumber(item.l2_tx_gas_limit).toFormat() }</ListItemMobileGrid.Value>
|
||||
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepositsListItem;
|
||||
|
||||
@@ -18,18 +18,19 @@ type Props = { item: OutputRootsItem };
|
||||
const OutputRootsListItem = ({ item }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_timestamp).fromNow();
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: 'L2 output index',
|
||||
value: item.l2_output_index,
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
value: timeAgo,
|
||||
},
|
||||
{
|
||||
name: 'L2 block #',
|
||||
value: (
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
|
||||
<ListItemMobileGrid.Label>L2 output index</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value fontWeight={ 600 } color="text">
|
||||
{ item.l2_output_index }
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkInternal
|
||||
display="flex"
|
||||
width="fit-content"
|
||||
@@ -38,11 +39,10 @@ const OutputRootsListItem = ({ item }: Props) => {
|
||||
>
|
||||
{ item.l2_block_number }
|
||||
</LinkInternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'L1 txn hash',
|
||||
value: (
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkExternal
|
||||
maxW="100%"
|
||||
display="inline-flex"
|
||||
@@ -52,20 +52,18 @@ const OutputRootsListItem = ({ item }: Props) => {
|
||||
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
|
||||
</LinkExternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Output root',
|
||||
value: (
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>Output root</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<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>
|
||||
<CopyToClipboard text={ item.output_root }/>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
];
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
return <ListItemMobileGrid items={ items } gridTemplateColumns="100px auto"/>;
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutputRootsListItem;
|
||||
|
||||
@@ -17,16 +17,15 @@ import TxDetails from 'ui/tx/TxDetails';
|
||||
import TxInternals from 'ui/tx/TxInternals';
|
||||
import TxLogs from 'ui/tx/TxLogs';
|
||||
import TxRawTrace from 'ui/tx/TxRawTrace';
|
||||
import TxState from 'ui/tx/TxState';
|
||||
import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
|
||||
// import TxState from 'ui/tx/TxState';
|
||||
|
||||
const TABS: Array<RoutedTab> = [
|
||||
{ id: 'index', title: 'Details', component: <TxDetails/> },
|
||||
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
|
||||
{ id: 'internal', title: 'Internal txns', component: <TxInternals/> },
|
||||
{ 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/> },
|
||||
];
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
@@ -23,8 +23,7 @@ type FilterProps = {
|
||||
type Props = {
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items?: Array<any>;
|
||||
items?: Array<unknown>;
|
||||
emptyText: string;
|
||||
actionBar?: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
|
||||
@@ -7,6 +7,10 @@ interface Props {
|
||||
}
|
||||
|
||||
const HashStringShorten = ({ hash, isTooltipDisabled }: Props) => {
|
||||
if (hash.length <= 8) {
|
||||
return <span>{ hash }</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={ hash } isDisabled={ isTooltipDisabled }>
|
||||
{ hash.slice(0, 4) + '...' + hash.slice(-4) }
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { Grid, Text, chakra } from '@chakra-ui/react';
|
||||
import { Grid, chakra, GridItem } from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
|
||||
type Item = {
|
||||
name: string;
|
||||
value: string | React.ReactNode;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: Array<Item>;
|
||||
interface ContainerProps {
|
||||
className?: string;
|
||||
isAnimated?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => {
|
||||
const Container = chakra(({ isAnimated, children, className }: ContainerProps) => {
|
||||
return (
|
||||
<Grid
|
||||
as={ motion.div }
|
||||
@@ -22,10 +17,11 @@ const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transitionDuration="normal"
|
||||
transitionTimingFunction="linear"
|
||||
rowGap={ 4 }
|
||||
rowGap={ 2 }
|
||||
columnGap={ 2 }
|
||||
gridTemplateColumns="max-content auto"
|
||||
paddingY={ 6 }
|
||||
gridTemplateColumns="86px auto"
|
||||
gridTemplateRows="minmax(30px, max-content)"
|
||||
paddingY={ 4 }
|
||||
borderColor="divider"
|
||||
borderTopWidth="1px"
|
||||
_last={{
|
||||
@@ -34,14 +30,46 @@ const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => {
|
||||
className={ className }
|
||||
fontSize="sm"
|
||||
>
|
||||
{ items.map(item => Boolean(item.value) && (
|
||||
<>
|
||||
<Text >{ item.name }</Text>
|
||||
{ typeof item.value === 'string' ? <Text variant="secondary">{ item.value }</Text> : item.value }
|
||||
</>
|
||||
)) }
|
||||
{ children }
|
||||
</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';
|
||||
import React from 'react';
|
||||
|
||||
import nftPlaceholder from 'icons/nft_shield.svg';
|
||||
import HashStringShorten from 'ui/shared/HashStringShorten';
|
||||
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
|
||||
@@ -11,9 +12,10 @@ interface Props {
|
||||
id: string;
|
||||
className?: string;
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -28,7 +30,7 @@ const TokenTransferNft = ({ hash, id, className, isDisabled }: Props) => {
|
||||
>
|
||||
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/>
|
||||
<Box maxW="calc(100% - 34px)">
|
||||
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
|
||||
{ truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> }
|
||||
</Box>
|
||||
</Component>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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();
|
||||
});
|
||||
@@ -1,23 +1,73 @@
|
||||
import { Accordion, Text } from '@chakra-ui/react';
|
||||
import { Accordion, Hide, Show, Skeleton, Text } from '@chakra-ui/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 TxStateTable from 'ui/tx/state/TxStateTable';
|
||||
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
|
||||
|
||||
import TxPendingAlert from './TxPendingAlert';
|
||||
import TxSocketAlert from './TxSocketAlert';
|
||||
|
||||
const TxState = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const list = isMobile ? <TxStateList/> : <TxStateTable/>;
|
||||
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
|
||||
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>
|
||||
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>
|
||||
<Accordion allowMultiple defaultIndex={ [] }>
|
||||
{ list }
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<TxStateTable data={ data }/>
|
||||
</Hide>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<TxStateList data={ data }/>
|
||||
</Show>
|
||||
</Accordion>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
isLoading={ isLoading }
|
||||
items={ data }
|
||||
emptyText="There are no state changes for this transaction."
|
||||
content={ content }
|
||||
skeletonProps={{ customSkeleton: skeleton }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 53 KiB |
@@ -1,13 +1,18 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { data } from 'data/txState';
|
||||
import type { TxStateChanges } from 'types/api/txStateChanges';
|
||||
|
||||
import TxStateListItem from 'ui/tx/state/TxStateListItem';
|
||||
|
||||
const TxStateList = () => {
|
||||
interface Props {
|
||||
data: TxStateChanges;
|
||||
}
|
||||
|
||||
const TxStateList = ({ data }: Props) => {
|
||||
return (
|
||||
<Box mt={ 6 }>
|
||||
{ data.map((item, index) => <TxStateListItem key={ index } { ...item }/>) }
|
||||
{ data.map((item, index) => <TxStateListItem key={ index } data={ item }/>) }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,101 +1,63 @@
|
||||
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 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 AddressIcon from 'ui/shared/address/AddressIcon';
|
||||
import AddressIcon from 'ui/shared/address/AddressIcon';
|
||||
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 (
|
||||
<ListItemMobile>
|
||||
<AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column">
|
||||
{ ({ isExpanded }) => (
|
||||
<>
|
||||
<Flex mb={ 6 }>
|
||||
<AccordionButton
|
||||
_hover={{ background: 'unset' }}
|
||||
padding="0"
|
||||
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>
|
||||
</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">{ capitalize(getNetworkValidatorTitle()) }</Text>
|
||||
<Link>{ miner }</Link>
|
||||
</Box>
|
||||
<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>
|
||||
<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 { appConfig.network.currency.symbol }</Text>
|
||||
<Stat>
|
||||
{ diff }
|
||||
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
|
||||
</Stat>
|
||||
</Flex>
|
||||
</>
|
||||
) }
|
||||
</AccordionItem>
|
||||
</ListItemMobile>
|
||||
<ListItemMobileGrid.Container>
|
||||
|
||||
<ListItemMobileGrid.Label>Address</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value py="3px">
|
||||
<Address flexGrow={ 1 } w="100%" alignSelf="center">
|
||||
<AddressIcon address={ data.address }/>
|
||||
<AddressLink type="address" hash={ data.address.hash } ml={ 2 } truncation="constant" mr={ 3 }/>
|
||||
{ tag }
|
||||
</Address>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ before && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>Before</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ before }</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ after && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>After</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ after }</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ change && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>Change</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ change }</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ tokenId && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>Token ID</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value py="0">{ tokenId }</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
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 {
|
||||
Tr,
|
||||
Th,
|
||||
} from '@chakra-ui/react';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import React from 'react';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import { data } from 'data/txState';
|
||||
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
|
||||
import type { TxStateChanges } from 'types/api/txStateChanges';
|
||||
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
|
||||
|
||||
const TxStateTable = () => {
|
||||
interface Props {
|
||||
data: TxStateChanges;
|
||||
}
|
||||
|
||||
const TxStateTable = ({ data }: Props) => {
|
||||
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 }>
|
||||
<Tr>
|
||||
<Th width="92px">Storage</Th>
|
||||
<Th width="140px">Type</Th>
|
||||
<Th width="146px">Address</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>{ `State difference ${ appConfig.network.currency.symbol }` }</Th>
|
||||
<Th width="33%" isNumeric>Before</Th>
|
||||
<Th width="33%" isNumeric>After</Th>
|
||||
<Th width="33%" isNumeric>Change</Th>
|
||||
<Th width="150px" minW="80px" maxW="150px">Token ID</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
|
||||
{ data.map((item, index) => <TxStateTableItem data={ item } key={ index }/>) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxStateTable;
|
||||
export default React.memo(TxStateTable);
|
||||
|
||||
@@ -1,95 +1,38 @@
|
||||
import {
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
Text,
|
||||
Box,
|
||||
Tr,
|
||||
Td,
|
||||
Stat,
|
||||
StatArrow,
|
||||
Portal,
|
||||
Link,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useRef } from 'react';
|
||||
import { Tr, Td } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TxStateChange } from 'types/api/txStateChanges';
|
||||
|
||||
import type { TTxStateItem } from 'data/txState';
|
||||
import { nbsp } from 'lib/html-entities';
|
||||
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 TxStateStorageItem from './TxStateStorageItem';
|
||||
import { getStateElements } from './utils';
|
||||
|
||||
const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => {
|
||||
const ref = useRef<HTMLTableDataCellElement>(null);
|
||||
interface Props {
|
||||
data: TxStateChange;
|
||||
}
|
||||
|
||||
const hasStorageData = Boolean(txStateItem.storage?.length);
|
||||
const TxStateTableItem = ({ data }: Props) => {
|
||||
const { before, after, change, tag, tokenId } = getStateElements(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccordionItem as="tr" isDisabled={ !hasStorageData } fontWeight={ 500 } border={ 0 }>
|
||||
{ ({ isExpanded }) => (
|
||||
<>
|
||||
<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 border={ 0 }>
|
||||
<Address height="30px">
|
||||
{ /* ??? */ }
|
||||
{ /* <AddressIcon hash={ txStateItem.address }/> */ }
|
||||
<AddressLink type="address" hash={ txStateItem.address } fontWeight="500" truncation="constant" ml={ 2 }/>
|
||||
</Address>
|
||||
</Td>
|
||||
<Td border={ 0 } lineHeight="30px"><Link>{ txStateItem.miner }</Link></Td>
|
||||
<Td border={ 0 } isNumeric lineHeight="30px">
|
||||
<Box>{ txStateItem.after.balance }</Box>
|
||||
{ typeof txStateItem.after.nonce !== 'undefined' && (
|
||||
<Box justifyContent="end" display="inline-flex">Nonce: <Text fontWeight={ 600 }>{ nbsp + txStateItem.after.nonce }</Text></Box>
|
||||
) }
|
||||
</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>
|
||||
</>
|
||||
<Tr>
|
||||
<Td lineHeight="30px">
|
||||
{ tag }
|
||||
</Td>
|
||||
<Td>
|
||||
<Address height="30px">
|
||||
<AddressIcon address={ data.address }/>
|
||||
<AddressLink type="address" hash={ data.address.hash } alias={ data.address.name } fontWeight="500" truncation="constant" ml={ 2 }/>
|
||||
</Address>
|
||||
</Td>
|
||||
<Td isNumeric lineHeight="30px">{ before }</Td>
|
||||
<Td isNumeric lineHeight="30px">{ after }</Td>
|
||||
<Td isNumeric lineHeight="30px"> { change } </Td>
|
||||
<Td lineHeight="30px">{ tokenId }</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxStateTableItem;
|
||||
export default React.memo(TxStateTableItem);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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);
|
||||
@@ -0,0 +1,122 @@
|
||||
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 };
|
||||
const TxnBatchesListItem = ({ item }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_timestamp).fromNow();
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: 'L2 block #',
|
||||
value: (
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
|
||||
<ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkInternal
|
||||
fontWeight={ 600 }
|
||||
display="flex"
|
||||
@@ -32,19 +33,17 @@ const TxnBatchesListItem = ({ item }: Props) => {
|
||||
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
{ item.l2_block_number }
|
||||
</LinkInternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'L2 block txn count',
|
||||
value: (
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>L2 block txn count</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString(), tab: 'txs' } }) }>
|
||||
{ item.tx_count }
|
||||
</LinkInternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Epoch number',
|
||||
value: (
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>Epoch number</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkExternal
|
||||
fontWeight={ 600 }
|
||||
display="inline-flex"
|
||||
@@ -52,11 +51,10 @@ const TxnBatchesListItem = ({ item }: Props) => {
|
||||
>
|
||||
{ item.epoch_number }
|
||||
</LinkExternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'L1 txn hash',
|
||||
value: (
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<VStack spacing={ 3 } w="100%" overflow="hidden">
|
||||
{ item.l1_tx_hashes.map(hash => (
|
||||
<LinkExternal
|
||||
@@ -70,15 +68,13 @@ const TxnBatchesListItem = ({ item }: Props) => {
|
||||
</LinkExternal>
|
||||
)) }
|
||||
</VStack>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
value: timeAgo,
|
||||
},
|
||||
];
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
return <ListItemMobileGrid items={ items } gridTemplateColumns="100px auto"/>;
|
||||
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
|
||||
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxnBatchesListItem;
|
||||
|
||||
@@ -18,26 +18,31 @@ import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
type Props = { item: WithdrawalsItem };
|
||||
|
||||
const WithdrawalsListItem = ({ item }: Props) => {
|
||||
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : '';
|
||||
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '';
|
||||
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' : null;
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: 'Msg nonce',
|
||||
value: item.msg_nonce_version + '-' + item.msg_nonce,
|
||||
},
|
||||
{
|
||||
name: 'From',
|
||||
value: item.from ? (
|
||||
<Address>
|
||||
<AddressIcon address={ item.from }/>
|
||||
<AddressLink hash={ item.from?.hash } type="address" truncation="dynamic" ml={ 2 }/>
|
||||
</Address>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
name: 'L2 txn hash',
|
||||
value: (
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
|
||||
<ListItemMobileGrid.Label>Msg nonce</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
{ item.msg_nonce_version + '-' + item.msg_nonce }
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ item.from && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>From</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Address>
|
||||
<AddressIcon address={ item.from }/>
|
||||
<AddressLink hash={ item.from.hash } type="address" truncation="dynamic" ml={ 2 }/>
|
||||
</Address>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
<ListItemMobileGrid.Label>L2 txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkInternal
|
||||
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
|
||||
display="flex"
|
||||
@@ -49,39 +54,48 @@ const WithdrawalsListItem = ({ item }: Props) => {
|
||||
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
|
||||
</LinkInternal>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
value: timeAgo,
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
value: item.status === 'Ready for relay' ?
|
||||
<LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> :
|
||||
item.status,
|
||||
},
|
||||
{
|
||||
name: 'L1 txn hash',
|
||||
value: item.l1_tx_hash ? (
|
||||
<LinkExternal
|
||||
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
|
||||
maxW="100%"
|
||||
display="inline-flex"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
|
||||
</LinkExternal>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
name: 'Time left',
|
||||
value: timeToEnd,
|
||||
},
|
||||
];
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
return <ListItemMobileGrid items={ items } gridTemplateColumns="92px auto"/>;
|
||||
{ timeAgo && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
<ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
{ item.status === 'Ready for relay' ?
|
||||
<LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> :
|
||||
item.status }
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ item.l1_tx_hash && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<LinkExternal
|
||||
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
|
||||
maxW="100%"
|
||||
display="inline-flex"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
|
||||
</LinkExternal>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ timeToEnd && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>Time left</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>{ timeToEnd }</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithdrawalsListItem;
|
||||
|
||||