Skip to content

Commit

Permalink
feat: add weavevm support (#681)
Browse files Browse the repository at this point in the history
* feat(db): add support for weavevm blob storage

* feat(web): add weavevm badge

* chore(web): optimize paradex svg image

* style(web): format main page source code

* feat(api): add weavevm storage

* feat(blob-storage-manager): add weavevm blob storage

* fix(blob-propagator): skip blob storage queue instantiation for blob storages without a defined worker

* feat(blob-storage-manager): add support for specifying Weavem storage API endpoint

* feat(docs): add weavem to available storages

* chore: add changeset

* style: update weavevm name + rename api endpoint env var

* fix(web): use dark and light weavevm logo

* fix(blob-propagator): fix available blob storages check when creating a new blob propagator
  • Loading branch information
PJColombo authored Jan 15, 2025
1 parent 2b252b1 commit e1421f6
Show file tree
Hide file tree
Showing 24 changed files with 406 additions and 37 deletions.
9 changes: 9 additions & 0 deletions .changeset/dirty-islands-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@blobscan/blob-storage-manager": minor
"@blobscan/api": minor
"@blobscan/env": minor
"@blobscan/db": minor
"@blobscan/web": minor
---

Added Weavevm blob storage support
2 changes: 2 additions & 0 deletions apps/docs/src/app/docs/environment/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ nextjs:
| `BEE_ENDPOINT` | Bee endpoint | No | (empty) |
| `FILE_SYSTEM_STORAGE_ENABLED` | Store blobs in filesystem | No | `false` |
| `FILE_SYSTEM_STORAGE_PATH` | Store blobs in this path | No | `/tmp/blobscan-blobs` |
| `WEAVEVM_STORAGE_ENABLED` | Weavevm storage usage | No | `false` |
| `WEAVEVM_STORAGE_API_BASE_URL` | Weavevm API base url | No | (empty) |
| `STATS_SYNCER_DAILY_CRON_PATTERN` | Cron pattern for the daily stats job | No | `30 0 * * * *` |
| `STATS_SYNCER_OVERALL_CRON_PATTERN` | Cron pattern for the overall stats job | No | `*/15 * * * *` |
| `SWARM_STAMP_CRON_PATTERN` | Cron pattern for swarm job | No | `*/15 * * * *` |
Expand Down
7 changes: 4 additions & 3 deletions apps/docs/src/app/docs/storages/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ nextjs:

Blobscan can be configured to use any of the following blob storages:

- PostgreSQL
- Google Cloud Storage
- Ethereum Swarm
- [Ethereum Swarm](https://www.ethswarm.org/)
- File system
- Google Cloud Storage
- PostgreSQL
- [Weavevm](https://www.wvm.dev/) (currently supports blob reading only)

By default all storages are disabled and you must enable at least one in order to run Blobscan. This is done using [environment variables](/docs/environment).

Expand Down
16 changes: 8 additions & 8 deletions apps/web/src/components/Badges/StorageBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FC, HTMLAttributes, ReactNode } from "react";
import type { FC, HTMLAttributes } from "react";
import React from "react";
import NextLink from "next/link";

Expand All @@ -10,32 +10,32 @@ import { Badge } from "./Badge";

type StorageConfig = {
name?: string;
icon: ReactNode;
style: HTMLAttributes<HTMLDivElement>["className"];
};

const STORAGE_CONFIGS: Record<BlobStorage, StorageConfig> = {
file_system: {
name: "File System",
icon: <StorageIcon storage="file_system" />,
style:
"bg-gray-100 hover:bg-gray-200 text-gray-800 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-gray-200",
},
google: {
icon: <StorageIcon storage="google" />,
style:
"bg-slate-100 hover:bg-slate-200 text-slate-800 hover:text-slate-900 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600 dark:hover:text-slate-200",
},
swarm: {
icon: <StorageIcon storage="swarm" />,
style:
"bg-orange-100 hover:bg-orange-200 text-orange-800 hover:text-orange-900 dark:bg-orange-900 dark:text-orange-300 dark:hover:bg-orange-800 dark:hover:text-orange-200",
},
postgres: {
icon: <StorageIcon storage="postgres" />,
style:
"bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-800",
},
weavevm: {
name: "WeaveVM",
style:
"bg-gray-100 hover:bg-gray-200 text-gray-800 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-gray-200",
},
};

type StorageBadgeProps = BadgeProps & {
Expand All @@ -48,12 +48,12 @@ export const StorageBadge: FC<StorageBadgeProps> = ({
url,
...props
}) => {
const { icon, name, style } = STORAGE_CONFIGS[storage];
const { name, style } = STORAGE_CONFIGS[storage];

return (
<NextLink href={url} target={url !== "#" ? "_blank" : "_self"}>
<Badge className={style} {...props}>
{icon}
<StorageIcon storage={storage} />
{name ?? capitalize(storage)}
</Badge>
</NextLink>
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/components/StorageIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import NextLink from "next/link";
import { ArchiveBoxIcon } from "@heroicons/react/24/outline";
import cn from "classnames";
import { useTheme } from "next-themes";

import GoogleIcon from "~/icons/google.svg";
import PostgresIcon from "~/icons/postgres.svg";
import SwarmIcon from "~/icons/swarm.svg";
import WeaveVMDarkIcon from "~/icons/weavevm-dark.svg";
import WeaveVMLightIcon from "~/icons/weavevm-light.svg";
import type { BlobStorage, Size } from "~/types";
import { capitalize } from "~/utils";

Expand All @@ -19,6 +22,7 @@ export const StorageIcon: React.FC<StorageIconProps> = ({
storage,
url = "#",
}) => {
const { resolvedTheme } = useTheme();
const commonStyles = cn({
"h-3 w-3": size === "sm",
"h-4 w-4": size === "md",
Expand All @@ -40,6 +44,14 @@ export const StorageIcon: React.FC<StorageIconProps> = ({
case "postgres":
storageIcon = <PostgresIcon className={commonStyles} />;
break;
case "weavevm":
storageIcon =
resolvedTheme === "light" ? (
<WeaveVMLightIcon className={commonStyles} />
) : (
<WeaveVMDarkIcon className={commonStyles} />
);
break;
}

return (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/icons/paradex.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/src/icons/weavevm-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/src/icons/weavevm-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 12 additions & 7 deletions apps/web/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ const Home: NextPage = () => {
blobs,
};
}, [rawBlocksData]);
const overallStats = useMemo(() => rawOverallStats ? deserializeOverallStats(rawOverallStats) : undefined, [rawOverallStats]);
const overallStats = useMemo(
() =>
rawOverallStats ? deserializeOverallStats(rawOverallStats) : undefined,
[rawOverallStats]
);

const error =
latestBlocksError ||
Expand All @@ -95,7 +99,6 @@ const Home: NextPage = () => {
);
}


return (
<div className="flex flex-col items-center justify-center gap-12 sm:gap-20">
<div className=" flex flex-col items-center justify-center gap-8 md:w-8/12">
Expand Down Expand Up @@ -127,8 +130,10 @@ const Home: NextPage = () => {
<MetricCard
name="Total Tx Fees Saved"
metric={{
value: overallStats ?
overallStats.totalBlobAsCalldataFee - overallStats.totalBlobFee : undefined,
value: overallStats
? overallStats.totalBlobAsCalldataFee -
overallStats.totalBlobFee
: undefined,
type: "ethereum",
}}
compact
Expand Down Expand Up @@ -176,7 +181,7 @@ const Home: NextPage = () => {
<div className="grid grid-cols-1 items-stretch justify-stretch gap-6 lg:grid-cols-3">
<Card
header={
<div className="flex-wrap flex flex-col justify-between gap-3 2xl:flex-row 2xl:items-center">
<div className="flex flex-col flex-wrap justify-between gap-3 2xl:flex-row 2xl:items-center">
<div>Latest Blocks</div>
<Button
variant="outline"
Expand Down Expand Up @@ -209,7 +214,7 @@ const Home: NextPage = () => {
</Card>
<Card
header={
<div className="flex-wrap flex flex-col justify-between gap-3 2xl:flex-row 2xl:items-center">
<div className="flex flex-col flex-wrap justify-between gap-3 2xl:flex-row 2xl:items-center">
<div>Latest Blob Transactions</div>
<Button
variant="outline"
Expand Down Expand Up @@ -261,7 +266,7 @@ const Home: NextPage = () => {
</Card>
<Card
header={
<div className="flex-wrap flex flex-col justify-between gap-3 2xl:flex-row 2xl:items-center">
<div className="flex flex-col flex-wrap justify-between gap-3 2xl:flex-row 2xl:items-center">
<div>Latest Blobs</div>
<Button
variant="outline"
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/utils/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const zodBlobStorageEnums = [
"swarm",
"postgres",
"file_system",
"weavevm",
] as const;

const zodRollupEnums = [
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/utils/serializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export function buildBlobDataUrl(
case "POSTGRES": {
return `${env.BLOBSCAN_API_BASE_URL}/blobs/${blobDataUri}/data`;
}
case "WEAVEVM": {
return `https://blobscan.shuttleapp.rs/v1/blob/${blobDataUri}`;
}
}
}

Expand Down
41 changes: 32 additions & 9 deletions packages/blob-propagator/src/BlobPropagator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ export type BlobPropagatorConfig = {

export const STORAGE_WORKER_PROCESSORS: Record<
BlobStorageName,
BlobPropagationWorkerProcessor
BlobPropagationWorkerProcessor | undefined
> = {
GOOGLE: gcsProcessor,
SWARM: swarmProcessor,
POSTGRES: postgresProcessor,
FILE_SYSTEM: fileSystemProcessor,
WEAVEVM: undefined,
};

export class BlobPropagator {
Expand All @@ -81,25 +82,47 @@ export class BlobPropagator {
...workerOptions,
};

const temporaryBlobStorage = blobStorageManager.getStorage(tmpBlobStorage);

if (!temporaryBlobStorage) {
throw new BlobPropagatorCreationError("Temporary blob storage not found");
}

const availableStorageNames = blobStorageManager
.getAllStorages()
.map((s) => s.name)
.filter((name) => name !== tmpBlobStorage);
const temporaryBlobStorage = blobStorageManager.getStorage(tmpBlobStorage);
.filter((name) => name !== tmpBlobStorage)
.filter((name) => {
const hasWorkerProcessor = !!STORAGE_WORKER_PROCESSORS[name];

if (!availableStorageNames) {
throw new BlobPropagatorCreationError("No blob storages available");
}
if (!hasWorkerProcessor) {
logger.warn(
`Worker processor not defined for storage "${name}"; skipping`
);
}

if (!temporaryBlobStorage) {
throw new BlobPropagatorCreationError("Temporary blob storage not found");
return hasWorkerProcessor;
});

if (!availableStorageNames.length) {
throw new BlobPropagatorCreationError(
"None of the available storages have worker processors defined"
);
}

this.storageWorkers = availableStorageNames.map(
(storageName: BlobStorageName) => {
const workerProcessor = STORAGE_WORKER_PROCESSORS[storageName];

if (!workerProcessor) {
throw new BlobPropagatorCreationError(
`Worker processor not defined for storage "${storageName}"`
);
}

return this.#createWorker(
STORAGE_WORKER_NAMES[storageName],
STORAGE_WORKER_PROCESSORS[storageName]({
workerProcessor({
prisma,
blobStorageManager,
}),
Expand Down
18 changes: 9 additions & 9 deletions packages/blob-propagator/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import type { JobsOptions, WorkerOptions } from "bullmq";
import { BlobStorage as BlobStorageName } from "@blobscan/db/prisma/enums";
import { env } from "@blobscan/env";

export const STORAGE_WORKER_NAMES = Object.values(BlobStorageName).reduce<
Record<BlobStorageName, string>
>(
(names, storage) => ({
...names,
[storage]: `${storage.toLowerCase()}-worker`,
}),
{} as Record<BlobStorageName, string>
);
export const STORAGE_WORKER_NAMES = Object.values(BlobStorageName)
.filter((blobStorageName) => blobStorageName !== "WEAVEVM")
.reduce<Record<BlobStorageName, string>>(
(names, storage) => ({
...names,
[storage]: `${storage.toLowerCase()}-worker`,
}),
{} as Record<BlobStorageName, string>
);

export const FINALIZER_WORKER_NAME = "finalizer-worker";

Expand Down
32 changes: 32 additions & 0 deletions packages/blob-propagator/test/BlobPropagator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
PostgresStorage,
WeaveVMStorage,
createStorageFromEnv,
getBlobStorageManager,
} from "@blobscan/blob-storage-manager";
Expand Down Expand Up @@ -37,6 +38,15 @@ export class MockedBlobPropagator extends BlobPropagator {
}
}

class MockedWeaveVMStorage extends WeaveVMStorage {
constructor() {
super({
apiBaseUrl: "http://localhost:8000",
chainId: 1,
});
}
}

describe("BlobPropagator", () => {
let blobStorageManager: BlobStorageManager;
let tmpBlobStorage: FileSystemStorage;
Expand Down Expand Up @@ -102,6 +112,28 @@ describe("BlobPropagator", () => {
}
);

testValidError(
"should throw a valid error when creating a blob propagator with blob storages without worker processors",
() => {
const weavevmStorage = new MockedWeaveVMStorage();

const blobStorageManager = new BlobStorageManager([weavevmStorage]);

new MockedBlobPropagator({
blobStorageManager,
prisma,
tmpBlobStorage: env.BLOB_PROPAGATOR_TMP_BLOB_STORAGE,
workerOptions: {
connection,
},
});
},
BlobPropagatorCreationError,
{
checkCause: true,
}
);

testValidError(
"should throw a valid error when creating a blob propagator with no temporary blob storage",
async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`BlobPropagator > should throw a valid error when creating a blob propagator with blob storages without worker processors 1`] = `"Blob propagator failed: Failed to create blob propagator"`;

exports[`BlobPropagator > should throw a valid error when creating a blob propagator with blob storages without worker processors 2`] = `"Temporary blob storage not found"`;

exports[`BlobPropagator > should throw a valid error when creating a blob propagator with no blob storages 1`] = `"Blob propagator failed: Failed to create blob propagator"`;

exports[`BlobPropagator > should throw a valid error when creating a blob propagator with no blob storages 2`] = `"Temporary blob storage not found"`;
Expand Down
7 changes: 7 additions & 0 deletions packages/blob-propagator/test/storage-workers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ function runWorkerTests(

const storageWorker = STORAGE_WORKER_PROCESSORS[storageName];

if (!storageWorker) {
throw new Error(
`No worker processor found for the "${storageName}" storage`
);
}

storageWorkerProcessor = storageWorker(workerParams);

blobStorageManager = workerParams.blobStorageManager;
prisma = workerParams.prisma;
});
Expand Down
Loading

0 comments on commit e1421f6

Please sign in to comment.