diff --git a/packages/client/.env.example b/packages/client/.env.example index 88e1c8fc..3208f12e 100644 --- a/packages/client/.env.example +++ b/packages/client/.env.example @@ -1,3 +1,4 @@ NEXT_PUBLIC_URL_API= NEXT_PUBLIC_GOOGLE_ANALYTICS= -NEXT_PUBLIC_TAG_MANAGER= \ No newline at end of file +NEXT_PUBLIC_TAG_MANAGER= +URL_API= \ No newline at end of file diff --git a/packages/client/components/layouts/Blocks.tsx b/packages/client/components/layouts/Blocks.tsx new file mode 100644 index 00000000..a92fc16c --- /dev/null +++ b/packages/client/components/layouts/Blocks.tsx @@ -0,0 +1,246 @@ +import React, { useState, useRef, useContext, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +// Axios +import axiosClient from '../../config/axios'; + +// Contexts +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; + +// Components +import LinkBlock from '../ui/LinkBlock'; +import LinkSlot from '../ui/LinkSlot'; + +// Types +import { BlockEL } from '../../types'; + +// Props +type Props = { + blocks: BlockEL[]; +}; + +const Blocks = ({ blocks }: Props) => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + // Router + const router = useRouter(); + const { network } = router.query; + + // Refs + const containerRef = useRef(null); + + // States + const [desktopView, setDesktopView] = useState(true); + const [blockGenesis, setBlockGenesis] = useState(0); + + useEffect(() => { + setDesktopView(window !== undefined && window.innerWidth > 768); + + if (network && blockGenesis == 0) { + getBlockGenesis(network as string); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleMouseMove = (e: any) => { + if (containerRef.current) { + const x = e.pageX; + const limit = 0.15; + + if (x < containerRef.current.clientWidth * limit) { + containerRef.current.scrollLeft -= 10; + } else if (x > containerRef.current.clientWidth * (1 - limit)) { + containerRef.current.scrollLeft += 10; + } + } + }; + + const getBlockGenesis = async (network: string) => { + try { + const genesisBlock = await axiosClient.get(`/api/networks/block/genesis`, { + params: { + network, + }, + }); + + setBlockGenesis(genesisBlock.data.block_genesis); + } catch (error) { + console.log(error); + } + }; + + //View blocks table desktop + const getContentBlocksDesktop = () => { + const titles = ['Block Number', 'Slot', 'Datetime', 'Transactions']; + return ( +
+
+
+ {titles.map((title, index) => ( +

+ {title} +

+ ))} +
+ + {blocks.map(element => ( +
+
+ +
+ +
+ +
+ +

+ {new Date(blockGenesis + Number(element.f_slot) * 12000).toLocaleString('ja-JP')} +

+ +

{(element.f_el_transactions ?? 0).toLocaleString()}

+
+ ))} + + {blocks.length === 0 && ( +
+

No blocks

+
+ )} +
+
+ ); + }; + //View blocks table mobile + const getContentBlocksMobile = () => { + return ( +
+ {blocks.map(block => ( +
+
+
+

+ Block number: +

+ +
+ +
+

+ Slot: +

+ +
+ +
+

+ Datetime: +

+
+

+ {new Date(blockGenesis + Number(block.f_slot) * 12000).toLocaleDateString( + 'ja-JP', + { + year: 'numeric', + month: 'numeric', + day: 'numeric', + } + )} +

+

+ {new Date(blockGenesis + Number(block.f_slot) * 12000).toLocaleTimeString( + 'ja-JP', + { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + } + )} +

+
+
+ +
+

+ Transactions: +

+

{(block.f_el_transactions ?? 0).toLocaleString()}

+
+
+
+ ))} + + {blocks.length === 0 && ( +
+

No blocks

+
+ )} +
+ ); + }; + + return
{desktopView ? getContentBlocksDesktop() : getContentBlocksMobile()}
; +}; + +export default Blocks; diff --git a/packages/client/components/layouts/Statitstics.tsx b/packages/client/components/layouts/Statitstics.tsx index d2b3d097..39b8366a 100644 --- a/packages/client/components/layouts/Statitstics.tsx +++ b/packages/client/components/layouts/Statitstics.tsx @@ -583,14 +583,14 @@ const Statitstics = ({ showCalculatingEpochs }: Props) => { title='Target' color='#343434' backgroundColor='#f5f5f5' - percent={1 - epoch.f_missing_target / epoch.f_num_vals} + percent={1 - epoch.f_missing_target / epoch.f_num_active_vals} tooltipColor='orange' tooltipContent={ <> Missing Target: {epoch.f_missing_target?.toLocaleString()} - Attestations: {epoch.f_num_vals?.toLocaleString()} + Attestations: {epoch.f_num_active_vals?.toLocaleString()} } widthTooltip={220} @@ -601,14 +601,14 @@ const Statitstics = ({ showCalculatingEpochs }: Props) => { title='Source' color='#343434' backgroundColor='#f5f5f5' - percent={1 - epoch.f_missing_source / epoch.f_num_vals} + percent={1 - epoch.f_missing_source / epoch.f_num_active_vals} tooltipColor='blue' tooltipContent={ <> Missing Source: {epoch.f_missing_source?.toLocaleString()} - Attestations: {epoch.f_num_vals?.toLocaleString()} + Attestations: {epoch.f_num_active_vals?.toLocaleString()} } widthTooltip={220} @@ -619,12 +619,12 @@ const Statitstics = ({ showCalculatingEpochs }: Props) => { title='Head' color='#343434' backgroundColor='#f5f5f5' - percent={1 - epoch.f_missing_head / epoch.f_num_vals} + percent={1 - epoch.f_missing_head / epoch.f_num_active_vals} tooltipColor='purple' tooltipContent={ <> Missing Head: {epoch.f_missing_head?.toLocaleString()} - Attestations: {epoch.f_num_vals?.toLocaleString()} + Attestations: {epoch.f_num_active_vals?.toLocaleString()} } widthTooltip={220} @@ -823,12 +823,12 @@ const Statitstics = ({ showCalculatingEpochs }: Props) => { title='Target' color='#343434' backgroundColor='#f5f5f5' - percent={1 - epoch.f_missing_target / epoch.f_num_vals} + percent={1 - epoch.f_missing_target / epoch.f_num_active_vals} tooltipColor='orange' tooltipContent={ <> Missing Target: {epoch.f_missing_target?.toLocaleString()} - Attestations: {epoch.f_num_vals?.toLocaleString()} + Attestations: {epoch.f_num_active_vals?.toLocaleString()} } widthTooltip={220} @@ -838,12 +838,12 @@ const Statitstics = ({ showCalculatingEpochs }: Props) => { title='Source' color='#343434' backgroundColor='#f5f5f5' - percent={1 - epoch.f_missing_source / epoch.f_num_vals} + percent={1 - epoch.f_missing_source / epoch.f_num_active_vals} tooltipColor='blue' tooltipContent={ <> Missing Source: {epoch.f_missing_source?.toLocaleString()} - Attestations: {epoch.f_num_vals?.toLocaleString()} + Attestations: {epoch.f_num_active_vals?.toLocaleString()} } widthTooltip={220} @@ -853,12 +853,12 @@ const Statitstics = ({ showCalculatingEpochs }: Props) => { title='Head' color='#343434' backgroundColor='#f5f5f5' - percent={1 - epoch.f_missing_head / epoch.f_num_vals} + percent={1 - epoch.f_missing_head / epoch.f_num_active_vals} tooltipColor='purple' tooltipContent={ <> Missing Head: {epoch.f_missing_head?.toLocaleString()} - Attestations: {epoch.f_num_vals?.toLocaleString()} + Attestations: {epoch.f_num_active_vals?.toLocaleString()} } widthTooltip={220} @@ -902,7 +902,7 @@ const Statitstics = ({ showCalculatingEpochs }: Props) => { title='Attesting/Total active' color='#343434' backgroundColor='#f5f5f5' - percent={epoch.f_num_att_vals / epoch.f_num_vals} + percent={epoch.f_num_att_vals / epoch.f_num_active_vals} tooltipColor='bluedark' tooltipContent={ <> diff --git a/packages/client/components/layouts/Transactions.tsx b/packages/client/components/layouts/Transactions.tsx new file mode 100644 index 00000000..d31b7fd8 --- /dev/null +++ b/packages/client/components/layouts/Transactions.tsx @@ -0,0 +1,384 @@ +import React, { useContext, useRef, useEffect, useState } from 'react'; + +// Contexts +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; + +// Hooks +import useLargeView from '../../hooks/useLargeView'; + +// Components +import TooltipContainer from '../../components/ui/TooltipContainer'; +import TooltipResponsive from '../../components/ui/TooltipResponsive'; +import CustomImage from '../ui/CustomImage'; +import Loader from '../ui/Loader'; +import LinkTransaction from '../ui/LinkTransaction'; +import CopyIcon from '../ui/CopyIcon'; + +// Helpers +import { getShortAddress } from '../../helpers/addressHelper'; +import { getTimeAgo } from '../../helpers/timeHelper'; + +// Types +import { Transaction } from '../../types'; + +// Props +type Props = { + transactions: Transaction[]; + loadingTransactions: boolean; +}; + +const Transactions = ({ transactions, loadingTransactions }: Props) => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + // Refs + const containerRef = useRef(null); + + // Large View Hook + const largeView = useLargeView(); + + // States + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + // Function to handle the Mouse Move event + const handleMouseMove = (e: any) => { + if (containerRef.current) { + const x = e.pageX; + const limit = 0.15; + + if (x < containerRef.current.clientWidth * limit) { + containerRef.current.scrollLeft -= 10; + } else if (x > containerRef.current.clientWidth * (1 - limit)) { + containerRef.current.scrollLeft += 10; + } + } + }; + + //Transactions tab - table desktop + const getTransactionsDesktop = () => { + return ( +
+
+
+

Txn Hash

+ + + + The hash of the transaction} + top='34px' + polygonLeft + /> + +
+
+

Age

+ + + + + How long ago + the transaction passed + + } + top='34px' + /> + +
+

From

+
+

To

+
+

Value

+ + + + + How much ETH + was sent + in the transaction + + } + top='34px' + /> + +
+
+

Txn Fee

+ + + + + The fee + the transaction cost + + } + top='34px' + polygonRight + /> + +
+
+ +
+ {transactions.map(element => ( +
+
+ + + +
+ +

{getTimeAgo(element.f_timestamp * 1000)}

+ +
+ +

{getShortAddress(element.f_from)}

+
+ +
+ +
+ +
+ +

{getShortAddress(element.f_to)}

+
+ +

+ {(element.f_value / 10 ** 18).toLocaleString()} ETH +

+

+ {(element.f_gas_fee_cap / 10 ** 9).toLocaleString()} GWEI +

+
+ ))} + + {!loadingTransactions && transactions.length === 0 && ( +
+

No transactions

+
+ )} +
+ + {loadingTransactions && ( +
+ +
+ )} +
+ ); + }; + + //Transactions tab - table mobile + const getTransactionsMobile = () => { + return ( +
+
+ {transactions.map(element => ( +
+
+

+ Txn Hash +

+ +
+ + +
+
+
+

+ Age +

+

{getTimeAgo(element.f_timestamp * 1000)}

+
+
+

+ From +

+

+ To +

+
+
+
+ +

{getShortAddress(element?.f_from)}

+
+ +
+ +

{getShortAddress(element?.f_to)}

+
+
+
+

+ Value +

+

{(element.f_value / 10 ** 18).toLocaleString()} ETH

+
+
+

+ Txn Fee +

+

{(element.f_gas_fee_cap / 10 ** 9).toLocaleString()} GWEI

+
+
+ ))} + + {!loadingTransactions && transactions.length === 0 && ( +
+

No transactions

+
+ )} +
+ + {loadingTransactions && ( +
+ +
+ )} +
+ ); + }; + + if (!isClient) { + return null; + } + + return <>{largeView ? getTransactionsDesktop() : getTransactionsMobile()}; +}; + +export default Transactions; diff --git a/packages/client/components/ui/CopyIcon.tsx b/packages/client/components/ui/CopyIcon.tsx new file mode 100644 index 00000000..7196de8b --- /dev/null +++ b/packages/client/components/ui/CopyIcon.tsx @@ -0,0 +1,43 @@ +import React, { useState, useContext } from 'react'; + +// Contexts +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; + +// Components +import CustomImage from './CustomImage'; + +// Props +type Props = { + value: string; +}; + +const CopyIcon = ({ value }: Props) => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + // States + const [copied, setCopied] = useState(false); + + // Function to handle the Copy Click event + const handleCopyClick = async () => { + await navigator.clipboard.writeText(value); + setCopied(true); + + setTimeout(() => { + setCopied(false); + }, 250); + }; + + return ( + handleCopyClick()} + /> + ); +}; + +export default CopyIcon; diff --git a/packages/client/components/ui/CustomImage.tsx b/packages/client/components/ui/CustomImage.tsx index f736a0ac..844ec649 100644 --- a/packages/client/components/ui/CustomImage.tsx +++ b/packages/client/components/ui/CustomImage.tsx @@ -8,10 +8,11 @@ type Props = { height: number; className?: string; priority?: boolean; + title?: string; onClick?: () => void; }; -const CustomImage = ({ src, alt, width, height, className, priority, onClick }: Props) => { +const CustomImage = ({ src, alt, width, height, className, priority, title, onClick }: Props) => { const assetPrefix = process.env.NEXT_PUBLIC_ASSET_PREFIX ?? ''; return ( @@ -22,6 +23,7 @@ const CustomImage = ({ src, alt, width, height, className, priority, onClick }: height={height} className={className} priority={priority} + title={title} onClick={onClick} /> ); diff --git a/packages/client/components/ui/EntityCard.tsx b/packages/client/components/ui/EntityCard.tsx index a8ca4a88..0d9e2c26 100644 --- a/packages/client/components/ui/EntityCard.tsx +++ b/packages/client/components/ui/EntityCard.tsx @@ -26,11 +26,10 @@ const EntityCard = ({ index, pool }: Props) => { return (
diff --git a/packages/client/components/ui/LinkBlock.tsx b/packages/client/components/ui/LinkBlock.tsx new file mode 100644 index 00000000..7d48cc6a --- /dev/null +++ b/packages/client/components/ui/LinkBlock.tsx @@ -0,0 +1,43 @@ +import React, { useContext } from 'react'; + +// Contexts +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; + +// Components +import LinkIcon from './LinkIcon'; +import NetworkLink from './NetworkLink'; + +// Types +type Props = { + block: number | undefined; + children?: React.ReactNode; + mxAuto?: boolean; +}; + +const LinkBlock = ({ block, children, mxAuto }: Props) => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + const baseStyle = { + color: themeMode?.darkMode ? 'var(--purple)' : 'var(--darkPurple)', + }; + return ( + + {children ?? ( + <> +

