Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c87099e
wip
PJColombo Jul 2, 2025
e815f68
feat(zod): add functions to hide sensitive data
PJColombo Jul 3, 2025
1c68a37
chore(jobs): add esbuild config logic
PJColombo Jul 3, 2025
251203c
fix(jobs): improve eth price cron job
PJColombo Jul 3, 2025
b73e36e
feat(jobs): add swarm stamp cron job
PJColombo Jul 3, 2025
3b2c84d
feat(jobs): add stats job
PJColombo Jul 3, 2025
9a343cb
chore(jobs): improve eth price env var display
PJColombo Jul 3, 2025
15f288e
chore(jobs): add script to build `dist` and metadata
PJColombo Jul 3, 2025
ac390db
chore(jobs): include esbuild config file in tsconfig
PJColombo Jul 3, 2025
57e0a27
chore(jobs): add `dev`script commands
PJColombo Jul 3, 2025
4f50215
chore: add script commands to run all jobs concurrently
PJColombo Jul 3, 2025
5d14b6d
chore(jobs): set up vitest
PJColombo Jul 3, 2025
9d8bd40
test(jobs): add UTs
PJColombo Jul 3, 2025
58c4937
style(jobs): rename `CronJob` to `BaseCronJob`
PJColombo Jul 3, 2025
3ab06ae
style(jobs): move swarm error to swarm job file
PJColombo Jul 3, 2025
94d8b6e
chore: uppdate `pnpm-lock` file
PJColombo Jul 3, 2025
8e9edc3
chore(jobs): move changelog
PJColombo Jul 3, 2025
0d9ab90
chore: match `fast-glob` deps
PJColombo Jul 3, 2025
b58d582
feat(rest-api-server): remove syncers
PJColombo Jul 3, 2025
7c49ea9
chore: add `apps` to vitest workspace config file
PJColombo Jul 3, 2025
5e09e2b
feat: drop `@blobscan/syncers` package
PJColombo Jul 3, 2025
5b6c957
docs: add jobs app
PJColombo Jul 3, 2025
9dda5e9
chore: add changeset
PJColombo Jul 3, 2025
cce007a
test(jobs): fix snapshot
PJColombo Jul 4, 2025
0f4d29a
feat(jobs): add hoodi
PJColombo Jul 4, 2025
1804065
chore(docker): add `@blobscan/jobs` build image and container executi…
PJColombo Jul 4, 2025
1acd535
ci: add steps to create and publish `@blobscan/jobs` dev and prod doc…
PJColombo Jul 4, 2025
20bd5de
refactor(jobs): expose a single entry point
PJColombo Jul 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-pigs-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@blobscan/rest-api-server": minor
---

Removed syncers
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ DIRECT_URL=postgresql://blobscan:s3cr3t@localhost:5432/blobscan_dev?schema=publi

BLOBSCAN_WEB_TAG=next
BLOBSCAN_API_TAG=next
BLOBSCAN_JOBS_TAG=next
INDEXER_TAG=master

### blobscan website
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,32 @@ jobs:
DATABASE_URL=${{ env.DATABASE_URL }}
DIRECT_URL=${{ env.DIRECT_URL }}

- name: Extract metadata (tags, labels) for Docker (jobs)
id: meta_jobs
uses: docker/metadata-action@v5
with:
images: |
blossomlabs/blobscan-jobs
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}

- name: Build and push (jobs)
uses: docker/[email protected]
with:
context: .
push: true
target: jobs
tags: ${{ steps.meta_api.outputs.tags }}
labels: ${{ steps.meta_api.outputs.labels }}
build-args: |
NEXT_PUBLIC_BLOBSCAN_RELEASE=${{ env.NEXT_PUBLIC_BLOBSCAN_RELEASE }}
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
GIT_COMMIT=${{ env.GIT_COMMIT }}
DATABASE_URL=${{ env.DATABASE_URL }}
DIRECT_URL=${{ env.DIRECT_URL }}

- name: Extract metadata (tags, labels) for Docker (Web)
id: meta_web
uses: docker/metadata-action@v5
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/docker_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ jobs:
DATABASE_URL=${{ env.DATABASE_URL }}
DIRECT_URL=${{ env.DIRECT_URL }}

- name: Build and push (jobs)
uses: docker/[email protected]
with:
context: .
push: true
target: jobs
tags: |
blossomlabs/blobscan-jobs:development
build-args: |
NEXT_PUBLIC_BLOBSCAN_RELEASE=${{ env.NEXT_PUBLIC_BLOBSCAN_RELEASE }}
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
GIT_COMMIT=${{ env.GIT_COMMIT }}
DATABASE_URL=${{ env.DATABASE_URL }}
DIRECT_URL=${{ env.DIRECT_URL }}

- name: Build and push (web)
uses: docker/[email protected]
with:
Expand Down
37 changes: 37 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ WORKDIR /prepare

RUN turbo prune @blobscan/web --docker --out-dir /prepare/web
RUN turbo prune @blobscan/rest-api-server --docker --out-dir /prepare/api
RUN turbo prune @blobscan/jobs --docker --out-dir /prepare/jobs

