diff --git a/.changeset/mean-rivers-study.md b/.changeset/mean-rivers-study.md new file mode 100644 index 000000000..5222689f3 --- /dev/null +++ b/.changeset/mean-rivers-study.md @@ -0,0 +1,5 @@ +--- +"@blobscan/api": minor +--- + +Added stat endpoint to return blobs per rollup data. diff --git a/.changeset/ninety-zebras-attack.md b/.changeset/ninety-zebras-attack.md new file mode 100644 index 000000000..7ce602140 --- /dev/null +++ b/.changeset/ninety-zebras-attack.md @@ -0,0 +1,5 @@ +--- +"@blobscan/web": minor +--- + +Updated Home page charts: increase to 30 days the 'Blob Gas Price Expenditure' chart and replace Daily Transactions chart by 'Blobs per rollup' diff --git a/apps/web/src/components/Cards/ChartCard.tsx b/apps/web/src/components/Cards/ChartCard.tsx index e3980ee93..f2369f5bd 100644 --- a/apps/web/src/components/Cards/ChartCard.tsx +++ b/apps/web/src/components/Cards/ChartCard.tsx @@ -18,9 +18,10 @@ type ChartCardProps = { function getSeriesDataState(series: EChartOption.Series[] | undefined) { return { - isLoading: series - ? series.some(({ data }) => data === undefined || data === null) - : true, + isLoading: + series && series.length > 0 + ? series.some(({ data }) => data === undefined || data === null) + : true, isEmpty: series ? series.some(({ data }) => data?.length === 0) : false, }; } diff --git a/apps/web/src/components/Charts/Blob/index.ts b/apps/web/src/components/Charts/Blob/index.ts index 7440ef98a..183775643 100644 --- a/apps/web/src/components/Charts/Blob/index.ts +++ b/apps/web/src/components/Charts/Blob/index.ts @@ -1,2 +1,3 @@ export * from "./DailyBlobsChart"; export * from "./DailyBlobSizeChart"; +export * from "../DailyBlobsPerRollupChart"; diff --git a/apps/web/src/components/Charts/Block/DailyAvgBlobGasPriceChart.tsx b/apps/web/src/components/Charts/Block/DailyAvgBlobGasPriceChart.tsx index 01d62fd9a..591a1a973 100644 --- a/apps/web/src/components/Charts/Block/DailyAvgBlobGasPriceChart.tsx +++ b/apps/web/src/components/Charts/Block/DailyAvgBlobGasPriceChart.tsx @@ -11,11 +11,12 @@ import { buildTimeSeriesOptions } from "~/utils"; export type DailyAvgBlobGasPriceChartProps = { days: DailyBlockStats["days"]; avgBlobGasPrices: DailyBlockStats["avgBlobGasPrices"]; + opts?: EChartOption; }; export const DailyAvgBlobGasPriceChart: FC< Partial -> = function ({ days, avgBlobGasPrices }) { +> = function ({ days, avgBlobGasPrices, opts = {} }) { const { unit } = useScaledWeiAmounts(avgBlobGasPrices); const options: EChartOption = { @@ -35,6 +36,7 @@ export const DailyAvgBlobGasPriceChart: FC< }, ], animationEasing: "cubicOut", + ...opts, }; return ( diff --git a/apps/web/src/components/Charts/DailyBlobsPerRollupChart.tsx b/apps/web/src/components/Charts/DailyBlobsPerRollupChart.tsx new file mode 100644 index 000000000..41c5cec4d --- /dev/null +++ b/apps/web/src/components/Charts/DailyBlobsPerRollupChart.tsx @@ -0,0 +1,40 @@ +import type { FC } from "react"; +import type { EChartOption } from "echarts"; + +import { ChartCard } from "~/components/Cards/ChartCard"; +import type { Rollup, RollupDailyStats } from "~/types"; +import { buildTimeSeriesOptions, capitalize, formatNumber } from "~/utils"; + +export type DailyRollupChartProps = { + days: RollupDailyStats["days"]; + blobsPerRollup: RollupDailyStats["blobsPerRollup"]; +}; + +export const DailyBlobsPerRollupChart: FC> = + function ({ days, blobsPerRollup }) { + const rollupNames = + blobsPerRollup && blobsPerRollup[0] + ? (Object.keys(blobsPerRollup[0]) as Rollup[]) + : []; + + const options: EChartOption = { + ...buildTimeSeriesOptions({ + dates: days, + axisFormatters: { + yAxisTooltip: (value) => formatNumber(value), + }, + }), + series: rollupNames.map((rollup) => ({ + name: capitalize(rollup), + type: "bar", + stack: "total", + data: blobsPerRollup + ? blobsPerRollup.map((dayEntry) => dayEntry[rollup as Rollup]) + : undefined, + })), + animationEasing: "cubicOut", + toolbox: { show: false }, + }; + + return ; + }; diff --git a/apps/web/src/pages/index.tsx b/apps/web/src/pages/index.tsx index e602ab75a..9ca97af47 100644 --- a/apps/web/src/pages/index.tsx +++ b/apps/web/src/pages/index.tsx @@ -9,8 +9,8 @@ import { MetricCard } from "~/components/Cards/MetricCard"; import { BlobCard } from "~/components/Cards/SurfaceCards/BlobCard"; import { BlobTransactionCard } from "~/components/Cards/SurfaceCards/BlobTransactionCard"; import { BlockCard } from "~/components/Cards/SurfaceCards/BlockCard"; -import { DailyBlobGasComparisonChart } from "~/components/Charts/Block"; -import { DailyTransactionsChart } from "~/components/Charts/Transaction"; +import { DailyBlobsPerRollupChart } from "~/components/Charts/Blob"; +import { DailyAvgBlobGasPriceChart } from "~/components/Charts/Block"; import { Link } from "~/components/Link"; import { SearchInput } from "~/components/SearchInput"; import { SlidableList } from "~/components/SlidableList"; @@ -26,7 +26,6 @@ import { } from "~/utils"; const LATEST_ITEMS_LENGTH = 5; -const DAILY_STATS_TIMEFRAME = "15d"; const CARD_HEIGHT = "sm:h-28"; @@ -46,14 +45,15 @@ const Home: NextPage = () => { }); const { data: rawOverallStats, error: overallStatsErr } = api.stats.getOverallStats.useQuery(); - const { data: dailyTxStats, error: dailyTxStatsErr } = - api.stats.getTransactionDailyStats.useQuery({ - timeFrame: DAILY_STATS_TIMEFRAME, - }); const { data: dailyBlockStats, error: dailyBlockStatsErr } = api.stats.getBlockDailyStats.useQuery({ - timeFrame: DAILY_STATS_TIMEFRAME, + timeFrame: "30d", + }); + const { data: dailyRollupStats, error: dailyRollupStatsErr } = + api.stats.getRollupDailyStats.useQuery({ + timeFrame: "90d", }); + const { blocks, transactions, blobs } = useMemo(() => { if (!rawBlocksData) { return { blocks: [], transactions: [], blobs: [] }; @@ -87,8 +87,8 @@ const Home: NextPage = () => { const error = latestBlocksError || overallStatsErr || - dailyTxStatsErr || - dailyBlockStatsErr; + dailyBlockStatsErr || + dailyRollupStatsErr; if (error) { return ( @@ -116,12 +116,9 @@ const Home: NextPage = () => {
-
@@ -170,12 +167,7 @@ const Home: NextPage = () => { />
- +
diff --git a/apps/web/src/types/routers.ts b/apps/web/src/types/routers.ts index e9eb84848..3693cbb20 100644 --- a/apps/web/src/types/routers.ts +++ b/apps/web/src/types/routers.ts @@ -22,3 +22,5 @@ export type DailyTransactionStats = export type DailyStats = RouterOutputs["stats"]["getDailyStats"][number]; export type OverallStats = RouterOutputs["stats"]["getOverallStats"]; + +export type RollupDailyStats = RouterOutputs["stats"]["getRollupDailyStats"]; diff --git a/packages/api/src/middlewares/withTimeFrame.ts b/packages/api/src/middlewares/withTimeFrame.ts index ea5761c22..e13d91a11 100644 --- a/packages/api/src/middlewares/withTimeFrame.ts +++ b/packages/api/src/middlewares/withTimeFrame.ts @@ -9,6 +9,7 @@ export const TIME_FRAMES = z.enum([ "7d", "15d", "30d", + "90d", "180d", "360d", "All", @@ -26,6 +27,8 @@ function getTimeFrameIntervals(timeFrame: TimeFrame): TimeInterval { case "7d": case "15d": case "30d": + case "90d": + case "180d": case "360d": default: { const day = parseInt(timeFrame.split("d")[0] ?? "1d"); diff --git a/packages/api/src/routers/stats/getRollupDailyStats.ts b/packages/api/src/routers/stats/getRollupDailyStats.ts new file mode 100644 index 000000000..e2ba329e1 --- /dev/null +++ b/packages/api/src/routers/stats/getRollupDailyStats.ts @@ -0,0 +1,70 @@ +import { z } from "@blobscan/zod"; + +import { + withTimeFrame, + withTimeFrameSchema, +} from "../../middlewares/withTimeFrame"; +import { publicProcedure } from "../../procedures"; +import { rollupSchema } from "../../utils"; + +const inputSchema = withTimeFrameSchema; + +export const outputSchema = z.object({ + days: z.array(z.string()), + blobsPerRollup: z.array(z.record(rollupSchema, z.number())), +}); + +type OutputSchema = z.infer; + +export const getRollupDailyStats = publicProcedure + .input(inputSchema) + .use(withTimeFrame) + .output(outputSchema) + .query(async ({ ctx: { prisma, timeFrame } }) => { + const stats = await prisma.dailyStats.groupBy({ + by: ["day", "rollup"], + _sum: { + totalBlobs: true, + }, + where: { + AND: [ + { + day: { + gte: timeFrame.initial.toDate(), + lte: timeFrame.final.toDate(), + }, + }, + { + category: null, + }, + ], + }, + orderBy: { + day: "asc", + }, + }); + + if (stats.length === 0) { + return { days: [], blobsPerRollup: [] }; + } + + const uniqueDays = Array.from( + new Set(stats.map((item) => item.day.toISOString())) + ); + + const formattedStats: OutputSchema = { + days: uniqueDays, + blobsPerRollup: uniqueDays.map((day) => + stats.reduce((acc, item) => { + if (item.day.toISOString() === day && item.rollup) { + const lowercaseRollup = item.rollup.toLowerCase(); + acc[lowercaseRollup] = + (acc[lowercaseRollup] ?? 0) + (item._sum.totalBlobs ?? 0); + } + return acc; + }, {} as { [key: string]: number }) + ), + }; + + return formattedStats; + }); diff --git a/packages/api/src/routers/stats/index.ts b/packages/api/src/routers/stats/index.ts index 5fdc25431..f3e5b28c6 100644 --- a/packages/api/src/routers/stats/index.ts +++ b/packages/api/src/routers/stats/index.ts @@ -5,6 +5,7 @@ import { getBlockDailyStats } from "./getBlockDailyStats"; import { getBlockOverallStats } from "./getBlockOverallStats"; import { getDailyStats } from "./getDailyStats"; import { getOverallStats } from "./getOverallStats"; +import { getRollupDailyStats } from "./getRollupDailyStats"; import { getTransactionDailyStats } from "./getTransactionDailyStats"; import { getTransactionOverallStats } from "./getTransactionOverallStats"; @@ -17,4 +18,5 @@ export const statsRouter = t.router({ getBlockOverallStats, getTransactionDailyStats, getTransactionOverallStats, + getRollupDailyStats, });