{block?.toLocaleString()}

+ + + )} +
+ ); +}; + +export default LinkBlock; diff --git a/packages/client/components/ui/LinkSlot.tsx b/packages/client/components/ui/LinkSlot.tsx index 5b62d157..e51ebaa5 100644 --- a/packages/client/components/ui/LinkSlot.tsx +++ b/packages/client/components/ui/LinkSlot.tsx @@ -26,7 +26,7 @@ const LinkSlot = ({ slot, children, mxAuto }: Props) => { { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + const baseStyle = { + color: themeMode?.darkMode ? 'var(--purple)' : 'var(--darkPurple)', + }; + + return ( + + {children ?? ( + <> + {getShortAddress(hash)} + + + )} + + ); +}; + +export default LinkTransaction; diff --git a/packages/client/components/ui/Menu.tsx b/packages/client/components/ui/Menu.tsx index d033d671..8d644fb4 100644 --- a/packages/client/components/ui/Menu.tsx +++ b/packages/client/components/ui/Menu.tsx @@ -18,7 +18,7 @@ const Menu = () => { const [networks, setNetworks] = useState([]); useEffect(() => { - if (!networks || networks.length === 0) { + if (networks.length === 0) { getNetworks(); } @@ -56,13 +56,21 @@ const Menu = () => { name: 'Validators', route: '/validators', }, + { + name: 'Blocks', + route: '/blocks', + }, + { + name: 'Transactions', + route: '/transactions', + }, ], Networks: networks.length > 0 ? networks.map((network: string) => { return { name: network.charAt(0).toUpperCase() + network.slice(1), - route: `/${network}`, + route: `/?network=${network}`, }; }) : [], diff --git a/packages/client/components/ui/NetworkLink.tsx b/packages/client/components/ui/NetworkLink.tsx index f24398c5..ab80856c 100644 --- a/packages/client/components/ui/NetworkLink.tsx +++ b/packages/client/components/ui/NetworkLink.tsx @@ -17,16 +17,16 @@ const NetworkLink = ({ children, href, ...rest }: Props) => { let adjustedHref = href; if (typeof href === 'string') { - adjustedHref = network ? `/${network}${href}` : href; + adjustedHref = network ? `${href}?network=${network}` : href; } else if (typeof href === 'object' && href.pathname) { adjustedHref = { ...href, - pathname: network ? `/${network}${href.pathname}` : href.pathname, + pathname: network ? `${href.pathname}?network=${network}` : href.pathname, }; } return ( - + {children} ); diff --git a/packages/client/components/ui/SearchEngine.tsx b/packages/client/components/ui/SearchEngine.tsx index 79275556..c34485ef 100644 --- a/packages/client/components/ui/SearchEngine.tsx +++ b/packages/client/components/ui/SearchEngine.tsx @@ -136,6 +136,14 @@ const SearchEngine = () => { }); } + if (searchContent.startsWith('0x')) { + // It can be a transaction + items.push({ + label: `Transaction: ${searchContent}`, + link: `/transaction/${searchContent}`, + }); + } + // It can be an entity const expression = new RegExp(searchContent, 'i'); diff --git a/packages/client/components/ui/TooltipResponsive.tsx b/packages/client/components/ui/TooltipResponsive.tsx index e225a250..4f6a235e 100644 --- a/packages/client/components/ui/TooltipResponsive.tsx +++ b/packages/client/components/ui/TooltipResponsive.tsx @@ -39,7 +39,7 @@ const TooltipResponsive = ({ width, backgroundColor, colorLetter, content, top, return (
`${value} ${unit}${value > 1 ? 's' : ''}`; + + // Function to get the days + const getDays = (value: number) => pluralize(value, 'day'); + + // Function to get the hours + const getHours = (value: number) => pluralize(value, 'hr'); + + // Function to get the minutes + const getMinutes = (value: number) => pluralize(value, 'min'); + + // Function to get the seconds + const getSeconds = (value: number) => pluralize(value, 'sec'); + + if (days > 0) { + const hoursMinusDays = hours - days * 24; + return `${getDays(days)} ${getHours(hoursMinusDays)} ago`; + } + + if (hours > 0) { + const minutesMinusHours = minutes - hours * 60; + return `${getHours(hours)} ${getMinutes(minutesMinusHours)} ago`; + } + + if (minutes > 0) { + const secondsMinusMinutes = seconds - minutes * 60; + return `${getMinutes(minutes)} ${getSeconds(secondsMinusMinutes)} ago`; + } + + return `${getSeconds(seconds)} ago`; +} diff --git a/packages/client/hooks/useLargeView.ts b/packages/client/hooks/useLargeView.ts new file mode 100644 index 00000000..ab152ca9 --- /dev/null +++ b/packages/client/hooks/useLargeView.ts @@ -0,0 +1,20 @@ +import { useState, useEffect } from 'react'; + +const useLargeView = () => { + const [largeView, setLargeView] = useState(true); + + useEffect(() => { + const handleResize = () => { + setLargeView(window.innerWidth > 768); + }; + + handleResize(); + window.addEventListener('resize', handleResize); + + return () => window.removeEventListener('resize', handleResize); + }, []); + + return largeView; +}; + +export default useLargeView; diff --git a/packages/client/middleware.ts b/packages/client/middleware.ts index 6786caa9..f2748f18 100644 --- a/packages/client/middleware.ts +++ b/packages/client/middleware.ts @@ -1,38 +1,25 @@ import { NextRequest, NextResponse } from 'next/server'; -// Cache for the networks -let networksCache: string[] | null = null; -let defaultNetworkCache: string | null = null; - -export async function fetchNetworks() { - if (!networksCache) { - try { - const response = await fetch(`${process.env.API_URL}/api/networks`); - const networksData = await response.json(); - networksCache = networksData.networks; - if ((networksCache as string[]).length > 0) { - defaultNetworkCache = (networksCache as string[])[0]; - } - } catch (err) { - console.error('Error fetching networks:', err); - } +export async function fetchNetworks(): Promise<{ networks: string[]; defaultNetwork: string | null }> { + try { + const response = await fetch(`${process.env.URL_API}/api/networks`); + const networksData = await response.json(); + + return { + networks: networksData.networks, + defaultNetwork: networksData.networks ? networksData.networks[0] : null, + }; + } catch (err) { + console.error('Error fetching networks:', err); + throw new Error('Error fetching networks'); } - return { networks: networksCache, default_network: defaultNetworkCache }; } export async function middleware(req: NextRequest) { - const { networks, default_network } = await fetchNetworks(); - - const pathsWithoutNetwork = [ - '/entity', - '/entities', - '/epoch', - '/epochs', - '/slot', - '/slots', - '/validator', - '/validators', - ]; + if (req.nextUrl.pathname.includes('_next') || req.nextUrl.pathname.includes('static')) return NextResponse.next(); + + const { networks, defaultNetwork } = await fetchNetworks(); + const oldPathsToReplace = [ { singular: 'entity', @@ -56,7 +43,18 @@ export async function middleware(req: NextRequest) { }, ]; - const isPathWithoutNetwork = pathsWithoutNetwork.find(item => req.nextUrl.pathname.startsWith(item)); + let network = req.nextUrl.searchParams.get('network'); + + const networkInsidePath = networks?.find(item => req.nextUrl.pathname.includes(`/${item}/`)); + + let mustRedirect = false; + + if (networkInsidePath) { + network = networkInsidePath; + req.nextUrl.pathname = req.nextUrl.pathname.replace(`/${network}/`, '/'); + req.nextUrl.searchParams.set('network', network); + mustRedirect = true; + } const hasOldPath = oldPathsToReplace.filter( @@ -67,34 +65,35 @@ export async function middleware(req: NextRequest) { !req.nextUrl.pathname.includes('graffitis')) ).length > 0; - const replaceOldPaths = (url: string) => { + const replaceOldPaths = (pathname: string) => { oldPathsToReplace.forEach(item => { - if (url.includes(`/${item.plural}/`)) { - url = url.replace(`/${item.plural}/`, `/${item.singular}/`); - } else if (url.endsWith(`/${item.singular}`) && !url.includes('graffiti') && !url.includes('graffitis')) { - url = url.replace(`/${item.singular}`, `/${item.plural}`); + if (pathname.includes(`/${item.plural}/`)) { + pathname = pathname.replace(`/${item.plural}/`, `/${item.singular}/`); + } else if ( + pathname.endsWith(`/${item.singular}`) && + !pathname.includes('graffiti') && + !pathname.includes('graffitis') + ) { + pathname = pathname.replace(`/${item.singular}`, `/${item.plural}`); } }); - return url; + return pathname; }; - if (isPathWithoutNetwork || req.nextUrl.pathname === '/') { - let newUrl = `${req.nextUrl.origin}/${default_network}${req.nextUrl.pathname}`; + if (!network || !networks?.includes(network)) { + req.nextUrl.searchParams.set('network', defaultNetwork as string); if (hasOldPath) { - newUrl = replaceOldPaths(newUrl); + req.nextUrl.pathname = replaceOldPaths(req.nextUrl.pathname); } - return NextResponse.redirect(newUrl); + return NextResponse.redirect(req.nextUrl); } else if (hasOldPath) { - return NextResponse.redirect(replaceOldPaths(req.nextUrl.href)); - } else if (!req.nextUrl.pathname.includes('_next') && !req.nextUrl.pathname.includes('static')) { - const network = req.nextUrl.pathname.split('/')[1]; - - if (!networks?.includes(network)) { - return NextResponse.redirect(`${req.nextUrl.origin}/${default_network}`); - } + req.nextUrl.pathname = replaceOldPaths(req.nextUrl.pathname); + return NextResponse.redirect(req.nextUrl); + } else if (mustRedirect) { + return NextResponse.redirect(req.nextUrl); } return NextResponse.next(); diff --git a/packages/client/pages/block/[id].tsx b/packages/client/pages/block/[id].tsx new file mode 100644 index 00000000..7a10c36d --- /dev/null +++ b/packages/client/pages/block/[id].tsx @@ -0,0 +1,357 @@ +import React, { useEffect, useState, useRef, useCallback, useContext } from 'react'; +import { useRouter } from 'next/router'; +import Head from 'next/head'; + +// Axios +import axiosClient from '../../config/axios'; + +// Contexts +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; + +// Components +import Layout from '../../components/layouts/Layout'; +import TabHeader from '../../components/ui/TabHeader'; +import Loader from '../../components/ui/Loader'; +import LinkSlot from '../../components/ui/LinkSlot'; +import Arrow from '../../components/ui/Arrow'; +import LinkBlock from '../../components/ui/LinkBlock'; +import Transactions from '../../components/layouts/Transactions'; + +// Types +import { BlockEL, Transaction } from '../../types'; + +type CardProps = { + title: string; + text?: string; + content?: React.ReactNode; +}; + +//Card style +const Card = ({ title, text, content }: CardProps) => { + // Theme Mode Context + const { themeMode } = React.useContext(ThemeModeContext) ?? {}; + return ( +
+

+ {title}: +

+
+ {text && ( +

+ {text} +

+ )} + + {content && <>{content}} +
+
+ ); +}; + +const BlockPage = () => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + // Next router + const router = useRouter(); + const { network, id } = router.query; + + // Refs + const slotRef = useRef(0); + const existsBlockRef = useRef(true); + + // States + const [block, setBlock] = useState(null); + const [transactions, setTransactions] = useState>([]); + const [existsBlock, setExistsBlock] = useState(true); + const [countdownText, setCountdownText] = useState(''); + const [tabPageIndex, setTabPageIndex] = useState(0); + const [loadingBlock, setLoadingBlock] = useState(true); + const [loadingTransactions, setLoadingTransactions] = useState(true); + const [blockGenesis, setBlockGenesis] = useState(0); + + // UseEffect + useEffect(() => { + if (id) { + slotRef.current = Number(id); + } + + if (network && ((id && !block) || (block && block.f_slot !== Number(id)))) { + getBlock(); + getTransactions(); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [network, id]); + + const shuffle = useCallback(() => { + const text: string = getCountdownText(); + setCountdownText(text); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const intervalID = setInterval(shuffle, 1000); + return () => clearInterval(intervalID); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shuffle, slotRef.current]); + + // Get blocks + const getBlock = async () => { + try { + setLoadingBlock(true); + + const [response, genesisBlock] = await Promise.all([ + axiosClient.get(`/api/blocks/${id}`, { + params: { + network, + }, + }), + axiosClient.get(`/api/networks/block/genesis`, { + params: { + network, + }, + }), + ]); + + const blockResponse: BlockEL = response.data.block; + setBlock(blockResponse); + setBlockGenesis(genesisBlock.data.block_genesis); + + if (!blockResponse) { + const expectedTimestamp = (genesisBlock.data.block_genesis + Number(id) * 12000) / 1000; + + setBlock({ + f_epoch: Math.floor(Number(id) / 32), + f_slot: Number(id), + f_timestamp: expectedTimestamp, + }); + + setExistsBlock(false); + existsBlockRef.current = false; + + const timeDifference = new Date(expectedTimestamp * 1000).getTime() - new Date().getTime(); + + if (timeDifference > 0) { + setTimeout(() => { + if (Number(id) === slotRef.current) { + getBlock(); + } + }, timeDifference + 2000); + } else if (timeDifference > -10000) { + setTimeout(() => { + if (Number(id) === slotRef.current) { + getBlock(); + } + }, 1000); + } + } else { + setExistsBlock(true); + existsBlockRef.current = true; + } + } catch (error) { + console.log(error); + } finally { + setLoadingBlock(false); + } + }; + + // Get transactions + const getTransactions = async () => { + try { + setLoadingTransactions(true); + + const response = await axiosClient.get(`/api/blocks/${id}/transactions`, { + params: { + network, + }, + }); + + setTransactions(response.data.transactions); + } catch (error) { + console.log(error); + } finally { + setLoadingTransactions(false); + } + }; + + // Get Short Address + const getShortAddress = (address: string | undefined) => { + if (typeof address === 'string') { + return `${address.slice(0, 6)}...${address.slice(address.length - 6, address.length)}`; + } else { + return 'Invalid Address'; + } + }; + + // Get Time Block + const getTimeBlock = () => { + let text; + + if (block) { + if (block.f_timestamp) { + text = new Date(block.f_timestamp * 1000).toLocaleString('ja-JP'); + } else { + text = new Date(blockGenesis + Number(id) * 12000).toLocaleString('ja-JP'); + } + } + + return text + countdownText; + }; + + // Get Countdown Text + const getCountdownText = () => { + let text = ''; + + if (!existsBlockRef.current) { + const expectedTimestamp = (blockGenesis + slotRef.current * 12000) / 1000; + const timeDifference = new Date(expectedTimestamp * 1000).getTime() - new Date().getTime(); + + const minutesMiliseconds = 1000 * 60; + const hoursMiliseconds = minutesMiliseconds * 60; + const daysMiliseconds = hoursMiliseconds * 24; + const yearsMiliseconds = daysMiliseconds * 365; + + if (timeDifference > yearsMiliseconds) { + const years = Math.floor(timeDifference / yearsMiliseconds); + text = ` (in ${years} ${years > 1 ? 'years' : 'year'})`; + } else if (timeDifference > daysMiliseconds) { + const days = Math.floor(timeDifference / daysMiliseconds); + text = ` (in ${days} ${days > 1 ? 'days' : 'day'})`; + } else if (timeDifference > hoursMiliseconds) { + const hours = Math.floor(timeDifference / hoursMiliseconds); + text = ` (in ${hours} ${hours > 1 ? 'hours' : 'hour'})`; + } else if (timeDifference > minutesMiliseconds) { + const minutes = Math.floor(timeDifference / minutesMiliseconds); + text = ` (in ${minutes} ${minutes > 1 ? 'minutes' : 'minute'})`; + } else if (timeDifference > 1000) { + const seconds = Math.floor(timeDifference / 1000); + text = ` (in ${seconds} ${seconds > 1 ? 'seconds' : 'second'})`; + } else if (timeDifference < -10000) { + text = ' (data not saved)'; + } else { + text = ' (updating...)'; + } + } + + return text; + }; + + //TABLE + //TABS + const getSelectedTab = () => { + switch (tabPageIndex) { + case 0: + return getOverview(); + + case 1: + return ; + } + }; + //TABS - Overview & withdrawals + const getInformationView = () => { + return ( +
+
+ setTabPageIndex(0)} /> + {existsBlock && ( + setTabPageIndex(1)} + /> + )} +
+ {getSelectedTab()} +
+ ); + }; + + //%Gas usage / limit + const percentGas = (a: number, b: number) => { + return (a / b) * 100; + }; + + //Overview tab - table + const getOverview = () => { + return ( +
+ {/* Table */} +
+ + } /> + + + + + + +
+
+ ); + }; + + //OVERVIEW BLOCK PAGE + return ( + + + + + + {/* Header */} +
+ + + + +

