diff --git a/.env.template b/.env.template index b45d4b432d..1fc6572a08 100644 --- a/.env.template +++ b/.env.template @@ -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__ diff --git a/configs/app/config.ts b/configs/app/config.ts index 24895becf8..bea02d5177 100644 --- a/configs/app/config.ts +++ b/configs/app/config.ts @@ -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: '', diff --git a/docs/ENVS.md b/docs/ENVS.md index b1dc4f8e18..11dbd48085 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -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. diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 4499932c55..8dd1c28e3e 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -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 = ResourcePayload; @@ -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 : diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx index 8b6903ddde..4a29ba9345 100644 --- a/lib/hooks/useNavItems.tsx +++ b/lib/hooks/useNavItems.tsx @@ -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 = [ diff --git a/lib/next/getServerSidePropsBeacon.ts b/lib/next/getServerSidePropsBeacon.ts new file mode 100644 index 0000000000..70532ed46b --- /dev/null +++ b/lib/next/getServerSidePropsBeacon.ts @@ -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 = async(args) => { + if (!appConfig.beaconChain.hasBeaconChain) { + return { + notFound: true, + }; + } + + return getServerSidePropsBase(args); +}; diff --git a/mocks/withdrawals/withdrawals.ts b/mocks/withdrawals/withdrawals.ts new file mode 100644 index 0000000000..d2c4a4c69e --- /dev/null +++ b/mocks/withdrawals/withdrawals.ts @@ -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, + }, +}; diff --git a/pages/withdrawals.tsx b/pages/withdrawals.tsx new file mode 100644 index 0000000000..db665d67c4 --- /dev/null +++ b/pages/withdrawals.tsx @@ -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 ( + <> + + { title } + + + + ); +}; + +export default WithdrawalsPage; + +export { getServerSideProps } from 'lib/next/getServerSidePropsBeacon'; diff --git a/types/api/address.ts b/types/api/address.ts index f5a87b2fde..242e760d68 100644 --- a/types/api/address.ts +++ b/types/api/address.ts @@ -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; + next_page_params: { + index: number; + items_count: number; + }; +} + +export type AddressWithdrawalsItem = { + amount: string; + block_number: number; + index: number; + timestamp: string; + validator_index: number; +} diff --git a/types/api/block.ts b/types/api/block.ts index 9bc1d69e48..831c8749bf 100644 --- a/types/api/block.ts +++ b/types/api/block.ts @@ -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; + next_page_params: { + index: number; + items_count: number; + }; +} + +export type BlockWithdrawalsItem = { + amount: string; + index: number; + receiver: AddressParam; + validator_index: number; +} diff --git a/types/api/withdrawals.ts b/types/api/withdrawals.ts new file mode 100644 index 0000000000..c7199e1679 --- /dev/null +++ b/types/api/withdrawals.ts @@ -0,0 +1,18 @@ +import type { AddressParam } from './addressParams'; + +export type WithdrawalsResponse = { + items: Array; + 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; +} diff --git a/types/nextjs-routes.d.ts b/types/nextjs-routes.d.ts index 06292823ac..dd5faef6d4 100644 --- a/types/nextjs-routes.d.ts +++ b/types/nextjs-routes.d.ts @@ -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; diff --git a/ui/address/AddressWithdrawals.tsx b/ui/address/AddressWithdrawals.tsx new file mode 100644 index 0000000000..237fa5fb3d --- /dev/null +++ b/ui/address/AddressWithdrawals.tsx @@ -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}) => { + 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 ? ( + <> + + { data.items.map((item) => ) } + + + + + + ) : null ; + + const actionBar = isPaginationVisible ? ( + + + + ) : null; + + return ( + + ); +}; + +export default AddressWithdrawals; diff --git a/ui/block/BlockWithdrawals.tsx b/ui/block/BlockWithdrawals.tsx new file mode 100644 index 0000000000..ff8c52757f --- /dev/null +++ b/ui/block/BlockWithdrawals.tsx @@ -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 & { + pagination: PaginationProps; + isPaginationVisible: boolean; +}; + +type Props = { + blockWithdrawalsQuery: QueryResult; +} + +const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => { + const content = blockWithdrawalsQuery.data?.items ? ( + <> + + { blockWithdrawalsQuery.data.items.map((item) => ) } + + + + + + ) : null ; + + return ( + + ); +}; + +export default BlockWithdrawals; diff --git a/ui/l2TxnBatches/TxnBatchesListItem.tsx b/ui/l2TxnBatches/TxnBatchesListItem.tsx index 2728dd3e56..30155130bc 100644 --- a/ui/l2TxnBatches/TxnBatchesListItem.tsx +++ b/ui/l2TxnBatches/TxnBatchesListItem.tsx @@ -19,7 +19,7 @@ const TxnBatchesListItem = ({ item }: Props) => { const timeAgo = dayjs(item.l1_timestamp).fromNow(); return ( - + L2 block # diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index 43954092c8..ea489bb5d6 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -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 = React.useMemo(() => { return [ { id: 'txs', title: 'Transactions', component: }, + appConfig.beaconChain.hasBeaconChain && addressQuery.data?.has_beacon_chain_withdrawals ? + { id: 'withdrawals', title: 'Withdrawals', component: } : + undefined, addressQuery.data?.has_token_transfers ? { id: 'token_transfers', title: 'Token transfers', component: } : undefined, diff --git a/ui/pages/Block.tsx b/ui/pages/Block.tsx index 0a1d57c879..1ab58198d4 100644 --- a/ui/pages/Block.tsx +++ b/ui/pages/Block.tsx @@ -4,16 +4,20 @@ import React from 'react'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; +import appConfig from 'configs/app/config'; import useApiQuery from 'lib/api/useApiQuery'; import { useAppContext } from 'lib/appContext'; 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 +46,14 @@ const BlockPageContent = () => { }, }); + const blockWithdrawalsQuery = useQueryWithPages({ + resourceName: 'block_withdrawals', + pathParams: { height }, + options: { + enabled: Boolean(blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'), + }, + }); + if (!height) { throw new Error('Block not found', { cause: { status: 404 } }); } @@ -53,9 +65,22 @@ const BlockPageContent = () => { const tabs: Array = React.useMemo(() => ([ { id: 'index', title: 'Details', component: }, { id: 'txs', title: 'Transactions', component: }, - ]), [ blockQuery, blockTxsQuery ]); + appConfig.beaconChain.hasBeaconChain && blockQuery.data?.has_beacon_chain_withdrawals ? + { id: 'withdrawals', title: 'Withdrawals', component: } : + 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 +96,14 @@ const BlockPageContent = () => { backLinkLabel="Back to blocks list" /> ) } - : null } - stickyEnabled={ hasPagination } - /> + { blockQuery.isLoading ? : ( + : null } + stickyEnabled={ hasPagination } + /> + ) } ); }; diff --git a/ui/pages/Withdrawals.pw.tsx b/ui/pages/Withdrawals.pw.tsx new file mode 100644 index 0000000000..3244fd570d --- /dev/null +++ b/ui/pages/Withdrawals.pw.tsx @@ -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( + + + , + ); + + await expect(component.locator('main')).toHaveScreenshot(); +}); diff --git a/ui/pages/Withdrawals.tsx b/ui/pages/Withdrawals.tsx new file mode 100644 index 0000000000..292dfdd2a1 --- /dev/null +++ b/ui/pages/Withdrawals.tsx @@ -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 ? ( + <> + { data.items.map((item => )) } + + + ) : null; + + const actionBar = isPaginationVisible ? ( + + + { !isMobile } + + + + ) : null; + + return ( + + + + + ); +}; + +export default Withdrawals; diff --git a/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png index a4a2a8c8ac..d6bf0623c3 100644 Binary files a/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png and b/ui/pages/__screenshots__/L2TxnBatches.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Withdrawals.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/Withdrawals.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..1b24c01e0b Binary files /dev/null and b/ui/pages/__screenshots__/Withdrawals.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Withdrawals.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/Withdrawals.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..309570521d Binary files /dev/null and b/ui/pages/__screenshots__/Withdrawals.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/withdrawals/WithdrawalsListItem.tsx b/ui/withdrawals/WithdrawalsListItem.tsx new file mode 100644 index 0000000000..b37752dfe7 --- /dev/null +++ b/ui/withdrawals/WithdrawalsListItem.tsx @@ -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 ( + + + Index + + { item.index } + + + Validator index + + { item.validator_index } + + + { view !== 'block' && ( + <> + Block + + + { item.block_number } + + + + ) } + + { view !== 'address' && ( + <> + To +
+ + +
+
+ + ) } + + { view !== 'block' && ( + <> + Age + + { dayjs(item.timestamp).fromNow() } + + + Value + + + + + ) } + +
+ ); +}; + +export default WithdrawalsListItem; diff --git a/ui/withdrawals/WithdrawalsTable.tsx b/ui/withdrawals/WithdrawalsTable.tsx new file mode 100644 index 0000000000..d09b765a44 --- /dev/null +++ b/ui/withdrawals/WithdrawalsTable.tsx @@ -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; + view: 'list'; + } | { + items: Array; + view: 'address'; + } | { + items: Array; + view: 'block'; + }); + +const WithdrawalsTable = ({ items, top, view = 'list' }: Props) => { + return ( + + + + + + { view !== 'block' && } + { view !== 'address' && } + { view !== 'block' && } + + + + + { view === 'list' && (items as Array).map((item) => ( + + )) } + { view === 'address' && (items as Array).map((item) => ( + + )) } + { view === 'block' && (items as Array).map((item) => ( + + )) } + +
IndexValidator indexBlockToAge{ `Value ${ appConfig.network.currency.symbol }` }
+ ); +}; + +export default WithdrawalsTable; diff --git a/ui/withdrawals/WithdrawalsTableItem.tsx b/ui/withdrawals/WithdrawalsTableItem.tsx new file mode 100644 index 0000000000..e9ff188780 --- /dev/null +++ b/ui/withdrawals/WithdrawalsTableItem.tsx @@ -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 ( + + + { item.index } + + + { item.validator_index } + + { view !== 'block' && ( + + + + { item.block_number } + + + ) } + { view !== 'address' && ( + +
+ + +
+ + ) } + { view !== 'block' && ( + + { dayjs(item.timestamp).fromNow() } + + ) } + + + + + ); +}; + +export default WithdrawalsTableItem;