Skip to content

Commit

Permalink
feat: implement OptimismCard component for displaying decoded transac…
Browse files Browse the repository at this point in the history
…tion fields and removes `autocompleteBlockHash` (#658)

* feat: implement OptimismCard component for displaying decoded transaction fields and removes `autocompleteBlockHash`

* chore: moves OptimismCard to its own file

* refactor: remove async/await from parsing functions

* test: add unit tests for checkBlockExists function

* fix: make txTimestamp prop optional in OptimismCard component

* refactor: simplify JSX structure in OptimismCard component

* refactor: replace CopyToClipboard with Copyable component in OptimismCard

* fix(web): Capitalize decoded field names

* chore: add changeset

---------

Co-authored-by: Pablo Castellano <pablo@anche.no>
Co-authored-by: PJColombo <paulo.colombo@pm.me>
  • Loading branch information
3 people authored Feb 13, 2025
1 parent 766ab15 commit 95f8043
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 143 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-suits-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/web": patch
---

Improved Optimism tx decoded fields card
5 changes: 5 additions & 0 deletions .changeset/light-balloons-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/api": patch
---

Removed auto-completion logic for hashes in Optimism decoded fields retrieval
95 changes: 95 additions & 0 deletions apps/web/src/components/OptimismCard.tsx
Original file line number Diff line number Diff line change
@@ -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<OptimismCardProps> = ({ 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 (
<Card header="Loading Decoded Fields...">
<div className="flex h-32 items-center justify-center">
<Loading className="h-8 w-8 animate-spin" />
</div>
</Card>
);
}

return (
<Card header="Decoded Fields">
<InfoGrid
fields={[
{
name: "Timestamp Since L2 Genesis",
value: <div className="whitespace-break-spaces">{timestamp}</div>,
},
{
name: "Last L1 Origin Number",
value: (
<Copyable
value={data.lastL1OriginNumber.toString()}
tooltipText="Copy last L1 origin number"
>
<Link href={blockLink}>{data.lastL1OriginNumber}</Link>
</Copyable>
),
},
{
name: "Parent L2 Partial Block Hash",
value: "0x" + data.parentL2BlockHash + "...",
},
{
name: "L1 Origin Partial Block Hash",
value: (
<Copyable value={hash} tooltipText="Copy L1 origin block hash">
<Link href={blockLink}>{hash}</Link>
</Copyable>
),
},
{
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,
},
]}
/>
</Card>
);
};
89 changes: 5 additions & 84 deletions apps/web/src/pages/tx/[hash].tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import type { FC } from "react";
import { useMemo } from "react";
import type { NextPage } from "next";
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";
Expand Down Expand Up @@ -256,62 +254,10 @@ const Tx: NextPage = () => {
/>

{decodedData && (
<Card header="Decoded Fields">
<div>
<InfoGrid
fields={[
{
name: "Timestamp since L2 genesis",
value: (
<div className="whitespace-break-spaces">
{tx
? formatTimestamp(
tx.blockTimestamp.subtract(
decodedData.timestampSinceL2Genesis,
"ms"
)
)
: ""}
</div>
),
},
{
name: "Last L1 origin number",
value: decodedData.lastL1OriginNumber,
},
{
name: "Parent L2 block hash",
value: "0x" + decodedData.parentL2BlockHash + "...",
},
{
name: "L1 origin block hash",
value: (
<BlockHash
fullHash={decodedData.fullL1OriginBlockHash}
partialHash={decodedData.l1OriginBlockHash}
/>
),
},
{
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,
},
]}
/>
</div>
</Card>
<OptimismCard
data={decodedData}
txTimestamp={tx ? tx.blockTimestamp : undefined}
/>
)}

<Card header={`Blobs ${tx ? `(${tx.blobs.length})` : ""}`}>
Expand All @@ -325,29 +271,4 @@ const Tx: NextPage = () => {
);
};

type BlockHashProps = {
partialHash: string;
fullHash: string | undefined;
};

const BlockHash: FC<BlockHashProps> = ({ fullHash, partialHash }) => {
if (fullHash === undefined) {
return "0x" + partialHash + "...";
}

const prefixedFullHash = "0x" + fullHash;

return (
<div className="flex items-center gap-2">
<Link href={`https://blobscan.com/block/${prefixedFullHash}`}>
{prefixedFullHash}
</Link>
<CopyToClipboard
value={prefixedFullHash}
tooltipText="Copy L1 origin block hash"
/>
</div>
);
};

export default Tx;
53 changes: 3 additions & 50 deletions packages/api/src/blob-parse/optimism.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -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<typeof OptimismDecodedDataSchema>;
export type OptimismDecodedData = z.infer<typeof OptimismDecodedDataSchema>;

export async function parseOptimismDecodedData(
export function parseOptimismDecodedData(
data: string
): Promise<OptimismDecodedData | null> {
): OptimismDecodedData | null {
let json;

try {
Expand All @@ -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;
}
4 changes: 2 additions & 2 deletions packages/api/src/blob-parse/parse-decoded-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export const decodedFields = z.union([OptimismSchema, UnknownSchema]);

type DecodedFields = z.infer<typeof decodedFields>;

export async function parseDecodedFields(data: string): Promise<DecodedFields> {
const optimismDecodedData = await parseOptimismDecodedData(data);
export function parseDecodedFields(data: string): DecodedFields {
const optimismDecodedData = parseOptimismDecodedData(data);

if (optimismDecodedData) {
return {
Expand Down
22 changes: 22 additions & 0 deletions packages/api/src/routers/block/checkBlobExists.ts
Original file line number Diff line number Diff line change
@@ -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);
});
2 changes: 2 additions & 0 deletions packages/api/src/routers/block/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { t } from "../../trpc-client";
import { checkBlockExists } from "./checkBlobExists";
import { getAll } from "./getAll";
import { getByBlockId } from "./getByBlockId";
import { getBySlot } from "./getBySlot";
Expand All @@ -11,4 +12,5 @@ export const blockRouter = t.router({
getBySlot,
getCount,
getLatestBlock,
checkBlockExists,
});
8 changes: 4 additions & 4 deletions packages/api/src/routers/tx/common/serializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,14 @@ export function serializeBaseTransactionFields(
};
}

export async function serializeTransaction(
export function serializeTransaction(
txQuery: FullQueriedTransaction
): Promise<SerializedTransaction> {
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,
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/routers/tx/getAll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 95f8043

Please sign in to comment.