+ Block {Number(id)?.toLocaleString()} +

+ + + + +
+ + {loadingBlock && ( +
+ +
+ )} + + {block?.f_slot && !loadingBlock && getInformationView()} +
+ ); +}; + +export default BlockPage; diff --git a/packages/client/pages/blocks.tsx b/packages/client/pages/blocks.tsx new file mode 100644 index 00000000..f44b4a87 --- /dev/null +++ b/packages/client/pages/blocks.tsx @@ -0,0 +1,117 @@ +import React, { useState, useEffect, useContext } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; + +// Axios +import axiosClient from '../config/axios'; + +// Contexts +import ThemeModeContext from '../contexts/theme-mode/ThemeModeContext'; + +// Components +import Layout from '../components/layouts/Layout'; +import BlockList from '../components/layouts/Blocks'; +import Loader from '../components/ui/Loader'; +import ViewMoreButton from '../components/ui/ViewMoreButton'; + +// Types +import { BlockEL } from '../types'; + +const Blocks = () => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + // Router + const router = useRouter(); + const { network } = router.query; + + // States + const [blocks, setBlocks] = useState([]); + const [currentPage, setCurrentPage] = useState(0); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (network && blocks.length === 0) { + getBlocks(0); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [network]); + + const getBlocks = async (page: number) => { + try { + setLoading(true); + + setCurrentPage(page); + + const response = await axiosClient.get(`/api/blocks`, { + params: { + network, + page, + limit: 32, + }, + }); + + setBlocks(prevState => [ + ...prevState, + ...response.data.blocks.filter( + (block: BlockEL) => !prevState.find((prevBlock: BlockEL) => prevBlock.f_slot === block.f_slot) + ), + ]); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }; + + return ( + + + Blocks of the Ethereum Chain - EthSeer.io + + + + + +

