diff --git a/apps/web/src/components/OptimismCard.tsx b/apps/web/src/components/OptimismCard.tsx new file mode 100644 index 000000000..ba1ae01ec --- /dev/null +++ b/apps/web/src/components/OptimismCard.tsx @@ -0,0 +1,95 @@ +import type { FC } from "react"; + +import type { OptimismDecodedData } from "@blobscan/api/src/blob-parse/optimism"; +import type dayjs from "@blobscan/dayjs"; + +import { InfoGrid } from "~/components/InfoGrid"; +import { Link } from "~/components/Link"; +import { api } from "~/api-client"; +import Loading from "~/icons/loading.svg"; +import { formatTimestamp } from "~/utils"; +import { Card } from "./Cards/Card"; +import { Copyable } from "./Copyable"; + +type OptimismCardProps = { + data: OptimismDecodedData; + txTimestamp?: dayjs.Dayjs; +}; + +export const OptimismCard: FC = ({ data, txTimestamp }) => { + const { data: blockExists, isLoading } = api.block.checkBlockExists.useQuery({ + blockNumber: data.lastL1OriginNumber, + }); + + const blockLink = blockExists + ? `https://blobscan.com/block/${data.lastL1OriginNumber}` + : `https://etherscan.io/block/${data.lastL1OriginNumber}`; + + const hash = `0x${data.l1OriginBlockHash}...`; + + const timestamp = txTimestamp + ? formatTimestamp(txTimestamp.subtract(data.timestampSinceL2Genesis, "ms")) + : undefined; + + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + return ( + + {timestamp}, + }, + { + name: "Last L1 origin number", + value: ( + + {data.lastL1OriginNumber} + + ), + }, + { + name: "Parent L2 block hash", + value: "0x" + data.parentL2BlockHash + "...", + }, + { + name: "L1 origin block hash", + value: ( + + {hash} + + ), + }, + { + name: "Number of L2 blocks", + value: data.numberOfL2Blocks, + }, + { + name: "Changed by L1 origin", + value: data.changedByL1Origin, + }, + { + name: "Total transactions", + value: data.totalTxs, + }, + { + name: "Contract creation transactions", + value: data.contractCreationTxsNumber, + }, + ]} + /> + + ); +}; diff --git a/apps/web/src/pages/tx/[hash].tsx b/apps/web/src/pages/tx/[hash].tsx index 2be335e22..9092d1301 100644 --- a/apps/web/src/pages/tx/[hash].tsx +++ b/apps/web/src/pages/tx/[hash].tsx @@ -1,4 +1,3 @@ -import type { FC } from "react"; import { useMemo } from "react"; import type { NextPage } from "next"; import { useRouter } from "next/router"; @@ -6,14 +5,13 @@ import { useRouter } from "next/router"; import { RollupBadge } from "~/components/Badges/RollupBadge"; import { Card } from "~/components/Cards/Card"; import { BlobCard } from "~/components/Cards/SurfaceCards/BlobCard"; -import { CopyToClipboard } from "~/components/CopyToClipboard"; import { Copyable } from "~/components/Copyable"; import { StandardEtherUnitDisplay } from "~/components/Displays/StandardEtherUnitDisplay"; -import { InfoGrid } from "~/components/InfoGrid"; import { DetailsLayout } from "~/components/Layouts/DetailsLayout"; import type { DetailsLayoutProps } from "~/components/Layouts/DetailsLayout"; import { Link } from "~/components/Link"; import { NavArrows } from "~/components/NavArrows"; +import { OptimismCard } from "~/components/OptimismCard"; import { BlockStatus } from "~/components/Status"; import { api } from "~/api-client"; import NextError from "~/pages/_error"; @@ -256,62 +254,10 @@ const Tx: NextPage = () => { /> {decodedData && ( - -
- - {tx - ? formatTimestamp( - tx.blockTimestamp.subtract( - decodedData.timestampSinceL2Genesis, - "ms" - ) - ) - : ""} -
- ), - }, - { - name: "Last L1 origin number", - value: decodedData.lastL1OriginNumber, - }, - { - name: "Parent L2 block hash", - value: "0x" + decodedData.parentL2BlockHash + "...", - }, - { - name: "L1 origin block hash", - value: ( - - ), - }, - { - name: "Number of L2 blocks", - value: decodedData.numberOfL2Blocks, - }, - { - name: "Changed by L1 origin", - value: decodedData.changedByL1Origin, - }, - { - name: "Total transactions", - value: decodedData.totalTxs, - }, - { - name: "Contract creation transactions", - value: decodedData.contractCreationTxsNumber, - }, - ]} - /> - -
+ )} @@ -325,29 +271,4 @@ const Tx: NextPage = () => { ); }; -type BlockHashProps = { - partialHash: string; - fullHash: string | undefined; -}; - -const BlockHash: FC = ({ fullHash, partialHash }) => { - if (fullHash === undefined) { - return "0x" + partialHash + "..."; - } - - const prefixedFullHash = "0x" + fullHash; - - return ( -
- - {prefixedFullHash} - - -
- ); -}; - export default Tx; diff --git a/packages/api/src/blob-parse/optimism.ts b/packages/api/src/blob-parse/optimism.ts index 538aaea8c..a61081f8f 100644 --- a/packages/api/src/blob-parse/optimism.ts +++ b/packages/api/src/blob-parse/optimism.ts @@ -1,8 +1,5 @@ import { z } from "zod"; -import { prisma } from "@blobscan/db"; -import { logger } from "@blobscan/logger"; - export const OptimismDecodedDataSchema = z.object({ timestampSinceL2Genesis: z.number(), lastL1OriginNumber: z.number(), @@ -12,14 +9,13 @@ export const OptimismDecodedDataSchema = z.object({ changedByL1Origin: z.number(), totalTxs: z.number(), contractCreationTxsNumber: z.number(), - fullL1OriginBlockHash: z.string().optional(), }); -type OptimismDecodedData = z.infer; +export type OptimismDecodedData = z.infer; -export async function parseOptimismDecodedData( +export function parseOptimismDecodedData( data: string -): Promise { +): OptimismDecodedData | null { let json; try { @@ -34,48 +30,5 @@ export async function parseOptimismDecodedData( return null; } - const hash = await autocompleteBlockHash(decoded.data.l1OriginBlockHash); - - if (hash) { - decoded.data.fullL1OriginBlockHash = hash; - } else { - logger.error( - `Failed to get full block hash for L1 origin block hash: ${decoded.data.l1OriginBlockHash}` - ); - } - return decoded.data; } - -/* Autocomplete a block hash from a truncated version of it. - @param partialHash - The first bytes of a block hash. - @returns The block hash, if there is a single occurrence, or null. - */ -async function autocompleteBlockHash(partialHash: string) { - if (!partialHash) { - return null; - } - - const blocks = await prisma.block.findMany({ - where: { - hash: { - startsWith: "0x" + partialHash, - }, - }, - select: { - hash: true, - }, - }); - - if (blocks[0] === undefined) { - return null; - } - - if (blocks.length > 1) { - logger.error( - `Found ${blocks.length} blocks while autocompleting block hash ${partialHash}` - ); - } - - return blocks[0].hash; -} diff --git a/packages/api/src/blob-parse/parse-decoded-fields.ts b/packages/api/src/blob-parse/parse-decoded-fields.ts index 8d76527b4..41e8faaa1 100644 --- a/packages/api/src/blob-parse/parse-decoded-fields.ts +++ b/packages/api/src/blob-parse/parse-decoded-fields.ts @@ -19,8 +19,8 @@ export const decodedFields = z.union([OptimismSchema, UnknownSchema]); type DecodedFields = z.infer; -export async function parseDecodedFields(data: string): Promise { - const optimismDecodedData = await parseOptimismDecodedData(data); +export function parseDecodedFields(data: string): DecodedFields { + const optimismDecodedData = parseOptimismDecodedData(data); if (optimismDecodedData) { return { diff --git a/packages/api/src/routers/block/checkBlobExists.ts b/packages/api/src/routers/block/checkBlobExists.ts new file mode 100644 index 000000000..c6b454fc3 --- /dev/null +++ b/packages/api/src/routers/block/checkBlobExists.ts @@ -0,0 +1,22 @@ +import { z } from "@blobscan/zod"; + +import { publicProcedure } from "../../procedures"; + +export const checkBlockExists = publicProcedure + .input( + z.object({ + blockNumber: z.number(), + }) + ) + .query(async ({ ctx: { prisma }, input }) => { + const block = await prisma.block.findFirst({ + where: { + number: input.blockNumber, + }, + select: { + number: true, + }, + }); + + return Boolean(block); + }); diff --git a/packages/api/src/routers/block/index.ts b/packages/api/src/routers/block/index.ts index 466c73898..996dd1ce6 100644 --- a/packages/api/src/routers/block/index.ts +++ b/packages/api/src/routers/block/index.ts @@ -1,4 +1,5 @@ import { t } from "../../trpc-client"; +import { checkBlockExists } from "./checkBlobExists"; import { getAll } from "./getAll"; import { getByBlockId } from "./getByBlockId"; import { getCount } from "./getCount"; @@ -9,4 +10,5 @@ export const blockRouter = t.router({ getByBlockId, getCount, getLatestBlock, + checkBlockExists, }); diff --git a/packages/api/src/routers/tx/common/serializers.ts b/packages/api/src/routers/tx/common/serializers.ts index 63ee2b31a..0be070ad8 100644 --- a/packages/api/src/routers/tx/common/serializers.ts +++ b/packages/api/src/routers/tx/common/serializers.ts @@ -125,14 +125,14 @@ export function serializeBaseTransactionFields( }; } -export async function serializeTransaction( +export function serializeTransaction( txQuery: FullQueriedTransaction -): Promise { - const serializedBaseTx = await serializeBaseTransactionFields(txQuery); +): SerializedTransaction { + const serializedBaseTx = serializeBaseTransactionFields(txQuery); const serializedAdditionalTx = serializeDerivedTxBlobGasFields(txQuery); const decodedFieldsString = JSON.stringify(txQuery.decodedFields); - const decodedFields = await parseDecodedFields(decodedFieldsString); + const decodedFields = parseDecodedFields(decodedFieldsString); return { ...serializedBaseTx, diff --git a/packages/api/src/routers/tx/getAll.ts b/packages/api/src/routers/tx/getAll.ts index 21d37dd9c..a59e656cf 100644 --- a/packages/api/src/routers/tx/getAll.ts +++ b/packages/api/src/routers/tx/getAll.ts @@ -90,9 +90,9 @@ export const getAll = publicProcedure countOp, ]); - const transactions = await Promise.all( - queriedTxs.map(addDerivedFieldsToTransaction).map(serializeTransaction) - ); + const transactions = queriedTxs + .map(addDerivedFieldsToTransaction) + .map(serializeTransaction); return { transactions, diff --git a/packages/api/test/block.test.ts b/packages/api/test/block.test.ts index 11f83cc86..532b39331 100644 --- a/packages/api/test/block.test.ts +++ b/packages/api/test/block.test.ts @@ -53,6 +53,24 @@ describe("Block router", async () => { }); }); + describe("checkBlockExists", () => { + it("should return true for an existing block", async () => { + const result = await caller.block.checkBlockExists({ + blockNumber: 1002, + }); + + expect(result).toBe(true); + }); + + it("should return false for a non-existent block", async () => { + const result = await caller.block.checkBlockExists({ + blockNumber: 99999999, + }); + + expect(result).toBe(false); + }); + }); + describe("getByBlockId", () => { it("should get a block by hash", async () => { const result = await caller.block.getByBlockId({