mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
withdrawals for beacon chain
This commit is contained in:
@@ -65,3 +65,6 @@ NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_GOOGLE_AN
|
||||
NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__
|
||||
NEXT_PUBLIC_L1_BASE_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L1_BASE_URL__
|
||||
NEXT_PUBLIC_L2_WITHDRAWAL_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L2_WITHDRAWAL_URL__
|
||||
|
||||
# beacon chain config
|
||||
NEXT_PUBLIC_HAS_BEACON_CHAIN=__PLACEHOLDER_FOR_NEXT_PUBLIC_HAS_BEACON_CHAIN__
|
||||
|
||||
@@ -114,6 +114,9 @@ const config = Object.freeze({
|
||||
L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL),
|
||||
withdrawalUrl: getEnvValue(process.env.NEXT_PUBLIC_L2_WITHDRAWAL_URL) || '',
|
||||
},
|
||||
beaconChain: {
|
||||
hasBeaconChain: getEnvValue(process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN) === 'true',
|
||||
},
|
||||
statsApi: {
|
||||
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
|
||||
basePath: '',
|
||||
|
||||
@@ -145,6 +145,15 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
|
||||
| NEXT_PUBLIC_L1_BASE_URL | `string` | Base Blockscout URL for L1 network | yes | - | `'http://eth-goerli.blockscout.com'` |
|
||||
| NEXT_PUBLIC_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | yes | - | `https://app.optimism.io/bridge/withdraw` |
|
||||
|
||||
## Beacon chain configuration
|
||||
|
||||
| Variable | Type| Description | Is required | Default value | Example value |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_HAS_BEACON_CHAIN | `boolean` | Set to true for networks with the beacon chain | - | - | `true` |
|
||||
|
||||
|
||||
|
||||
|
||||
# How to add new environment variable
|
||||
|
||||
If the variable should be exposed to the browser don't forget to add prefix `NEXT_PUBLIC_` to its name.
|
||||
|
||||
+25
-2
@@ -12,9 +12,10 @@ import type {
|
||||
AddressTokenTransferFilters,
|
||||
AddressTokensFilter,
|
||||
AddressTokensResponse,
|
||||
AddressWithdrawalsResponse,
|
||||
} from 'types/api/address';
|
||||
import type { AddressesResponse } from 'types/api/addresses';
|
||||
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
|
||||
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
|
||||
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
|
||||
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
|
||||
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
|
||||
@@ -42,6 +43,7 @@ import type { TransactionsResponseValidated, TransactionsResponsePending, Transa
|
||||
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';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
@@ -127,6 +129,12 @@ export const RESOURCES = {
|
||||
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
|
||||
filterFields: [],
|
||||
},
|
||||
block_withdrawals: {
|
||||
path: '/api/v2/blocks/:height/withdrawals',
|
||||
pathParams: [ 'height' as const ],
|
||||
paginationFields: [ 'items_count' as const, 'index' as const ],
|
||||
filterFields: [],
|
||||
},
|
||||
txs_validated: {
|
||||
path: '/api/v2/transactions',
|
||||
paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ],
|
||||
@@ -167,6 +175,11 @@ export const RESOURCES = {
|
||||
path: '/api/v2/transactions/:hash/state-changes',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
withdrawals: {
|
||||
path: '/api/v2/withdrawals',
|
||||
paginationFields: [ 'index' as const, 'items_count' as const ],
|
||||
filterFields: [],
|
||||
},
|
||||
|
||||
// ADDRESSES
|
||||
addresses: {
|
||||
@@ -234,6 +247,12 @@ export const RESOURCES = {
|
||||
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const ],
|
||||
filterFields: [ 'type' as const ],
|
||||
},
|
||||
address_withdrawals: {
|
||||
path: '/api/v2/addresses/:hash/withdrawals',
|
||||
pathParams: [ 'hash' as const ],
|
||||
paginationFields: [ 'items_count' as const, 'index' as const ],
|
||||
filterFields: [],
|
||||
},
|
||||
|
||||
// CONTRACT
|
||||
contract: {
|
||||
@@ -474,7 +493,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
|
||||
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
|
||||
'token_instance_transfers' |
|
||||
'verified_contracts' |
|
||||
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits';
|
||||
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
|
||||
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
|
||||
|
||||
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
|
||||
|
||||
@@ -500,6 +520,7 @@ Q extends 'stats_line' ? StatsChart :
|
||||
Q extends 'blocks' ? BlocksResponse :
|
||||
Q extends 'block' ? Block :
|
||||
Q extends 'block_txs' ? BlockTransactionsResponse :
|
||||
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
|
||||
Q extends 'txs_validated' ? TransactionsResponseValidated :
|
||||
Q extends 'txs_pending' ? TransactionsResponsePending :
|
||||
Q extends 'tx' ? Transaction :
|
||||
@@ -519,6 +540,7 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
|
||||
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
|
||||
Q extends 'address_logs' ? LogsResponseAddress :
|
||||
Q extends 'address_tokens' ? AddressTokensResponse :
|
||||
Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
|
||||
Q extends 'token' ? TokenInfo :
|
||||
Q extends 'token_counters' ? TokenCounters :
|
||||
Q extends 'token_transfers' ? TokenTransferResponse :
|
||||
@@ -539,6 +561,7 @@ Q extends 'verified_contracts' ? VerifiedContractsResponse :
|
||||
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
|
||||
Q extends 'visualize_sol2uml' ? VisualizedContract :
|
||||
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
|
||||
Q extends 'withdrawals' ? WithdrawalsResponse :
|
||||
Q extends 'l2_output_roots' ? L2OutputRootsResponse :
|
||||
Q extends 'l2_withdrawals' ? L2WithdrawalsResponse :
|
||||
Q extends 'l2_deposits' ? L2DepositsResponse :
|
||||
|
||||
@@ -125,7 +125,14 @@ export default function useNavItems(): ReturnType {
|
||||
blocks,
|
||||
topAccounts,
|
||||
verifiedContracts,
|
||||
];
|
||||
appConfig.beaconChain.hasBeaconChain && {
|
||||
text: 'Withdrawals',
|
||||
nextRoute: { pathname: '/withdrawals' as const },
|
||||
icon: withdrawalsIcon,
|
||||
isActive: pathname === '/withdrawals',
|
||||
isNewUi: true,
|
||||
},
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
const otherNavItems: Array<NavItem> = [
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { GetServerSideProps } from 'next';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import type { Props } from 'lib/next/getServerSideProps';
|
||||
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
|
||||
if (!appConfig.beaconChain.hasBeaconChain) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return getServerSidePropsBase(args);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
export const data = {
|
||||
items: [
|
||||
{
|
||||
amount: '192175',
|
||||
block_number: 43242,
|
||||
index: 11688,
|
||||
receiver: {
|
||||
hash: '0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134',
|
||||
implementation_name: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
},
|
||||
timestamp: '2022-06-07T18:12:24.000000Z',
|
||||
validator_index: 49622,
|
||||
},
|
||||
{
|
||||
amount: '192175',
|
||||
block_number: 43242,
|
||||
index: 11687,
|
||||
receiver: {
|
||||
hash: '0xf97e987c050e5Ab072211Ad2C213Eb5AEE4DF134',
|
||||
implementation_name: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
},
|
||||
timestamp: '2022-05-07T18:12:24.000000Z',
|
||||
validator_index: 49621,
|
||||
},
|
||||
{
|
||||
amount: '182773',
|
||||
block_number: 43242,
|
||||
index: 11686,
|
||||
receiver: {
|
||||
hash: '0xf97e123c050e5Ab072211Ad2C213Eb5AEE4DF134',
|
||||
implementation_name: null,
|
||||
is_contract: false,
|
||||
is_verified: null,
|
||||
name: null,
|
||||
},
|
||||
timestamp: '2022-04-07T18:12:24.000000Z',
|
||||
validator_index: 49620,
|
||||
},
|
||||
],
|
||||
next_page_params: {
|
||||
index: 11639,
|
||||
items_count: 50,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { NextPage } from 'next';
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
|
||||
import getNetworkTitle from 'lib/networks/getNetworkTitle';
|
||||
import Withdrawals from 'ui/pages/Withdrawals';
|
||||
|
||||
const WithdrawalsPage: NextPage = () => {
|
||||
const title = getNetworkTitle();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{ title }</title>
|
||||
</Head>
|
||||
<Withdrawals/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithdrawalsPage;
|
||||
|
||||
export { getServerSideProps } from 'lib/next/getServerSidePropsBeacon';
|
||||
@@ -12,6 +12,7 @@ export interface Address {
|
||||
creator_address_hash: string | null;
|
||||
creation_tx_hash: string | null;
|
||||
exchange_rate: string | null;
|
||||
has_beacon_chain_withdrawals?: boolean;
|
||||
has_custom_methods_read: boolean;
|
||||
has_custom_methods_write: boolean;
|
||||
has_decompiled_code: boolean;
|
||||
@@ -128,3 +129,19 @@ export interface AddressInternalTxsResponse {
|
||||
transaction_index: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export type AddressWithdrawalsResponse = {
|
||||
items: Array<AddressWithdrawalsItem>;
|
||||
next_page_params: {
|
||||
index: number;
|
||||
items_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type AddressWithdrawalsItem = {
|
||||
amount: string;
|
||||
block_number: number;
|
||||
index: number;
|
||||
timestamp: string;
|
||||
validator_index: number;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Block {
|
||||
tx_count: number;
|
||||
miner: AddressParam;
|
||||
size: number;
|
||||
has_beacon_chain_withdrawals?: boolean;
|
||||
hash: string;
|
||||
parent_hash: string;
|
||||
difficulty: string;
|
||||
@@ -56,3 +57,18 @@ export interface NewBlockSocketResponse {
|
||||
export interface BlockFilters {
|
||||
type?: BlockType;
|
||||
}
|
||||
|
||||
export type BlockWithdrawalsResponse = {
|
||||
items: Array<BlockWithdrawalsItem>;
|
||||
next_page_params: {
|
||||
index: number;
|
||||
items_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type BlockWithdrawalsItem = {
|
||||
amount: string;
|
||||
index: number;
|
||||
receiver: AddressParam;
|
||||
validator_index: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AddressParam } from './addressParams';
|
||||
|
||||
export type WithdrawalsResponse = {
|
||||
items: Array<WithdrawalsItem>;
|
||||
next_page_params: {
|
||||
index: number;
|
||||
items_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type WithdrawalsItem = {
|
||||
amount: string;
|
||||
block_number: number;
|
||||
index: number;
|
||||
receiver: AddressParam;
|
||||
timestamp: string;
|
||||
validator_index: number;
|
||||
}
|
||||
Vendored
+2
-1
@@ -41,7 +41,8 @@ declare module "nextjs-routes" {
|
||||
| DynamicRoute<"/tx/[hash]", { "hash": string }>
|
||||
| StaticRoute<"/txs">
|
||||
| StaticRoute<"/verified-contracts">
|
||||
| StaticRoute<"/visualize/sol2uml">;
|
||||
| StaticRoute<"/visualize/sol2uml">
|
||||
| StaticRoute<"/withdrawals">;
|
||||
|
||||
interface StaticRoute<Pathname> {
|
||||
pathname: Pathname;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Show, Hide } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
|
||||
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
|
||||
|
||||
const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const hash = getQueryParamString(router.query.hash);
|
||||
|
||||
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
|
||||
resourceName: 'address_withdrawals',
|
||||
pathParams: { hash },
|
||||
scrollRef,
|
||||
});
|
||||
const content = data?.items ? (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
{ data.items.map((item) => <WithdrawalsListItem item={ item } key={ item.index } view="address"/>) }
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<WithdrawalsTable items={ data.items } view="address" top={ isPaginationVisible ? 80 : 0 }/>
|
||||
</Hide>
|
||||
</>
|
||||
) : null ;
|
||||
|
||||
const actionBar = isPaginationVisible ? (
|
||||
<ActionBar mt={ -6 } showShadow={ isLoading }>
|
||||
<Pagination ml="auto" { ...pagination }/>
|
||||
</ActionBar>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
isLoading={ isLoading }
|
||||
items={ data?.items }
|
||||
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(5).fill(`${ 100 / 5 }%`), skeletonDesktopMinW: '950px' }}
|
||||
emptyText="There are no withdrawals for this address."
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressWithdrawals;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Show, Hide } from '@chakra-ui/react';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockWithdrawalsResponse } from 'types/api/block';
|
||||
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
|
||||
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
|
||||
|
||||
type QueryResult = UseQueryResult<BlockWithdrawalsResponse> & {
|
||||
pagination: PaginationProps;
|
||||
isPaginationVisible: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
blockWithdrawalsQuery: QueryResult;
|
||||
}
|
||||
|
||||
const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
|
||||
const content = blockWithdrawalsQuery.data?.items ? (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
{ blockWithdrawalsQuery.data.items.map((item) => <WithdrawalsListItem item={ item } key={ item.index } view="block"/>) }
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<WithdrawalsTable items={ blockWithdrawalsQuery.data.items } view="block" top={ blockWithdrawalsQuery.isPaginationVisible ? 80 : 0 }/>
|
||||
</Hide>
|
||||
</>
|
||||
) : null ;
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ blockWithdrawalsQuery.isError }
|
||||
isLoading={ blockWithdrawalsQuery.isLoading }
|
||||
items={ blockWithdrawalsQuery.data?.items }
|
||||
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(4).fill(`${ 100 / 4 }%`), skeletonDesktopMinW: '950px' }}
|
||||
emptyText="There are no withdrawals for this block."
|
||||
content={ content }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockWithdrawals;
|
||||
@@ -19,7 +19,7 @@ const TxnBatchesListItem = ({ item }: Props) => {
|
||||
const timeAgo = dayjs(item.l1_timestamp).fromNow();
|
||||
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
|
||||
|
||||
<ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import type { TokenType } from 'types/api/token';
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import iconSuccess from 'icons/status/success.svg';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { useAppContext } from 'lib/appContext';
|
||||
@@ -19,6 +20,7 @@ import AddressLogs from 'ui/address/AddressLogs';
|
||||
import AddressTokens from 'ui/address/AddressTokens';
|
||||
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
|
||||
import AddressTxs from 'ui/address/AddressTxs';
|
||||
import AddressWithdrawals from 'ui/address/AddressWithdrawals';
|
||||
import TextAd from 'ui/shared/ad/TextAd';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
@@ -64,6 +66,9 @@ const AddressPageContent = () => {
|
||||
const tabs: Array<RoutedTab> = React.useMemo(() => {
|
||||
return [
|
||||
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
|
||||
appConfig.beaconChain.hasBeaconChain && addressQuery.data?.has_beacon_chain_withdrawals ?
|
||||
{ id: 'withdrawals', title: 'Withdrawals', component: <AddressWithdrawals scrollRef={ tabsScrollRef }/> } :
|
||||
undefined,
|
||||
addressQuery.data?.has_token_transfers ?
|
||||
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
|
||||
undefined,
|
||||
|
||||
+34
-8
@@ -10,10 +10,13 @@ import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import BlockDetails from 'ui/block/BlockDetails';
|
||||
import BlockWithdrawals from 'ui/block/BlockWithdrawals';
|
||||
import TextAd from 'ui/shared/ad/TextAd';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
|
||||
import TxsContent from 'ui/txs/TxsContent';
|
||||
|
||||
const TAB_LIST_PROPS = {
|
||||
@@ -42,6 +45,14 @@ const BlockPageContent = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const blockWithdrawalsQuery = useQueryWithPages({
|
||||
resourceName: 'block_withdrawals',
|
||||
pathParams: { height },
|
||||
options: {
|
||||
enabled: Boolean(blockQuery.data?.height && tab === 'withdrawals'),
|
||||
},
|
||||
});
|
||||
|
||||
if (!height) {
|
||||
throw new Error('Block not found', { cause: { status: 404 } });
|
||||
}
|
||||
@@ -53,9 +64,22 @@ const BlockPageContent = () => {
|
||||
const tabs: Array<RoutedTab> = React.useMemo(() => ([
|
||||
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
|
||||
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
|
||||
]), [ blockQuery, blockTxsQuery ]);
|
||||
blockQuery.data?.has_beacon_chain_withdrawals ?
|
||||
{ id: 'withdrawals', title: 'Withdrawals', component: <BlockWithdrawals blockWithdrawalsQuery={ blockWithdrawalsQuery }/> } :
|
||||
null,
|
||||
].filter(Boolean)), [ blockQuery, blockTxsQuery, blockWithdrawalsQuery ]);
|
||||
|
||||
const hasPagination = !isMobile && tab === 'txs' && blockTxsQuery.isPaginationVisible;
|
||||
const hasPagination = !isMobile && (
|
||||
(tab === 'txs' && blockTxsQuery.isPaginationVisible) ||
|
||||
(tab === 'withdrawals' && blockWithdrawalsQuery.isPaginationVisible)
|
||||
);
|
||||
|
||||
let pagination;
|
||||
if (tab === 'txs') {
|
||||
pagination = blockTxsQuery.pagination;
|
||||
} else if (tab === 'withdrawals') {
|
||||
pagination = blockWithdrawalsQuery.pagination;
|
||||
}
|
||||
|
||||
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks');
|
||||
|
||||
@@ -71,12 +95,14 @@ const BlockPageContent = () => {
|
||||
backLinkLabel="Back to blocks list"
|
||||
/>
|
||||
) }
|
||||
<RoutedTabs
|
||||
tabs={ tabs }
|
||||
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
|
||||
rightSlot={ hasPagination ? <Pagination { ...blockTxsQuery.pagination }/> : null }
|
||||
stickyEnabled={ hasPagination }
|
||||
/>
|
||||
{ blockQuery.isLoading ? <SkeletonTabs/> : (
|
||||
<RoutedTabs
|
||||
tabs={ tabs }
|
||||
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
|
||||
rightSlot={ hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
|
||||
stickyEnabled={ hasPagination }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import { data as withdrawalsData } from 'mocks/withdrawals/withdrawals';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
import buildApiUrl from 'playwright/utils/buildApiUrl';
|
||||
|
||||
import Withdrawals from './Withdrawals';
|
||||
|
||||
const WITHDRAWALS_API_URL = buildApiUrl('withdrawals');
|
||||
|
||||
test('base view +@mobile', async({ mount, page }) => {
|
||||
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: '',
|
||||
}));
|
||||
|
||||
await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(withdrawalsData),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Withdrawals/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component.locator('main')).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Flex, Hide, Show } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
|
||||
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
|
||||
|
||||
const Withdrawals = () => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
|
||||
resourceName: 'withdrawals',
|
||||
});
|
||||
|
||||
const content = data?.items ? (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.index } item={ item } view="list"/>)) }</Show>
|
||||
<Hide below="lg" ssr={ false }><WithdrawalsTable items={ data.items } view="list" top={ isPaginationVisible ? 80 : 0 }/></Hide>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
const actionBar = isPaginationVisible ? (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Flex alignItems="center" justifyContent="space-between" w="100%">
|
||||
{ !isMobile }
|
||||
<Pagination ml="auto" { ...pagination }/>
|
||||
</Flex>
|
||||
</ActionBar>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageTitle text="Withdrawals" withTextAd/>
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
isLoading={ isLoading }
|
||||
items={ data?.items }
|
||||
skeletonProps={{ skeletonDesktopColumns: Array(6).fill(`${ 100 / 6 }%`), skeletonDesktopMinW: '950px' }}
|
||||
emptyText="There are no withdrawals."
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Withdrawals;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,89 @@
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressWithdrawalsItem } from 'types/api/address';
|
||||
import type { BlockWithdrawalsItem } from 'types/api/block';
|
||||
import type { WithdrawalsItem } from 'types/api/withdrawals';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import blockIcon from 'icons/block.svg';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import Address from 'ui/shared/address/Address';
|
||||
import AddressIcon from 'ui/shared/address/AddressIcon';
|
||||
import AddressLink from 'ui/shared/address/AddressLink';
|
||||
import CurrencyValue from 'ui/shared/CurrencyValue';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
|
||||
|
||||
type Props = {
|
||||
item: WithdrawalsItem;
|
||||
view: 'list';
|
||||
} | {
|
||||
item: AddressWithdrawalsItem;
|
||||
view: 'address';
|
||||
} | {
|
||||
item: BlockWithdrawalsItem;
|
||||
view: 'block';
|
||||
};
|
||||
|
||||
const WithdrawalsListItem = ({ item, view }: Props) => {
|
||||
return (
|
||||
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
|
||||
|
||||
<ListItemMobileGrid.Label>Index</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
{ item.index }
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>Validator index</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
{ item.validator_index }
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ view !== 'block' && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>Block</ListItemMobileGrid.Label><ListItemMobileGrid.Value>
|
||||
<LinkInternal
|
||||
href={ route({ pathname: '/block/[height]', query: { height: item.block_number.toString() } }) }
|
||||
display="flex"
|
||||
width="fit-content"
|
||||
alignItems="center"
|
||||
>
|
||||
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
{ item.block_number }
|
||||
</LinkInternal>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ view !== 'address' && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>To</ListItemMobileGrid.Label><ListItemMobileGrid.Value>
|
||||
<Address>
|
||||
<AddressIcon address={ item.receiver }/>
|
||||
<AddressLink type="address" hash={ item.receiver.hash } truncation="dynamic" ml={ 2 }/>
|
||||
</Address>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ view !== 'block' && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
{ dayjs(item.timestamp).fromNow() }
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label>Value</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<CurrencyValue value={ item.amount } currency={ appConfig.network.currency.symbol }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithdrawalsListItem;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressWithdrawalsItem } from 'types/api/address';
|
||||
import type { BlockWithdrawalsItem } from 'types/api/block';
|
||||
import type { WithdrawalsItem } from 'types/api/withdrawals';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
|
||||
import WithdrawalsTableItem from './WithdrawalsTableItem';
|
||||
|
||||
type Props = {
|
||||
top: number;
|
||||
} & ({
|
||||
items: Array<WithdrawalsItem>;
|
||||
view: 'list';
|
||||
} | {
|
||||
items: Array<AddressWithdrawalsItem>;
|
||||
view: 'address';
|
||||
} | {
|
||||
items: Array<BlockWithdrawalsItem>;
|
||||
view: 'block';
|
||||
});
|
||||
|
||||
const WithdrawalsTable = ({ items, top, view = 'list' }: Props) => {
|
||||
return (
|
||||
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
|
||||
<Thead top={ top }>
|
||||
<Tr>
|
||||
<Th>Index</Th>
|
||||
<Th>Validator index</Th>
|
||||
{ view !== 'block' && <Th>Block</Th> }
|
||||
{ view !== 'address' && <Th>To</Th> }
|
||||
{ view !== 'block' && <Th>Age</Th> }
|
||||
<Th>{ `Value ${ appConfig.network.currency.symbol }` }</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ view === 'list' && (items as Array<WithdrawalsItem>).map((item) => (
|
||||
<WithdrawalsTableItem key={ item.index } item={ item } view="list"/>
|
||||
)) }
|
||||
{ view === 'address' && (items as Array<AddressWithdrawalsItem>).map((item) => (
|
||||
<WithdrawalsTableItem key={ item.index } item={ item } view="address"/>
|
||||
)) }
|
||||
{ view === 'block' && (items as Array<BlockWithdrawalsItem>).map((item) => (
|
||||
<WithdrawalsTableItem key={ item.index } item={ item } view="block"/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithdrawalsTable;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Td, Tr, Text, Icon } from '@chakra-ui/react';
|
||||
import { route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressWithdrawalsItem } from 'types/api/address';
|
||||
import type { BlockWithdrawalsItem } from 'types/api/block';
|
||||
import type { WithdrawalsItem } from 'types/api/withdrawals';
|
||||
|
||||
import blockIcon from 'icons/block.svg';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import Address from 'ui/shared/address/Address';
|
||||
import AddressIcon from 'ui/shared/address/AddressIcon';
|
||||
import AddressLink from 'ui/shared/address/AddressLink';
|
||||
import CurrencyValue from 'ui/shared/CurrencyValue';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
|
||||
type Props = {
|
||||
item: WithdrawalsItem;
|
||||
view: 'list';
|
||||
} | {
|
||||
item: AddressWithdrawalsItem;
|
||||
view: 'address';
|
||||
} | {
|
||||
item: BlockWithdrawalsItem;
|
||||
view: 'block';
|
||||
};
|
||||
|
||||
const WithdrawalsTableItem = ({ item, view }: Props) => {
|
||||
return (
|
||||
<Tr>
|
||||
<Td verticalAlign="middle">
|
||||
<Text>{ item.index }</Text>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Text>{ item.validator_index }</Text>
|
||||
</Td>
|
||||
{ view !== 'block' && (
|
||||
<Td verticalAlign="middle">
|
||||
<LinkInternal
|
||||
href={ route({ pathname: '/block/[height]', query: { height: item.block_number.toString() } }) }
|
||||
display="flex"
|
||||
width="fit-content"
|
||||
alignItems="center"
|
||||
>
|
||||
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
|
||||
{ item.block_number }
|
||||
</LinkInternal>
|
||||
</Td>
|
||||
) }
|
||||
{ view !== 'address' && (
|
||||
<Td verticalAlign="middle">
|
||||
<Address>
|
||||
<AddressIcon address={ item.receiver }/>
|
||||
<AddressLink type="address" hash={ item.receiver.hash } truncation="constant" ml={ 2 }/>
|
||||
</Address>
|
||||
</Td>
|
||||
) }
|
||||
{ view !== 'block' && (
|
||||
<Td verticalAlign="middle" pr={ 12 }>
|
||||
<Text variant="secondary">{ dayjs(item.timestamp).fromNow() }</Text>
|
||||
</Td>
|
||||
) }
|
||||
<Td verticalAlign="middle">
|
||||
<CurrencyValue value={ item.amount }/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithdrawalsTableItem;
|
||||
Reference in New Issue
Block a user