+ Ethereum Blocks +

+ +
+

+ Blocks are the fundamental unit of consensus for blockchains. In it you will find a number of + transactions and interactions with smart contracts. +

+
+ +
{blocks.length > 0 && }
+ + {loading && ( +
+ +
+ )} + + {blocks.length > 0 && getBlocks(currentPage + 1)} />} +
+ ); +}; + +export default Blocks; diff --git a/packages/client/pages/[network]/entities.tsx b/packages/client/pages/entities.tsx similarity index 66% rename from packages/client/pages/[network]/entities.tsx rename to packages/client/pages/entities.tsx index 29994bad..54693687 100644 --- a/packages/client/pages/[network]/entities.tsx +++ b/packages/client/pages/entities.tsx @@ -1,23 +1,25 @@ import React, { useContext, useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; import Head from 'next/head'; +// Axios +import axiosClient from '../config/axios'; + // Contexts -import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; +import ThemeModeContext from '../contexts/theme-mode/ThemeModeContext'; // Components -import Layout from '../../components/layouts/Layout'; -import EntityCard from '../../components/ui/EntityCard'; - -// Constants -import { useRouter } from 'next/router'; -import axiosClient from '../../config/axios'; -import Loader from '../../components/ui/Loader'; -import Animation from '../../components/layouts/Animation'; +import Layout from '../components/layouts/Layout'; +import EntityCard from '../components/ui/EntityCard'; +import CustomImage from '../components/ui/CustomImage'; +import Loader from '../components/ui/Loader'; +import Animation from '../components/layouts/Animation'; type Entity = { f_pool_name: string; act_number_validators: number; }; + const Entities = () => { // Theme Mode Context const { themeMode } = useContext(ThemeModeContext) ?? {}; @@ -95,7 +97,7 @@ const Entities = () => { style={{ background: themeMode?.darkMode ? 'var(--bgDarkMode)' : 'var(--bgMainLightMode)' }} >

{ deposit address analysis, among others. EthSeer also monitors their performance.

+
+
+

+ This is a card example with the entity information you'll find: +

+
+ +
+ Entity name + ordered by + Number of validators +
+
+
+
{loading && (
diff --git a/packages/client/pages/[network]/entity/[name].tsx b/packages/client/pages/entity/[name].tsx similarity index 97% rename from packages/client/pages/[network]/entity/[name].tsx rename to packages/client/pages/entity/[name].tsx index 83d3385e..66b81299 100644 --- a/packages/client/pages/[network]/entity/[name].tsx +++ b/packages/client/pages/entity/[name].tsx @@ -2,21 +2,20 @@ import React, { useContext, useEffect, useState } from 'react'; import { useRouter } from 'next/router'; // Axios -import axiosClient from '../../../config/axios'; +import axiosClient from '../../config/axios'; // Contexts -import ThemeModeContext from '../../../contexts/theme-mode/ThemeModeContext'; +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; // Components -import Layout from '../../../components/layouts/Layout'; -import BlockGif from '../../../components/ui/BlockGif'; -import Animation from '../../../components/layouts/Animation'; -import Loader from '../../../components/ui/Loader'; -import ProgressSmoothBar from '../../../components/ui/ProgressSmoothBar'; -import TabHeader from '../../../components/ui/TabHeader'; +import Layout from '../../components/layouts/Layout'; +import Animation from '../../components/layouts/Animation'; +import Loader from '../../components/ui/Loader'; +import ProgressSmoothBar from '../../components/ui/ProgressSmoothBar'; +import TabHeader from '../../components/ui/TabHeader'; // Types -import { Entity } from '../../../types'; +import { Entity } from '../../types'; type Props = { content: string; diff --git a/packages/client/pages/[network]/epoch/[id].tsx b/packages/client/pages/epoch/[id].tsx similarity index 95% rename from packages/client/pages/[network]/epoch/[id].tsx rename to packages/client/pages/epoch/[id].tsx index 6538223e..e0cc7633 100644 --- a/packages/client/pages/[network]/epoch/[id].tsx +++ b/packages/client/pages/epoch/[id].tsx @@ -3,22 +3,22 @@ import { useRouter } from 'next/router'; import Head from 'next/head'; // Axios -import axiosClient from '../../../config/axios'; +import axiosClient from '../../config/axios'; // Contexts -import ThemeModeContext from '../../../contexts/theme-mode/ThemeModeContext'; +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; // Components -import Layout from '../../../components/layouts/Layout'; -import ProgressSmoothBar from '../../../components/ui/ProgressSmoothBar'; -import EpochAnimation from '../../../components/layouts/EpochAnimation'; -import Loader from '../../../components/ui/Loader'; -import LinkEpoch from '../../../components/ui/LinkEpoch'; -import Slots from '../../../components/layouts/Slots'; -import Arrow from '../../../components/ui/Arrow'; +import Layout from '../../components/layouts/Layout'; +import ProgressSmoothBar from '../../components/ui/ProgressSmoothBar'; +import EpochAnimation from '../../components/layouts/EpochAnimation'; +import Loader from '../../components/ui/Loader'; +import LinkEpoch from '../../components/ui/LinkEpoch'; +import Slots from '../../components/layouts/Slots'; +import Arrow from '../../components/ui/Arrow'; // Types -import { Epoch, Slot } from '../../../types'; +import { Epoch, Slot } from '../../types'; type Props = { content: string; diff --git a/packages/client/pages/[network]/epochs.tsx b/packages/client/pages/epochs.tsx similarity index 91% rename from packages/client/pages/[network]/epochs.tsx rename to packages/client/pages/epochs.tsx index 4bced28a..c3bb369f 100644 --- a/packages/client/pages/[network]/epochs.tsx +++ b/packages/client/pages/epochs.tsx @@ -2,11 +2,11 @@ import React, { useContext } from 'react'; import Head from 'next/head'; // Contexts -import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; +import ThemeModeContext from '../contexts/theme-mode/ThemeModeContext'; // Components -import Layout from '../../components/layouts/Layout'; -import Statitstics from '../../components/layouts/Statitstics'; +import Layout from '../components/layouts/Layout'; +import Statitstics from '../components/layouts/Statitstics'; const Epochs = () => { // Theme Mode Context @@ -41,7 +41,7 @@ const Epochs = () => { style={{ background: themeMode?.darkMode ? 'var(--bgDarkMode)' : 'var(--bgMainLightMode)' }} >

{ // Theme Mode Context const { themeMode } = React.useContext(ThemeModeContext) ?? {}; @@ -205,6 +204,7 @@ const Slot = () => { return address && `${address.slice(0, 6)}...${address.slice(address.length - 6, address.length)}`; }; + // Get Time Block const getTimeBlock = () => { let text; @@ -219,6 +219,7 @@ const Slot = () => { return text + countdownText; }; + // Get Countdown Text const getCountdownText = () => { let text = ''; @@ -256,6 +257,7 @@ const Slot = () => { return text; }; + // Get Handle Mouse const handleMouseMove = (e: any) => { if (containerRef.current) { const x = e.pageX; @@ -269,53 +271,42 @@ const Slot = () => { } }; - //Tabs + //TABLE + + //TABS const getSelectedTab = () => { switch (tabPageIndex) { case 0: - return getConsensusLayerView(); + return getOverview(); case 1: - return getExecutionLayerView(); - - case 2: - return desktopView ? getWithdrawlsViewDesktop() : getWithdrawlsViewMobile(); + return desktopView ? getWithdrawalsDesktop() : getWithdrawalsMobile(); } }; - //Tab sections information + //TABS - Overview & withdrawals const getInformationView = () => { return (
- setTabPageIndex(0)} - /> + setTabPageIndex(0)} /> {existsBlock && ( <> setTabPageIndex(1)} /> - setTabPageIndex(2)} - /> )}
- {getSelectedTab()}
); }; - //Overview tab view - const getConsensusLayerView = () => { + //Overview tab - table + const getOverview = () => { return (
{ color: themeMode?.darkMode ? 'var(--white)' : 'var(--black)', }} > + {/* Table */}
} /> + } /> {existsBlock && ( @@ -362,7 +355,7 @@ const Slot = () => { {existsBlock && ( - } /> + } /> )} {existsBlock && } @@ -410,48 +403,8 @@ const Slot = () => { ); }; - const getExecutionLayerView = () => { - return ( -
- - - - - - - - - -
- ); - }; - - //View withdrawals table desktop - const getWithdrawlsViewDesktop = () => { + //Withdrawals tab - table desktop + const getWithdrawalsDesktop = () => { return (
{ ); }; - //View withdrawals table mobile - const getWithdrawlsViewMobile = () => { + //Withdrawals tab - table mobile + const getWithdrawalsMobile = () => { return (
{
{withdrawals.map(element => (
{
))} - {withdrawals.length == 0 && ( -
-

No withdrawals

+
+

No withdrawals

)}
@@ -590,7 +553,7 @@ const Slot = () => { ); }; - //Overview slot page + //OVERVIEW SLOT PAGE return ( diff --git a/packages/client/pages/[network]/slot/graffiti/[graffiti].tsx b/packages/client/pages/slot/graffiti/[graffiti].tsx similarity index 72% rename from packages/client/pages/[network]/slot/graffiti/[graffiti].tsx rename to packages/client/pages/slot/graffiti/[graffiti].tsx index 25d91e45..2d829c5c 100644 --- a/packages/client/pages/[network]/slot/graffiti/[graffiti].tsx +++ b/packages/client/pages/slot/graffiti/[graffiti].tsx @@ -2,8 +2,8 @@ import React from 'react'; import Head from 'next/head'; // Components -import Layout from '../../../../components/layouts/Layout'; -import Graffitis from '../../../../components/layouts/Graffitis'; +import Layout from '../../../components/layouts/Layout'; +import Graffitis from '../../../components/layouts/Graffitis'; const SlotGraffitiSearch = () => { return ( diff --git a/packages/client/pages/[network]/slots.tsx b/packages/client/pages/slots.tsx similarity index 89% rename from packages/client/pages/[network]/slots.tsx rename to packages/client/pages/slots.tsx index 5b9c0546..d3dd2785 100644 --- a/packages/client/pages/[network]/slots.tsx +++ b/packages/client/pages/slots.tsx @@ -2,20 +2,20 @@ import React, { useState, useEffect, useContext } from 'react'; import Head from 'next/head'; import { useRouter } from 'next/router'; -// Contexts -import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; - // Axios -import axiosClient from '../../config/axios'; +import axiosClient from '../config/axios'; + +// Contexts +import ThemeModeContext from '../contexts/theme-mode/ThemeModeContext'; // Components -import Layout from '../../components/layouts/Layout'; -import SlotsList from '../../components/layouts/Slots'; -import Loader from '../../components/ui/Loader'; -import ViewMoreButton from '../../components/ui/ViewMoreButton'; +import Layout from '../components/layouts/Layout'; +import SlotsList from '../components/layouts/Slots'; +import Loader from '../components/ui/Loader'; +import ViewMoreButton from '../components/ui/ViewMoreButton'; // Types -import { Slot } from '../../types'; +import { Slot } from '../types'; const Slots = () => { // Theme Mode Context @@ -92,7 +92,7 @@ const Slots = () => { style={{ background: themeMode?.darkMode ? 'var(--bgDarkMode)' : 'var(--bgMainLightMode)' }} >

{ + // Theme Mode Context + const { themeMode } = React.useContext(ThemeModeContext) ?? {}; + return ( +
+

+ {title}: +

+
+ {text && ( +

+ {text} +

+ )} + + {content && <>{content}} +
+
+ ); +}; + +const TransactionPage = () => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + // Next router + const router = useRouter(); + const { network, hash } = router.query; + + // States + const [transaction, setTransaction] = useState(null); + const [tabPageIndex, setTabPageIndex] = useState(0); + const [loading, setLoading] = useState(true); + + // UseEffect + useEffect(() => { + if (network && hash && !transaction) { + getTransaction(); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [network, hash]); + + // Get Transaction + const getTransaction = async () => { + try { + setLoading(true); + + const response = await axiosClient.get(`/api/transactions/${hash}`, { + params: { + network, + }, + }); + + setTransaction(response.data.transaction); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }; + + // Tabs + const getSelectedTab = () => { + switch (tabPageIndex) { + case 0: + return getOverview(); + + case 1: + return getMoreDetails(); + } + }; + + const getOverview = () => { + return ( +
+
+ + } /> + + + + + + +
+
+ ); + }; + + const getMoreDetails = () => { + return ( +
+
+ + + + +
+
+ ); + }; + + return ( + + + + + +

+ Transaction +

+ + {loading && ( +
+ +
+ )} + + {transaction && ( +
+
+ setTabPageIndex(0)} + /> + setTabPageIndex(1)} + /> +
+ + {getSelectedTab()} +
+ )} +
+ ); +}; + +export default TransactionPage; diff --git a/packages/client/pages/transactions.tsx b/packages/client/pages/transactions.tsx new file mode 100644 index 00000000..bea0424d --- /dev/null +++ b/packages/client/pages/transactions.tsx @@ -0,0 +1,117 @@ +import React, { useState, useEffect, useContext } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; + +// Axios +import axiosClient from '../config/axios'; + +// Contexts +import ThemeModeContext from '../contexts/theme-mode/ThemeModeContext'; + +// Components +import Layout from '../components/layouts/Layout'; +import ViewMoreButton from '../components/ui/ViewMoreButton'; +import TransactionsComponent from '../components/layouts/Transactions'; + +// Types +import { Transaction } from '../types'; + +const Transactions = () => { + // Theme Mode Context + const { themeMode } = useContext(ThemeModeContext) ?? {}; + + // Router + const router = useRouter(); + const { network } = router.query; + + // States + const [transactions, setTransactions] = useState([]); + const [currentPage, setCurrentPage] = useState(0); + const [loading, setLoading] = useState(true); + + // UseEffect + useEffect(() => { + if (network && transactions.length === 0) { + getTransactions(0); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [network]); + + //TRANSACTIONS TABLE + const getTransactions = async (page: number) => { + try { + setLoading(true); + setCurrentPage(page); + + const response = await axiosClient.get('/api/transactions', { + params: { + network, + page, + limit: 20, + threshold: transactions.length > 0 ? transactions[transactions.length - 1].f_tx_idx : null, + }, + }); + + setTransactions(prevState => [ + ...prevState, + ...response.data.transactions.filter( + (transaction: Transaction) => + !prevState.find( + (prevTransaction: Transaction) => prevTransaction.f_tx_idx === transaction.f_tx_idx + ) + ), + ]); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }; + + //Overview Transaction page + return ( + + + Transactions of the Ethereum Chain - EthSeer.io + + + + + +