# stage: web-builder
FROM deps AS web-builder
Expand Down Expand Up @@ -116,3 +117,39 @@ ENV PORT=3001
ADD docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["api"]

# stage: jobs-builder
FROM deps AS jobs-builder

WORKDIR /app

ARG DATABASE_URL
ARG DIRECT_URL

COPY --from=deps /prepare/jobs/json .
COPY --from=deps /prepare/jobs/pnpm-lock.yaml .

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch -r
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile

COPY --from=deps /prepare/jobs/full .

# Copy original which includes pipelines
COPY --from=deps /prepare/turbo.json .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store DATABASE_URL=${DATABASE_URL} DIRECT_URL=${DIRECT_URL} pnpm build --filter=@blobscan/jobs

# stage: jobs
FROM base AS jobs

WORKDIR /app

ENV NODE_ENV=production

COPY --from=jobs-builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=jobs-builder /app/node_modules/prisma ./node_modules/prisma
COPY --from=jobs-builder /app/node_modules/@prisma ./node_modules/@prisma
COPY --from=jobs-builder /app/apps/jobs/dist ./

ADD docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["jobs"]
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@tailwindcss/typography": "^0.5.7",
"autoprefixer": "^10.4.14",
"clsx": "^2.1.0",
"fast-glob": "^3.2.12",
"fast-glob": "^3.3.2",
"flexsearch": "^0.7.31",
"js-yaml": "^4.1.0",
"next": "^14.2.30",
Expand Down
8 changes: 4 additions & 4 deletions apps/docs/src/app/docs/background-jobs/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ nextjs:

Blobscan requires [BullMQ](https://bullmq.io/) to run the following tasks in the background:

- `DailyStatsSyncer` - calculates metrics for the blobs, block and transactions charts.
- `OverallStatsSyncer` - calculates other metrics such as Total Tx Fees Saved, Total Blocks and Total Blobs.
- `SwarmStampSyncer` - updates TTL for Ethereum Swarm batches.
- `DailyStatsCronJob` - calculates metrics for the blobs, block and transactions charts.
- `OverallStatsCronJob` - calculates other metrics such as Total Tx Fees Saved, Total Blocks and Total Blobs.
- `SwarmStampCronJob` - updates TTL for Ethereum Swarm batches.

For more information, check out the [@blobscan/syncers](https://github.com/Blobscan/blobscan/tree/main/packages/syncers/src/syncers) package.
For more information, check out the [@blobscan/jobs](https://github.com/Blobscan/blobscan/tree/main/apps/jobs) app.

The BullMQ queue is also used to upload blobs in parallel to different Storages.
2 changes: 2 additions & 0 deletions apps/docs/src/app/docs/codebase-overview/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Blobscan.com is comprised of the following major components:

- **Web App**: A [Next.js](https://nextjs.org/) application hosted on [Vercel](https://vercel.com/) that spins up a [tRPC API](https://trpc.io) that communicates with the database via [Prisma](https://www.prisma.io/). It also uses [Tailwind CSS](https://tailwindcss.com/) for styling.
- **REST API**: An express app that runs the tRPC API with [OpenAPI](https://www.openapis.org/) support. It exposes some of the tRPC API endpoints as REST endpoints for the public and external services such as the indexer.
- **Jobs**: A Node.js app running the following BullMQ background cron jobs: eth usd price syncing, stats aggregation and swarm stamp syncing.
- **Indexer**: A Rust service that listens to the Ethereum blockchain looking for blocks and transactions containing blobs and forwards them to the REST API to be indexed.

{% figure src="/architecture.svg" appendCurrentTheme=true /%}
Expand All @@ -33,6 +34,7 @@ Blobscan is composed of the following apps:
|  [`@blobscan/docs`](https://github.com/Blobscan/blobscan/tree/main/apps/docs) | Nextjs app that contains the documentation. |
|  [`@blobscan/web`](https://github.com/Blobscan/blobscan/tree/main/apps/web) | Nextjs app that contains the web app. |
|  [`@blobscan/rest-api-server`](https://github.com/Blobscan/blobscan/tree/main/apps/rest-api-server) | Express app that contains the REST API. |
|  [`@blobscan/jobs`](https://github.com/Blobscan/blobscan/tree/main/apps/jobs) | Node.js app running BullMQ cron jobs. |

### CLI

Expand Down
2 changes: 1 addition & 1 deletion packages/syncers/CHANGELOG.md → apps/jobs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @blobscan/syncers
# @blobscan/jobs

## 0.5.0

Expand Down
19 changes: 19 additions & 0 deletions apps/jobs/esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// esbuild.config.js
import * as esbuild from "esbuild";
import fs from "fs";

const result = await esbuild.build({
entryPoints: ["src/index.ts"],
outdir: "dist",
outbase: "src",
platform: "node",
target: "node20",
format: "cjs",
metafile: !!process.env.BUILD_METADATA_ENABLED,
bundle: true,
external: [".prisma", "prisma", "@prisma/client"],
});

if (process.env.BUILD_METADATA_ENABLED) {
fs.writeFileSync("dist/meta.json", JSON.stringify(result.metafile));
}
45 changes: 45 additions & 0 deletions apps/jobs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@blobscan/jobs",
"version": "0.5.0",
"private": true,
"scripts": {
"build": "node esbuild.config.mjs",
"build:metadata": "BUILD_METADATA_ENABLED=true pnpm build",
"clean": "git clean -xdf node_modules",
"dev": "pnpm with-env tsx src/index.ts",
"lint": "eslint .",
"lint:fix": "pnpm lint --fix",
"start": "pnpm with-env node dist/index.js",
"test": "pnpm with-env:test vitest",
"test:ui": "pnpm with-env:test vitest --ui",
"with-env:test": "dotenv -e ../../.env.test --",
"with-env": "dotenv -e ../../.env --",
"type-check": "tsc --noEmit"
},
"keywords": [],
"license": "MIT",
"dependencies": {
"@blobscan/dates": "workspace:^0.0.1",
"@blobscan/dayjs": "workspace:^0.1.0",
"@blobscan/db": "workspace:^0.18.0",
"@blobscan/logger": "workspace:^0.1.2",
"@blobscan/price-feed": "workspace:^0.1.0",
"@blobscan/zod": "workspace:^0.1.0",
"axios": "^1.7.2",
"bullmq": "^4.13.2",
"ioredis": "^5.3.2",
"viem": "^2.17.4"
},
"devDependencies": {
"concurrently": "^9.2.0",
"esbuild": "0.25.5",
"fast-glob": "^3.3.2",
"tsx": "^4.19.2"
},
"eslintConfig": {
"root": true,
"extends": [
"@blobscan/eslint-config/base"
]
}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import type { Processor } from "bullmq";
import { Queue, Worker } from "bullmq";
import type { Redis } from "ioredis";

import { createModuleLogger } from "@blobscan/logger";
import type { Logger } from "@blobscan/logger";

import { ErrorException, SyncerError } from "./errors";
import { createRedisConnection } from "./utils";
import { ErrorException } from "../errors";
import { createRedis } from "../redis";

export interface CommonSyncerConfig {
export interface CommonCronJobConfig {
redisUriOrConnection: Redis | string;
cronPattern: string;
}

export interface BaseSyncerConfig extends CommonSyncerConfig {
export interface BaseCronJobConfig extends CommonCronJobConfig {
name: string;
syncerFn: () => Promise<void>;
processor: Processor;
jobData?: Record<string, unknown>;
}

export class BaseSyncer {
export class CronJobError extends ErrorException {
constructor(cronJobName: string, message: string, cause?: unknown) {
super(`Cron job "${cronJobName}" failed: ${message}`, cause);
}
}

export class BaseCronJob {
name: string;
cronPattern: string;

protected syncerFn: () => Promise<void>;
protected logger: Logger;

protected connection: Redis;
protected worker: Worker | undefined;
protected queue: Queue | undefined;
protected worker?: Worker;
protected queue?: Queue;

protected jobData?: Record<string, unknown>;

constructor({
name,
cronPattern,
redisUriOrConnection,
syncerFn,
}: BaseSyncerConfig) {
this.name = `${name}-syncer`;
processor: processorFile,
jobData,
}: BaseCronJobConfig) {
this.name = `${name}-cron-job`;
this.cronPattern = cronPattern;
this.logger = createModuleLogger(this.name);
this.jobData = jobData;

let connection: Redis;

if (typeof redisUriOrConnection === "string") {
connection = createRedisConnection(redisUriOrConnection);
connection = createRedis(redisUriOrConnection);
} else {
connection = redisUriOrConnection;
}
Expand All @@ -51,7 +62,7 @@ export class BaseSyncer {
connection,
});

this.worker = new Worker(this.queue.name, syncerFn, {
this.worker = new Worker(this.queue.name, processorFile, {
connection,
});

Expand All @@ -64,25 +75,24 @@ export class BaseSyncer {
});

this.connection = connection;
this.syncerFn = syncerFn;
}

async start() {
try {
const jobName = `${this.name}-job`;
const repeatableJob = await this.queue?.add(jobName, null, {
const repeatableJob = await this.queue?.add(jobName, this.jobData, {
repeat: {
pattern: this.cronPattern,
},
});

this.logger.info("Syncer started successfully");
this.logger.debug("Cron job started successfully");

return repeatableJob;
} catch (err) {
throw new SyncerError(
throw new CronJobError(
this.name,
"An error ocurred when starting syncer",
"An error ocurred when starting cron job",
err
);
}
Expand All @@ -107,15 +117,15 @@ export class BaseSyncer {
this.queue?.removeAllListeners().close()
);

this.logger.info("Syncer closed successfully");
this.logger.info("Cron job closed successfully");
});
}

async #performClosingOperation(operation: () => Promise<void> | undefined) {
try {
await operation();
} catch (err) {
const err_ = new SyncerError(
const err_ = new CronJobError(
this.name,
"An error ocurred when performing closing operation",
err
Expand Down
Loading
Loading