Skip to content

Commit

Permalink
feat(api): add support for fetching blocks by slot (#700)
Browse files Browse the repository at this point in the history
* feat(api): add support for fetching blocks by slot

* chore: add changeset

* fix(api): extract logic into a separate procedure

* test(api): add test to ensure getBySlot does not return reorg blocks
  • Loading branch information
PJColombo authored Feb 5, 2025
1 parent 93da4b0 commit b90971b
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 80 deletions.
5 changes: 5 additions & 0 deletions .changeset/early-shrimps-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/api": minor
---

Added support for fetching blocks by slot
1 change: 1 addition & 0 deletions packages/api/src/routers/block/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./queries";
export * from "./selects";
export * from "./serializers";
100 changes: 100 additions & 0 deletions packages/api/src/routers/block/common/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { BlobStorageManager } from "@blobscan/blob-storage-manager";
import type { BlobscanPrismaClient, Prisma } from "@blobscan/db";

import type { Expands } from "../../../middlewares/withExpands";
import type { Filters } from "../../../middlewares/withFilters";
import {
calculateDerivedTxBlobGasFields,
retrieveBlobData,
} from "../../../utils";
import { createBlockSelect } from "./selects";
import type { QueriedBlock } from "./serializers";

export type BlockId = "hash" | "number" | "slot";
export type BlockIdField =
| { type: "hash"; value: string }
| { type: "number"; value: number }
| { type: "slot"; value: number };

function buildBlockWhereClause(
{ type, value }: BlockIdField,
filters: Filters
): Prisma.BlockWhereInput {
switch (type) {
case "hash": {
return { hash: value };
}
case "number": {
return { number: value, transactionForks: filters.blockType };
}
case "slot": {
return { slot: value, transactionForks: filters.blockType };
}
}
}

export async function fetchBlock(
blockId: BlockIdField,
{
blobStorageManager,
prisma,
filters,
expands,
}: {
blobStorageManager: BlobStorageManager;
prisma: BlobscanPrismaClient;
filters: Filters;
expands: Expands;
}
) {
const where = buildBlockWhereClause(blockId, filters);

const queriedBlock = await prisma.block.findFirst({
select: createBlockSelect(expands),
where,
});

if (!queriedBlock) {
return;
}

const block: QueriedBlock = queriedBlock;

if (expands.transaction) {
block.transactions = block.transactions.map((tx) => {
const { blobAsCalldataGasUsed, blobGasUsed, gasPrice, maxFeePerBlobGas } =
tx;
const derivedFields =
maxFeePerBlobGas && blobAsCalldataGasUsed && blobGasUsed && gasPrice
? calculateDerivedTxBlobGasFields({
blobAsCalldataGasUsed,
blobGasUsed,
gasPrice,
blobGasPrice: block.blobGasPrice,
maxFeePerBlobGas,
})
: {};

return {
...tx,
...derivedFields,
};
});
}

if (expands.blobData) {
const txsBlobs = block.transactions.flatMap((tx) => tx.blobs);

await Promise.all(
txsBlobs.map(async ({ blob }) => {
if (blob.dataStorageReferences?.length) {
const data = await retrieveBlobData(blobStorageManager, blob);

blob.data = data;
}
})
);
}

return block;
}
107 changes: 30 additions & 77 deletions packages/api/src/routers/block/getByBlockId.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";

import { z } from "@blobscan/zod";
import { hashSchema, z } from "@blobscan/zod";

import {
createExpandsSchema,
Expand All @@ -11,35 +11,16 @@ import {
withTypeFilterSchema,
} from "../../middlewares/withFilters";
import { publicProcedure } from "../../procedures";
import { calculateDerivedTxBlobGasFields, retrieveBlobData } from "../../utils";
import {
createBlockSelect,
serializeBlock,
serializedBlockSchema,
} from "./common";
import type { QueriedBlock } from "./common";
import type { BlockIdField } from "./common";
import { fetchBlock, serializeBlock, serializedBlockSchema } from "./common";

const blockIdSchema = z
.string()
.refine(
(id) => {
const isHash = id.startsWith("0x") && id.length === 66;
const s_ = Number(id);
const isNumber = !isNaN(s_) && s_ > 0;
const blockHashSchema = hashSchema.refine((value) => value.length === 66, {
message: "Block hashes must be 66 characters long",
});

return isHash || isNumber;
},
{
message: "Invalid block id",
}
)
.transform((id) => {
if (id.startsWith("0x")) {
return id;
}
const blockNumberSchema = z.coerce.number().int().positive();

return Number(id);
});
const blockIdSchema = z.union([blockHashSchema, blockNumberSchema]);

const inputSchema = z
.object({
Expand Down Expand Up @@ -68,64 +49,36 @@ export const getByBlockId = publicProcedure
ctx: { blobStorageManager, prisma, expands, filters },
input: { id },
}) => {
const isNumber = typeof id === "number";
let blockIdField: BlockIdField | undefined;

const queriedBlock = await prisma.block.findFirst({
select: createBlockSelect(expands),
where: {
[isNumber ? "number" : "hash"]: id,
// Hash is unique, so we don't need to filter by transaction forks if we're querying by it
transactionForks: isNumber ? filters.blockType : undefined,
},
});
const parsedHash = blockHashSchema.safeParse(id);
const parsedBlockNumber = blockNumberSchema.safeParse(id);

if (!queriedBlock) {
throw new TRPCError({
code: "NOT_FOUND",
message: `No block with id '${id}'.`,
});
if (parsedHash.success) {
blockIdField = { type: "hash", value: parsedHash.data };
} else if (parsedBlockNumber.success) {
blockIdField = { type: "number", value: parsedBlockNumber.data };
}

const block: QueriedBlock = queriedBlock;

if (expands.transaction) {
block.transactions = block.transactions.map((tx) => {
const {
blobAsCalldataGasUsed,
blobGasUsed,
gasPrice,
maxFeePerBlobGas,
} = tx;
const derivedFields =
maxFeePerBlobGas && blobAsCalldataGasUsed && blobGasUsed && gasPrice
? calculateDerivedTxBlobGasFields({
blobAsCalldataGasUsed,
blobGasUsed,
gasPrice,
blobGasPrice: block.blobGasPrice,
maxFeePerBlobGas,
})
: {};

return {
...tx,
...derivedFields,
};
if (!blockIdField) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Invalid block id "${id}"`,
});
}

if (expands.blobData) {
const txsBlobs = block.transactions.flatMap((tx) => tx.blobs);

await Promise.all(
txsBlobs.map(async ({ blob }) => {
if (blob.dataStorageReferences?.length) {
const data = await retrieveBlobData(blobStorageManager, blob);
const block = await fetchBlock(blockIdField, {
blobStorageManager,
prisma,
filters,
expands,
});

blob.data = data;
}
})
);
if (!block) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Block with id "${id}" not found`,
});
}

return serializeBlock(block);
Expand Down
59 changes: 59 additions & 0 deletions packages/api/src/routers/block/getBySlot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { TRPCError } from "@trpc/server";

import { z } from "@blobscan/zod";

import {
createExpandsSchema,
withExpands,
} from "../../middlewares/withExpands";
import {
withFilters,
withSortFilterSchema,
} from "../../middlewares/withFilters";
import { publicProcedure } from "../../procedures";
import type { BlockIdField } from "./common";
import { fetchBlock, serializeBlock } from "./common";

const inputSchema = z
.object({
slot: z.coerce.number().int().positive(),
})
.merge(withSortFilterSchema)
.merge(createExpandsSchema(["transaction", "blob", "blob_data"]));

export const getBySlot = publicProcedure
.meta({
openapi: {
method: "GET",
path: `/slots/{slot}`,
tags: ["slots"],
summary: "retrieves block details for given slot.",
},
})
.input(inputSchema)
.use(withExpands)
.use(withFilters)
.query(
async ({
ctx: { blobStorageManager, prisma, filters, expands },
input: { slot },
}) => {
const blockIdField: BlockIdField = { type: "slot", value: slot };

const block = await fetchBlock(blockIdField, {
blobStorageManager,
prisma,
filters,
expands,
});

if (!block) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Block with slot ${slot} not found`,
});
}

return serializeBlock(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,12 +1,14 @@
import { t } from "../../trpc-client";
import { getAll } from "./getAll";
import { getByBlockId } from "./getByBlockId";
import { getBySlot } from "./getBySlot";
import { getCount } from "./getCount";
import { getLatestBlock } from "./getGasPrice";

export const blockRouter = t.router({
getAll,
getByBlockId,
getBySlot,
getCount,
getLatestBlock,
});
Loading

0 comments on commit b90971b

Please sign in to comment.