+ Ethereum Transactions +

+ +
+

+ Transactions are the atomic components that create the state of the Ethereum Virtual Machine. +

+
+ +
+ +
+ + getTransactions(currentPage + 1)} /> +
+ ); +}; + +export default Transactions; diff --git a/packages/client/pages/[network]/validator/[id].tsx b/packages/client/pages/validator/[id].tsx similarity index 97% rename from packages/client/pages/[network]/validator/[id].tsx rename to packages/client/pages/validator/[id].tsx index d5ddea65..c2517770 100644 --- a/packages/client/pages/[network]/validator/[id].tsx +++ b/packages/client/pages/validator/[id].tsx @@ -3,27 +3,27 @@ import { useRouter } from 'next/router'; import Head from 'next/head'; // Axios -import axiosClient from '../../../config/axios'; +import axiosClient from '../../config/axios'; // Contexts -import ThemeModeContext from '../../../contexts/theme-mode/ThemeModeContext'; +import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; // Components -import Layout from '../../../components/layouts/Layout'; -import BlockImage from '../../../components/ui/BlockImage'; -import TabHeader from '../../../components/ui/TabHeader'; -import Animation from '../../../components/layouts/Animation'; -import ProgressSmoothBar from '../../../components/ui/ProgressSmoothBar'; -import Loader from '../../../components/ui/Loader'; -import ValidatorStatus from '../../../components/ui/ValidatorStatus'; -import LinkEpoch from '../../../components/ui/LinkEpoch'; -import LinkSlot from '../../../components/ui/LinkSlot'; -import LinkEntity from '../../../components/ui/LinkEntity'; -import LinkValidator from '../../../components/ui/LinkValidator'; -import Arrow from '../../../components/ui/Arrow'; +import Layout from '../../components/layouts/Layout'; +import BlockImage from '../../components/ui/BlockImage'; +import TabHeader from '../../components/ui/TabHeader'; +import Animation from '../../components/layouts/Animation'; +import ProgressSmoothBar from '../../components/ui/ProgressSmoothBar'; +import Loader from '../../components/ui/Loader'; +import ValidatorStatus from '../../components/ui/ValidatorStatus'; +import LinkEpoch from '../../components/ui/LinkEpoch'; +import LinkSlot from '../../components/ui/LinkSlot'; +import LinkEntity from '../../components/ui/LinkEntity'; +import LinkValidator from '../../components/ui/LinkValidator'; +import Arrow from '../../components/ui/Arrow'; // Types -import { Validator, Slot, Withdrawal } from '../../../types'; +import { Validator, Slot, Withdrawal } from '../../types'; type Props = { content: string; diff --git a/packages/client/pages/[network]/validators.tsx b/packages/client/pages/validators.tsx similarity index 95% rename from packages/client/pages/[network]/validators.tsx rename to packages/client/pages/validators.tsx index 060c344b..3fbe66b3 100644 --- a/packages/client/pages/[network]/validators.tsx +++ b/packages/client/pages/validators.tsx @@ -3,21 +3,21 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; // Axios -import axiosClient from '../../config/axios'; +import axiosClient from '../config/axios'; // Contexts -import ThemeModeContext from '../../contexts/theme-mode/ThemeModeContext'; +import ThemeModeContext from '../contexts/theme-mode/ThemeModeContext'; // Components -import Layout from '../../components/layouts/Layout'; -import ValidatorStatus from '../../components/ui/ValidatorStatus'; -import Loader from '../../components/ui/Loader'; -import ViewMoreButton from '../../components/ui/ViewMoreButton'; -import LinkValidator from '../../components/ui/LinkValidator'; -import LinkEntity from '../../components/ui/LinkEntity'; +import Layout from '../components/layouts/Layout'; +import ValidatorStatus from '../components/ui/ValidatorStatus'; +import Loader from '../components/ui/Loader'; +import ViewMoreButton from '../components/ui/ViewMoreButton'; +import LinkValidator from '../components/ui/LinkValidator'; +import LinkEntity from '../components/ui/LinkEntity'; // Types -import { Validator } from '../../types'; +import { Validator } from '../types'; const Validators = () => { // Theme Mode Context @@ -246,7 +246,7 @@ const Validators = () => { style={{ background: themeMode?.darkMode ? 'var(--bgDarkMode)' : 'var(--bgMainLightMode)' }} >

