From e044643344b4f011ebb70810e8cc7f70a8505243 Mon Sep 17 00:00:00 2001 From: Mauro Eijsenring Date: Mon, 7 Oct 2024 09:53:56 +0200 Subject: [PATCH] feat: landing page (#96) Co-authored-by: filip --- .../http-server/routes/controllers/stats.ts | 6 +- services/explorer-ui/src/api/stats.ts | 26 ++++ .../components/blocks/block-table-columns.tsx | 97 +++++++++++---- .../src/components/blocks/blocks-schema.ts | 6 +- .../tx-effects/tx-effects-columns.tsx | 28 +++-- .../tx-effects/tx-effects-schema.ts | 3 +- services/explorer-ui/src/hooks/stats.ts | 37 ++++++ services/explorer-ui/src/lib/utils.ts | 18 ++- .../explorer-ui/src/pages/landing/index.tsx | 117 ++++++++++++++++++ .../explorer-ui/src/pages/landing/util.ts | 33 +++++ .../explorer-ui/src/routes/index.lazy.tsx | 54 +------- .../src/routes/tx-effects/index.lazy.tsx | 25 ++-- services/explorer-ui/src/service/constants.ts | 6 + 13 files changed, 339 insertions(+), 117 deletions(-) create mode 100644 services/explorer-ui/src/api/stats.ts create mode 100644 services/explorer-ui/src/hooks/stats.ts create mode 100644 services/explorer-ui/src/pages/landing/index.tsx create mode 100644 services/explorer-ui/src/pages/landing/util.ts diff --git a/services/explorer-api/src/http-server/routes/controllers/stats.ts b/services/explorer-api/src/http-server/routes/controllers/stats.ts index ffd1d198..702a0e7b 100644 --- a/services/explorer-api/src/http-server/routes/controllers/stats.ts +++ b/services/explorer-api/src/http-server/routes/controllers/stats.ts @@ -12,13 +12,13 @@ export const GET_STATS_TOTAL_TX_EFFECTS = asyncHandler(async (_req, res) => { res.status(200).send(JSON.stringify(total)); }); -export const GET_STATS_TOTAL_TX_EFFECTS_LAST_24H = asyncHandler((_req, res) => { - const txEffects = dbWrapper.get( +export const GET_STATS_TOTAL_TX_EFFECTS_LAST_24H = asyncHandler(async (_req, res) => { + const nbrOfTxEffects = await dbWrapper.get( ["stats", "totalTxEffectsLast24h"], () => db.l2TxEffect.getTotalTxEffectsLast24h(), CACHE_LATEST_TTL_SECONDS ); - res.status(200).send(JSON.stringify(txEffects)); + res.status(200).send(JSON.stringify(nbrOfTxEffects)); }); export const GET_STATS_TOTAL_CONTRACTS = asyncHandler(async (_req, res) => { diff --git a/services/explorer-ui/src/api/stats.ts b/services/explorer-ui/src/api/stats.ts new file mode 100644 index 00000000..80008da9 --- /dev/null +++ b/services/explorer-ui/src/api/stats.ts @@ -0,0 +1,26 @@ +import { aztecExplorer } from "~/service/constants"; +import client, { validateResponse } from "./client"; +import { z } from "zod"; + +export const statsL2Api = { + getL2TotalTxEffects: async (): Promise => { + const response = await client.get(aztecExplorer.getL2TotalTxEffects); + return validateResponse(z.string(), response.data); + }, + getL2TotalTxEffectsLast24h: async (): Promise => { + const response = await client.get(aztecExplorer.getL2TotalTxEffectsLast24h); + return validateResponse(z.string(), response.data); + }, + getL2TotalContracts: async (): Promise => { + const response = await client.get(aztecExplorer.getL2TotalContracts); + return validateResponse(z.string(), response.data); + }, + getL2AverageFees: async (): Promise => { + const response = await client.get(aztecExplorer.getL2AverageFees); + return validateResponse(z.string(), response.data); + }, + getL2AverageBlockTime: async (): Promise => { + const response = await client.get(aztecExplorer.getL2AverageBlockTime); + return validateResponse(z.string(), response.data); + }, +}; diff --git a/services/explorer-ui/src/components/blocks/block-table-columns.tsx b/services/explorer-ui/src/components/blocks/block-table-columns.tsx index ca261f13..2c6f3350 100644 --- a/services/explorer-ui/src/components/blocks/block-table-columns.tsx +++ b/services/explorer-ui/src/components/blocks/block-table-columns.tsx @@ -1,9 +1,9 @@ -import { type ColumnDef } from "@tanstack/react-table"; import { Link } from "@tanstack/react-router"; -import {routes} from "~/routes/__root"; +import { type ColumnDef } from "@tanstack/react-table"; import { DataTableColumnHeader } from "~/components/data-table"; +import { formatTimeSince } from "~/lib/utils"; +import { routes } from "~/routes/__root"; import type { BlockTableSchema } from "./blocks-schema"; -import {formatTimeSince} from "~/lib/utils"; const text = { height: "BLOCK HEIGHT", @@ -17,60 +17,113 @@ const text = { export const BlockTableColumns: ColumnDef[] = [ { accessorKey: "height", - header: ({ column }) => , + header: ({ column }) => ( + + ), cell: ({ row }) => { const height = row.getValue("height"); // eslint-disable-next-line @typescript-eslint/restrict-plus-operands const r = routes.blocks.route + "/" + height; - return (
- {row.getValue("height")} -
); + return ( +
+ {row.getValue("height")} +
+ ); }, enableSorting: true, enableHiding: false, }, { accessorKey: "blockHash", - header: ({ column }) => , + header: ({ column }) => ( + + ), cell: ({ row }) => { const blockHash = row.getValue("blockHash"); - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - const r = routes.blocks.route + "/" + blockHash; - return (
- {row.getValue("blockHash")} -
); + if (typeof blockHash !== "string") return null; + const r = `${routes.blocks.route}/${blockHash}`; + const truncatedBlockHash = `${blockHash.slice(0, 6)}...${blockHash.slice( + -4 + )}`; + return ( +
+ {truncatedBlockHash} +
+ ); }, enableSorting: false, enableHiding: false, }, { accessorKey: "numberOfTransactions", - header: ({ column }) => , - cell: ({ row }) =>
{row.getValue("numberOfTransactions")}
, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue("numberOfTransactions")} +
+ ), enableSorting: true, enableHiding: false, }, { accessorKey: "txEffectsLength", - header: ({ column }) => , - cell: ({ row }) =>
{row.getValue("txEffectsLength")}
, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.getValue("txEffectsLength")}
+ ), enableSorting: true, enableHiding: false, }, { accessorKey: "totalFees", - header: ({ column }) => , - cell: ({ row }) =>
{row.getValue("totalFees")}
, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.getValue("totalFees")}
+ ), enableSorting: true, enableHiding: false, }, { accessorKey: "timestamp", - header: ({ column }) => , + header: ({ column }) => ( + + ), cell: ({ row }) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - const formattedTime = formatTimeSince(row.getValue("timestamp") as unknown as number * 1000); - return (
{formattedTime}
); + const formattedTime = formatTimeSince( + (row.getValue("timestamp") as unknown as number) * 1000 + ); + return
{formattedTime}
; }, enableSorting: true, enableHiding: false, diff --git a/services/explorer-ui/src/components/blocks/blocks-schema.ts b/services/explorer-ui/src/components/blocks/blocks-schema.ts index 17f208f2..39de1ad1 100644 --- a/services/explorer-ui/src/components/blocks/blocks-schema.ts +++ b/services/explorer-ui/src/components/blocks/blocks-schema.ts @@ -1,12 +1,8 @@ import { z } from "zod"; +import { frNumberSchema } from "~/lib/utils"; export type BlockTableSchema = z.infer; -const frNumberSchema = z.preprocess((val) => { - if (typeof val === "string") return parseInt(val, 16); - return val; -}, z.coerce.number()); - export const blockSchema = z.object({ height: z.number(), blockHash: z.string(), diff --git a/services/explorer-ui/src/components/tx-effects/tx-effects-columns.tsx b/services/explorer-ui/src/components/tx-effects/tx-effects-columns.tsx index c2b274c3..869cfad8 100644 --- a/services/explorer-ui/src/components/tx-effects/tx-effects-columns.tsx +++ b/services/explorer-ui/src/components/tx-effects/tx-effects-columns.tsx @@ -3,13 +3,14 @@ import { Link } from "@tanstack/react-router"; import { routes } from "~/routes/__root"; import { DataTableColumnHeader } from "~/components/data-table"; import { type TxEffectTableSchema } from "./tx-effects-schema"; +import { formatTimeSince } from "~/lib/utils"; const text = { txHash: "TX EFFECT HASH", transactionFee: "TRANSACTION FEE", logCount: "LOGS COUNT", blockHeight: "BLOCK HEIGHT", - timestamp: "TIMESTAMP", + timeSince: "TIME SINCE", }; export const TxEffectsTableColumns: ColumnDef[] = [ @@ -24,16 +25,15 @@ export const TxEffectsTableColumns: ColumnDef[] = [ ), cell: ({ row }) => { const txHash = row.getValue("txHash"); - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - const r = routes.txEffects.route + "/" + txHash; + if (typeof txHash !== "string") return null; + const r = `${routes.txEffects.route}/${txHash}`; + const truncatedTxHash = `${txHash.slice(0, 6)}...${txHash.slice(-4)}`; return ( -
- {row.getValue("txHash")} +
+ {truncatedTxHash}
); }, - enableSorting: false, - enableHiding: false, }, { accessorKey: "transactionFee", @@ -84,14 +84,18 @@ export const TxEffectsTableColumns: ColumnDef[] = [ accessorKey: "timestamp", header: ({ column }) => ( ), - cell: ({ row }) => ( -
{new Date(row.getValue("timestamp")).toLocaleString()}
- ), + cell: ({ row }) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const formattedTime = formatTimeSince( + (row.getValue("timestamp") as unknown as number) * 1000 + ); + return
{formattedTime}
; + }, enableSorting: true, enableHiding: false, }, diff --git a/services/explorer-ui/src/components/tx-effects/tx-effects-schema.ts b/services/explorer-ui/src/components/tx-effects/tx-effects-schema.ts index e793a447..f5a744ec 100644 --- a/services/explorer-ui/src/components/tx-effects/tx-effects-schema.ts +++ b/services/explorer-ui/src/components/tx-effects/tx-effects-schema.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import {frNumberSchema} from "~/lib/utils"; export type TxEffectTableSchema = z.infer; @@ -7,5 +8,5 @@ export const txEffectSchema = z.object({ transactionFee: z.number(), logCount: z.number(), blockNumber: z.number(), - timestamp: z.number(), + timestamp: frNumberSchema, }); diff --git a/services/explorer-ui/src/hooks/stats.ts b/services/explorer-ui/src/hooks/stats.ts new file mode 100644 index 00000000..94d56e98 --- /dev/null +++ b/services/explorer-ui/src/hooks/stats.ts @@ -0,0 +1,37 @@ +import { type UseQueryResult, useQuery } from "@tanstack/react-query"; +import { statsL2Api } from "~/api/stats"; + +export const useTotalTxEffects = (): UseQueryResult => { + return useQuery({ + queryKey: ["useTotalTxEffects"], + queryFn: statsL2Api.getL2TotalTxEffects, + }); +}; + +export const useTotalTxEffectsLast24h = (): UseQueryResult => { + return useQuery({ + queryKey: ["useTotalTxEffectsLast24h"], + queryFn: statsL2Api.getL2TotalTxEffectsLast24h, + }); +}; + +export const useTotalContracts = (): UseQueryResult => { + return useQuery({ + queryKey: ["useTotalContracts"], + queryFn: statsL2Api.getL2TotalContracts, + }); +}; + +export const useAvarageFees = (): UseQueryResult => { + return useQuery({ + queryKey: ["useAvarageFees"], + queryFn: statsL2Api.getL2AverageFees, + }); +}; + +export const useAvarageBlockTime = (): UseQueryResult => { + return useQuery({ + queryKey: ["useAvarageBlockTime"], + queryFn: statsL2Api.getL2AverageBlockTime, + }); +}; diff --git a/services/explorer-ui/src/lib/utils.ts b/services/explorer-ui/src/lib/utils.ts index 9d05db5c..b252a919 100644 --- a/services/explorer-ui/src/lib/utils.ts +++ b/services/explorer-ui/src/lib/utils.ts @@ -1,5 +1,6 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; +import { z } from "zod"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -62,12 +63,12 @@ export const hexToHSL = (hex: string): string => { return `hsl(${h}, ${s}%, ${l}%)`; }; - const intervals = [ - { label: "day", seconds: 86400 }, - { label: "hour", seconds: 3600 }, - { label: "minute", seconds: 60 }, - { label: "second", seconds: 1 }, - ]; +const intervals = [ + { label: "day", seconds: 86400 }, + { label: "hour", seconds: 3600 }, + { label: "minute", seconds: 60 }, + { label: "second", seconds: 1 }, +]; export const formatTimeSince = (unixTimestamp: number | null) => { if (unixTimestamp === null) return "no timestamp"; @@ -82,3 +83,8 @@ export const formatTimeSince = (unixTimestamp: number | null) => { } return "just now"; }; + +export const frNumberSchema = z.preprocess((val) => { + if (typeof val === "string") return parseInt(val, 16); + return val; +}, z.coerce.number()); diff --git a/services/explorer-ui/src/pages/landing/index.tsx b/services/explorer-ui/src/pages/landing/index.tsx new file mode 100644 index 00000000..bedf2a1f --- /dev/null +++ b/services/explorer-ui/src/pages/landing/index.tsx @@ -0,0 +1,117 @@ +import { FC } from "react"; +import { BlocksTable } from "~/components/blocks/blocks-table"; +import { TxEffectsTable } from "~/components/tx-effects/tx-effects-table"; +import { useLatestBlocks } from "~/hooks"; +import { + useAvarageBlockTime, + useAvarageFees, + useTotalContracts, + useTotalTxEffects, + useTotalTxEffectsLast24h, +} from "~/hooks/stats"; +import { mapLatestBlocks, mapLatestTxEffects } from "./util"; + +export const Landing: FC = () => { + const { data: latestBlocks, isLoading, error } = useLatestBlocks(); + const { + data: totalTxEffects, + isLoading: loadingTotalEffects, + error: errorTotalEffects, + } = useTotalTxEffects(); + const { + data: totalTxEffects24h, + isLoading: loadingTotalEffects24h, + error: errorTotalEffects24h, + } = useTotalTxEffectsLast24h(); + const { + data: avarageFees, + isLoading: loadingAvarageFees, + error: errorAvarageFees, + } = useAvarageFees(); + const { + data: totalAmountOfContracts, + isLoading: loadingAmountContracts, + error: errorAmountContracts, + } = useTotalContracts(); + const { + data: avarageBlockTime, + isLoading: loadingAvarageBlockTime, + error: errorAvarageBlockTime, + } = useAvarageBlockTime(); + + if (isLoading) return

Loading...

; + if (error) return

{error.message}

; + if (!latestBlocks) return

No data

; + + const getStatsData = ( + isLoading: boolean, + error: Error | null, + data?: string, + ) => { + let text; + if (!data) text = "No Data"; + if (isLoading) text = "Loading"; + if (error) text = error.message; + if (data) text = data; + + return

{text}

; + }; + + return ( +
+
+
+

Total Blocks

+ {getStatsData( + loadingTotalEffects, + errorTotalEffects, + totalTxEffects, + )} +
+
+

TX-Effects last 24 hours

+ {getStatsData( + loadingTotalEffects24h, + errorTotalEffects24h, + totalTxEffects24h, + )} +
+
+

Total amount of contracts

+ {getStatsData( + loadingAmountContracts, + errorAmountContracts, + totalAmountOfContracts, + )} +
+
+

Average fee's

+ {getStatsData(loadingAvarageFees, errorAvarageFees, avarageFees)} +
+
+

Average block time

+ {getStatsData( + loadingAvarageBlockTime, + errorAvarageBlockTime, + avarageBlockTime, + )} +
+
+

Todo

+

TODO

+
+
+
+
+

Latest Blocks

+ +
+ +
+

Latest TX-Effects

+ +
+
+
+ ); +}; diff --git a/services/explorer-ui/src/pages/landing/util.ts b/services/explorer-ui/src/pages/landing/util.ts new file mode 100644 index 00000000..e24d013f --- /dev/null +++ b/services/explorer-ui/src/pages/landing/util.ts @@ -0,0 +1,33 @@ +import { ChicmozL2Block } from "@chicmoz-pkg/types"; +import { blockSchema } from "~/components/blocks/blocks-schema"; +import { txEffectSchema } from "~/components/tx-effects/tx-effects-schema"; + +export const mapLatestBlocks = (latestBlocks: ChicmozL2Block[]) => { + return latestBlocks.map((block) => { + return blockSchema.parse({ + height: block.height, + blockHash: block.hash, + numberOfTransactions: block.header.contentCommitment.numTxs, + txEffectsLength: block.body.txEffects.length, + totalFees: block.header.totalFees, + timestamp: block.header.globalVariables.timestamp, + }); + }); +}; + +export const mapLatestTxEffects = (latestBlocks: ChicmozL2Block[]) => { + return latestBlocks.flatMap((block) => { + return block.body.txEffects.map((txEffect) => + txEffectSchema.parse({ + txHash: txEffect.txHash, + transactionFee: parseInt(txEffect.transactionFee, 16), + logCount: + parseInt(txEffect.encryptedLogsLength, 16) + + parseInt(txEffect.unencryptedLogsLength, 16) + + parseInt(txEffect.noteEncryptedLogsLength, 16), + blockNumber: block.height, + timestamp: block.header.globalVariables.timestamp, + }), + ); + }); +}; diff --git a/services/explorer-ui/src/routes/index.lazy.tsx b/services/explorer-ui/src/routes/index.lazy.tsx index aad86b35..6ac08211 100644 --- a/services/explorer-ui/src/routes/index.lazy.tsx +++ b/services/explorer-ui/src/routes/index.lazy.tsx @@ -1,54 +1,6 @@ -import { Link, createLazyFileRoute } from "@tanstack/react-router"; -import { useLatestBlock } from "~/hooks/"; -import { routes } from "./__root"; - -const LatestBlockData = () => { - const { data: latestBlock, isLoading, error } = useLatestBlock(); - - if (isLoading) return

Loading...

; - if (error) return

{error.message}

; - if (!latestBlock) return

No data

; - - return ( -
    -
  • - - Block Number:{" "} - {parseInt(latestBlock.header.globalVariables.blockNumber, 16)} - -
  • -
  • - Number of Transactions:{" "} - {parseInt(latestBlock.header.contentCommitment.numTxs, 16)} -
  • -
  • - Total Fees:{" "} - {parseInt(latestBlock.header.totalFees, 16)} (wei) -
  • -
  • - State Root:{" "} - {latestBlock.header.state.partial.publicDataTree.root} -
  • -
- ); -}; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { Landing } from "~/pages/landing"; export const Route = createLazyFileRoute("/")({ - component: Index, + component: Landing, }); - -function Index() { - return ( -
-

{text.exploreThePrivacyOnAztec}

-
-

Latest Block Data

- -
-
- ); -} - -const text = { - exploreThePrivacyOnAztec: "Explore the power of privacy on Aztec", -}; diff --git a/services/explorer-ui/src/routes/tx-effects/index.lazy.tsx b/services/explorer-ui/src/routes/tx-effects/index.lazy.tsx index 608020e6..7ca4c309 100644 --- a/services/explorer-ui/src/routes/tx-effects/index.lazy.tsx +++ b/services/explorer-ui/src/routes/tx-effects/index.lazy.tsx @@ -1,4 +1,4 @@ -import { Outlet, createLazyFileRoute, useParams } from "@tanstack/react-router"; +import { createLazyFileRoute } from "@tanstack/react-router"; import { txEffectSchema } from "~/components/tx-effects/tx-effects-schema"; import { TxEffectsTable } from "~/components/tx-effects/tx-effects-table"; import { useLatestBlocks } from "~/hooks"; @@ -9,17 +9,12 @@ export const Route = createLazyFileRoute("/tx-effects/")({ function TxEffects() { const { data: latestBlocks, isLoading, error } = useLatestBlocks(); - let isIndex = true; - try { - const params = useParams({ from: "/tx-effects/$txHash" }); - isIndex = !params.txHash; - } catch (e) { - console.error(e); - isIndex = true; - } + if (isLoading) return

Loading...

; if (error) return

{error.message}

; - const latestTxEffects = latestBlocks?.flatMap((block) => { + if (!latestBlocks) return

No data

; + + const latestTxEffects = latestBlocks.flatMap((block) => { return block.body.txEffects.map((txEffect) => txEffectSchema.parse({ txHash: txEffect.txHash, @@ -30,17 +25,13 @@ function TxEffects() { parseInt(txEffect.noteEncryptedLogsLength, 16), blockNumber: block.height, timestamp: parseInt(block.header.globalVariables.timestamp, 16) * 1000, - }) + }), ); }); - if (!latestTxEffects) return

No data

; - const text = { - title: isIndex ? "All TxEffects" : "TxEffect Details", - }; return (
-

{text.title}

+

All tx-effects

TxEffects in the last 24 hours

@@ -52,7 +43,7 @@ function TxEffects() {

TODO

- {isIndex ? : } +
); } diff --git a/services/explorer-ui/src/service/constants.ts b/services/explorer-ui/src/service/constants.ts index f8d709f6..b0fc91d3 100644 --- a/services/explorer-ui/src/service/constants.ts +++ b/services/explorer-ui/src/service/constants.ts @@ -14,6 +14,12 @@ export const aztecExplorer = { getL2ContractInstances: "l2/contract-instances", getL2ContractInstancesByBlockHash: (hash: string) => `l2/blocks/${hash}/contract-instances`, + + getL2TotalTxEffects: "l2/stats/total-tx-effects", + getL2TotalTxEffectsLast24h: "/l2/stats/tx-effects-last-24h", + getL2TotalContracts: "/l2/stats/total-contracts", + getL2AverageFees: "/l2/stats/average-fees", + getL2AverageBlockTime: "/l2/stats/average-block-time", }; export const API_URL = import.meta.env.VITE_API_URL;