{ + + try { + + const { network, page = 0, limit = 32 } = req.query; + + const pgPool = pgPools[network as string]; + + const skip = Number(page) * Number(limit); + + const [ blocks ] = + await Promise.all([ + pgPool.query(` + SELECT f_timestamp, f_slot, f_epoch + f_el_fee_recp, f_el_gas_limit, f_el_gas_used, + f_el_transactions, f_el_block_hash, f_payload_size_bytes, + f_el_block_number + FROM t_block_metrics + ORDER BY f_el_block_number DESC + OFFSET ${skip} + LIMIT ${Number(limit)} + `) + ]); + + res.json({ + blocks: blocks.rows + }); + + } catch (error) { + console.log(error); + return res.status(500).json({ + msg: 'An error occurred on the server' + }); + } +}; + +export const getBlockById = async (req: Request, res: Response) => { + + try { + + const { id } = req.params; + const { network } = req.query; + + const pgPool = pgPools[network as string]; + + const [ block ] = + await Promise.all([ + pgPool.query(` + SELECT f_timestamp, f_slot, f_epoch, + f_el_fee_recp, f_el_gas_limit, f_el_gas_used, + f_el_transactions, f_el_block_hash, f_payload_size_bytes + FROM t_block_metrics + WHERE f_el_block_number = '${id}' + `) + ]); + // 18022299 + + + res.json({ + block: { + ...block.rows[0], + }, + }); + + } catch (error) { + console.log(error); + return res.status(500).json({ + msg: 'An error occurred on the server' + }); + } +}; + +export const getTransactionsByBlock = async (req: Request, res: Response) => { + + try { + + const { id } = req.params; + const { network } = req.query; + + const pgPool = pgPools[network as string]; + + const transactions = + await pgPool.query(` + SELECT f_tx_type, + f_value, + f_gas_fee_cap, + f_to, + f_hash, + f_timestamp, + f_from + FROM t_transactions + WHERE f_el_block_number = '${id}' + `); + + res.json({ + transactions: transactions.rows, + }); + + } catch (error) { + console.log(error); + return res.status(500).json({ + msg: 'An error occurred on the server' + }); + } +}; \ No newline at end of file diff --git a/packages/server/controllers/epochs.ts b/packages/server/controllers/epochs.ts index 712b66f8..754c01cd 100644 --- a/packages/server/controllers/epochs.ts +++ b/packages/server/controllers/epochs.ts @@ -14,7 +14,7 @@ export const getEpochsStatistics = async (req: Request, res: Response) => { const [ epochsStats, blocksStats ] = await Promise.all([ pgPool.query(` - SELECT f_epoch, f_slot, f_num_att_vals, f_num_vals, + SELECT f_epoch, f_slot, f_num_att_vals, f_num_active_vals, f_att_effective_balance_eth, f_total_effective_balance_eth, f_missing_source, f_missing_target, f_missing_head FROM t_epoch_metrics_summary @@ -67,7 +67,7 @@ export const getEpochById = async (req: Request, res: Response) => { const [ epochStats, blocksProposed, withdrawals ] = await Promise.all([ pgPool.query(` - SELECT f_epoch, f_slot, f_num_att_vals, f_num_vals, + SELECT f_epoch, f_slot, f_num_att_vals, f_num_active_vals, f_att_effective_balance_eth, f_total_effective_balance_eth, f_missing_source, f_missing_target, f_missing_head FROM t_epoch_metrics_summary diff --git a/packages/server/controllers/slots.ts b/packages/server/controllers/slots.ts index ac4ef5d5..a5ce88e6 100644 --- a/packages/server/controllers/slots.ts +++ b/packages/server/controllers/slots.ts @@ -135,7 +135,8 @@ export const getSlotById = async (req: Request, res: Response) => { t_block_metrics.f_attestations, t_block_metrics.f_deposits, t_block_metrics.f_proposer_slashings, t_block_metrics.f_att_slashings, t_block_metrics.f_voluntary_exits, t_block_metrics.f_sync_bits, t_block_metrics.f_el_fee_recp, t_block_metrics.f_el_gas_limit, t_block_metrics.f_el_gas_used, - t_block_metrics.f_el_transactions, t_block_metrics.f_el_block_hash, t_eth2_pubkeys.f_pool_name + t_block_metrics.f_el_transactions, t_block_metrics.f_el_block_hash, t_eth2_pubkeys.f_pool_name, + t_block_metrics.f_el_block_number FROM t_block_metrics LEFT OUTER JOIN t_eth2_pubkeys ON t_block_metrics.f_proposer_index = t_eth2_pubkeys.f_val_idx WHERE f_slot = '${id}' diff --git a/packages/server/controllers/transactions.ts b/packages/server/controllers/transactions.ts new file mode 100644 index 00000000..70fb5e06 --- /dev/null +++ b/packages/server/controllers/transactions.ts @@ -0,0 +1,65 @@ +import { Request, Response } from 'express'; +import { pgPools } from '../config/db'; + +export const getTransactions = async (req: Request, res: Response) => { + + try { + + const { network, page = 0, limit = 10, threshold } = req.query; + + const pgPool = pgPools[network as string]; + + const skip = Number(page) * Number(limit); + + const transactions = + await pgPool.query(` + SELECT f_tx_idx, f_gas_fee_cap, f_value, f_to, f_hash, f_timestamp, f_from, f_el_block_number, + f_gas_price, f_gas, f_tx_type, f_data + FROM t_transactions + ${threshold ? `WHERE f_tx_idx <= ${Number(threshold)}` : ''} + ORDER by f_el_block_number DESC, f_tx_idx DESC, f_timestamp DESC + OFFSET ${skip} + LIMIT ${Number(limit)} + `); + + res.json({ + transactions: transactions.rows + }); + + } catch (error) { + console.log(error); + return res.status(500).json({ + msg: 'An error occurred on the server' + }); + } +}; + +export const getTransactionByHash = async (req: Request, res: Response) => { + + try { + + const { hash } = req.params; + + const { network } = req.query; + + const pgPool = pgPools[network as string]; + + const transaction = + await pgPool.query(` + SELECT f_tx_idx, f_gas_fee_cap, f_value, f_to, f_hash, f_timestamp, f_from, f_el_block_number, + f_gas_price, f_gas, f_tx_type, f_data, f_nonce + FROM t_transactions + WHERE f_hash = '${hash}' + `); + + res.json({ + transaction: transaction.rows[0] + }); + + } catch (error) { + console.log(error); + return res.status(500).json({ + msg: 'An error occurred on the server' + }); + } +}; \ No newline at end of file diff --git a/packages/server/models/server.ts b/packages/server/models/server.ts index 8f10936f..609214fa 100644 --- a/packages/server/models/server.ts +++ b/packages/server/models/server.ts @@ -5,8 +5,10 @@ import { dbConnection } from '../config/db'; import entitiesRoutes from '../routes/entities'; import epochsRoutes from '../routes/epochs'; import slotsRoutes from '../routes/slots'; +import blocksRoutes from '../routes/blocks'; import validatorsRoutes from '../routes/validators'; import networksRoutes from '../routes/networks'; +import transactionsRoutes from '../routes/transactions'; class Server { @@ -17,8 +19,10 @@ class Server { entities: '/api/entities', epochs: '/api/epochs', slots: '/api/slots', + blocks: '/api/blocks', validators: '/api/validators', - networks: '/api/networks' + networks: '/api/networks', + transactions: '/api/transactions', }; private callsVerbose: boolean; @@ -63,8 +67,10 @@ class Server { this.app.use(this.paths.entities, entitiesRoutes); this.app.use(this.paths.epochs, epochsRoutes); this.app.use(this.paths.slots, slotsRoutes); + this.app.use(this.paths.blocks, blocksRoutes); this.app.use(this.paths.validators, validatorsRoutes); this.app.use(this.paths.networks, networksRoutes); + this.app.use(this.paths.transactions, transactionsRoutes); } listen() { diff --git a/packages/server/routes/blocks.ts b/packages/server/routes/blocks.ts new file mode 100644 index 00000000..e6c5c3e0 --- /dev/null +++ b/packages/server/routes/blocks.ts @@ -0,0 +1,34 @@ +import { Router } from 'express'; +import { check, query } from 'express-validator'; + +import { + getBlockById, getTransactionsByBlock, getBlocks +} from '../controllers/blocks'; + +import { checkFields } from '../middlewares/check-fields'; +import { existsNetwork } from '../helpers/network-validator'; + + +const router = Router(); + + +router.get('/:id', [ + check('id').isInt({ min: 0, max: 2147483647 }), + query('network').not().isEmpty(), + query('network').custom(existsNetwork), + checkFields, +], getBlockById); +router.get('/:id/transactions', [ + check('id').isInt({ min: 0, max: 2147483647 }), + query('network').not().isEmpty(), + query('network').custom(existsNetwork), + checkFields, +], getTransactionsByBlock); +router.get('/', [ + query('network').not().isEmpty(), + query('network').custom(existsNetwork), + checkFields, +], getBlocks); + + +export default router; \ No newline at end of file diff --git a/packages/server/routes/transactions.ts b/packages/server/routes/transactions.ts new file mode 100644 index 00000000..fa9b1433 --- /dev/null +++ b/packages/server/routes/transactions.ts @@ -0,0 +1,26 @@ +import { Router } from 'express'; +import { query } from 'express-validator'; + +import { + getTransactions, + getTransactionByHash, +} from '../controllers/transactions'; + +import { checkFields } from '../middlewares/check-fields'; +import { existsNetwork } from '../helpers/network-validator'; + +const router = Router(); + +router.get('/', [ + query('network').not().isEmpty(), + query('network').custom(existsNetwork), + checkFields, +], getTransactions); + +router.get('/:hash', [ + query('network').not().isEmpty(), + query('network').custom(existsNetwork), + checkFields, +], getTransactionByHash); + +export default router;