├── .nvmrc
├── .githooks
└── pre-commit
├── packages
├── contracts
│ ├── remappings.txt
│ ├── foundry.lock
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── .github
│ │ └── workflows
│ │ │ └── test.yml
│ ├── foundry.toml
│ ├── README.md
│ ├── package.json
│ └── src
│ │ └── index.ts
├── sdk
│ ├── src
│ │ ├── abi
│ │ │ └── index.ts
│ │ ├── logger.ts
│ │ ├── index.ts
│ │ ├── browser.ts
│ │ ├── serialize.ts
│ │ ├── environment.ts
│ │ ├── codecs
│ │ │ └── index.ts
│ │ ├── blob-reader.ts
│ │ ├── metrics.ts
│ │ └── proxy-client.ts
│ ├── tsconfig.json
│ ├── test
│ │ ├── env.ts
│ │ ├── __unit__
│ │ │ └── blob-encoding.test.ts
│ │ └── setup.ts
│ ├── jest.config.ts
│ ├── build.js
│ ├── rollup.config.ts
│ ├── rollup.config.js
│ ├── package.json
│ └── README.md
└── proxy-server
│ ├── tsconfig.json
│ ├── src
│ ├── middleware
│ │ ├── rate-limit.ts
│ │ ├── error-handler.ts
│ │ └── validation.ts
│ ├── routes
│ │ ├── metrics.ts
│ │ └── health.ts
│ ├── config.ts
│ ├── types.ts
│ ├── services
│ │ ├── job-cache.ts
│ │ └── blob-executor.ts
│ ├── index.ts
│ ├── monitoring
│ │ └── metrics.ts
│ ├── app.ts
│ └── cli.ts
│ ├── test
│ ├── README.md
│ ├── env.ts
│ └── setup.ts
│ ├── jest.config.ts
│ ├── package.json
│ └── README.md
├── ops
└── chart
│ ├── Chart.yaml
│ ├── templates
│ ├── service-account.yaml
│ ├── _helpers.tpl
│ ├── service.yaml
│ ├── autoscale.yaml
│ ├── ingress.yaml
│ └── deployment.yaml
│ └── values.yaml
├── website
├── postcss.config.mjs
├── public
│ ├── vercel.svg
│ ├── window.svg
│ ├── file.svg
│ ├── globe.svg
│ └── next.svg
├── next.config.mjs
├── tailwind.config.ts
├── eslint.config.mjs
├── .gitignore
├── tsconfig.json
├── app
│ ├── globals.css
│ └── layout.tsx
├── package.json
└── README.md
├── ISSUES.md
├── .markdownlint.json
├── .gitmodules
├── .changeset
└── config.json
├── CHANGELOG
├── .dockerignore
├── .github
└── dependabot.yml
├── .prettierrc.json
├── test
├── setup.ts
├── package.json
├── jest.config.js
└── e2e-integration.test.ts
├── .prettierignore
├── scripts
├── tsconfig.json
├── package.json
└── check-env.mjs
├── deployments
└── mainnet.json
├── Makefile
├── .gitignore
├── tsconfig.base.json
├── .env.example
├── docker-compose.yml
├── Dockerfile
├── docs
├── distributed-tracing.md
├── getting-started.md
├── proxy
│ └── README.md
├── contracts
│ └── README.md
├── README.md
├── quick-start.md
├── secure-deployment.md
└── deployment-guide.md
├── package.json
├── eslint.config.js
├── SECURITY.md
└── CONTRIBUTING.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 |
--------------------------------------------------------------------------------
/.githooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | npm run format:check
5 | npm run lint
6 | npm run type-check
7 |
--------------------------------------------------------------------------------
/packages/contracts/remappings.txt:
--------------------------------------------------------------------------------
1 | @forge-std/=lib/forge-std/src/
2 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
--------------------------------------------------------------------------------
/packages/sdk/src/abi/index.ts:
--------------------------------------------------------------------------------
1 | import { default as EscrowContractABI } from './EscrowContract.js';
2 | export { EscrowContractABI };
3 |
--------------------------------------------------------------------------------
/ops/chart/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | appVersion: "1.0"
3 | description: Blobkit Proxy Server
4 | name: blobkit-proxy
5 | version: 1.0.0
6 |
--------------------------------------------------------------------------------
/website/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss", "autoprefixer"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/website/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ISSUES.md:
--------------------------------------------------------------------------------
1 | [ ] When retrying job submission, change the job ID if not user provided and the job has expired
2 | [x] Reading a blob still does not work (getting not supported error, but skeptical that it is the RPC, since yet to see if work)
--------------------------------------------------------------------------------
/packages/contracts/foundry.lock:
--------------------------------------------------------------------------------
1 | {
2 | "lib/forge-std": {
3 | "rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505"
4 | },
5 | "lib/openzeppelin-contracts": {
6 | "rev": "fd81a96f01cc42ef1c9a5399364968d0e07e9e90"
7 | }
8 | }
--------------------------------------------------------------------------------
/packages/contracts/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiler files
2 | cache/
3 | out/
4 |
5 | # Ignores development broadcast logs
6 | !/broadcast
7 | /broadcast/*/31337/
8 | /broadcast/**/dry-run/
9 |
10 | # Docs
11 | docs/
12 |
13 | # Dotenv file
14 | .env
15 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "default": true,
3 | "MD003": { "style": "atx" },
4 | "MD007": { "indent": 2 },
5 | "MD013": { "line_length": 100 },
6 | "MD024": { "allow_different_nesting": true },
7 | "MD033": false,
8 | "MD041": false
9 | }
10 |
--------------------------------------------------------------------------------
/website/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | trailingSlash: true,
4 | images: {
5 | unoptimized: true,
6 | },
7 | eslint: {
8 | ignoreDuringBuilds: true,
9 | },
10 | }
11 |
12 | export default nextConfig
13 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "packages/contracts/lib/forge-std"]
2 | path = packages/contracts/lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 | [submodule "packages/contracts/lib/openzeppelin-contracts"]
5 | path = packages/contracts/lib/openzeppelin-contracts
6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts
7 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/packages/contracts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "composite": true,
7 | "declarationMap": true,
8 | "types": ["node"]
9 | },
10 | "include": ["src/**/*"],
11 | "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist"]
12 | }
13 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | * Added support for EIP 7594 Blob Transactions
2 | * EIP 7918 support by using fee history RPC
3 | * EIP 4844 Blobs - Gas Estimate Correction and reduced expensive calculations
4 | * Removed superflous configuration variables
5 | * Deleted unused code
6 | * No longer required to pass in kzg configuration values
7 | * Significant reduction in execution time
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /gha-creds*
2 | /dist
3 | /node_modules
4 | /packages/*/node_modules
5 | /packages/*/dist
6 | /packages/*/tsconfig.tsbuildinfo
7 | /packages/*/.rollup.cache
8 | /dist
9 | /docs
10 | /ops
11 | /.claude
12 | /.github
13 | /.githooks
14 | /.gitignore
15 | /.dockerignore
16 | /.gitmodules
17 | /.env*
18 | /README.md
19 | /SECURITY.md
20 | /CONTRIBUTING.md
21 | /docker-compose.yml
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | directory: '/'
5 | schedule:
6 | interval: 'weekly'
7 | open-pull-requests-limit: 10
8 | reviewers:
9 | - 'ETHCF/blobkit-maintainers'
10 | assignees:
11 | - 'ETHCF/blobkit-maintainers'
12 | commit-message:
13 | prefix: 'deps'
14 | include: 'scope'
15 |
--------------------------------------------------------------------------------
/packages/sdk/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "composite": true,
7 | "declaration": true,
8 | "declarationMap": true,
9 | "declarationDir": "./dist"
10 | },
11 | "include": ["src/**/*"],
12 | "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist"]
13 | }
14 |
--------------------------------------------------------------------------------
/website/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/website/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "none",
4 | "singleQuote": true,
5 | "printWidth": 100,
6 | "tabWidth": 2,
7 | "useTabs": false,
8 | "arrowParens": "avoid",
9 | "endOfLine": "lf",
10 | "bracketSpacing": true,
11 | "bracketSameLine": false,
12 | "quoteProps": "as-needed",
13 | "proseWrap": "preserve",
14 | "htmlWhitespaceSensitivity": "css",
15 | "embeddedLanguageFormatting": "auto"
16 | }
17 |
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Global test setup - Configuration only
3 | *
4 | * This file configures Jest for the entire monorepo.
5 | * No logic should be placed here, only configuration.
6 | */
7 |
8 | import { jest } from '@jest/globals';
9 |
10 | // Set global test timeout for integration tests
11 | jest.setTimeout(120000);
12 |
13 | // Ensure tests run in UTC timezone for consistency
14 | process.env.TZ = 'UTC';
15 |
16 | // Set test environment
17 | process.env.NODE_ENV = 'test';
18 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules/
3 | packages/*/node_modules/
4 |
5 | # Build outputs
6 | dist/
7 | build/
8 | out/
9 | coverage/
10 | .next/
11 |
12 | # Contract artifacts
13 | packages/contracts/artifacts/
14 | packages/contracts/cache/
15 | packages/contracts/lib/
16 |
17 | # Generated files
18 | *.generated.*
19 | *.min.js
20 | *.min.css
21 |
22 | # Lock files
23 | package-lock.json
24 | yarn.lock
25 | pnpm-lock.yaml
26 |
27 | # Misc
28 | .git/
29 | .DS_Store
30 | *.log
31 |
--------------------------------------------------------------------------------
/packages/sdk/src/logger.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * SDK Logger - Re-exports the core logger with SDK-specific configuration
3 | */
4 |
5 | export { Logger, LogLevel, ConsoleAdapter, OpenTelemetryAdapter, logger } from './core/logger';
6 |
7 | export type { LogEntry, LoggerAdapter, LoggerConfig } from './core/logger';
8 |
9 | export {
10 | DatadogAdapter,
11 | CloudWatchAdapter,
12 | LogstashAdapter,
13 | MultiAdapter,
14 | FileAdapter,
15 | BufferAdapter
16 | } from './core/logger-adapters';
17 |
--------------------------------------------------------------------------------
/scripts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2020",
5 | "lib": ["ES2020"],
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "skipLibCheck": true,
11 | "resolveJsonModule": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "types": ["node"]
15 | },
16 | "include": ["*.ts"],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/deployments/mainnet.json:
--------------------------------------------------------------------------------
1 | {
2 | "network": "mainnet",
3 | "chainId": 1,
4 | "address": "0x2e8e414bc5c6B0b8339853CEDf965B4A28FB4838",
5 | "deployer": "0x9CFe9dc15b6cA16147dF1b93E487bAaDd422F693",
6 | "blockNumber": 23090830,
7 | "transactionHash": "0x22554bb185c8f59dfc8d2341e31dffedbc50db7006435b068e5543ed5fa022eb",
8 | "timestamp": "2025-08-07T17:59:49.348Z",
9 | "config": {
10 | "jobTimeout": 300,
11 | "proxies": [],
12 | "proxyFeePercent": 2
13 | },
14 | "gasUsed": "1138226",
15 | "deploymentCost": "0.000596540147848174 ETH"
16 | }
17 |
--------------------------------------------------------------------------------
/packages/proxy-server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "composite": true,
7 | "declarationMap": true,
8 | "types": ["node", "jest"],
9 | "skipLibCheck": true,
10 | "moduleResolution": "node",
11 | "paths": {
12 | "@blobkit/sdk": ["../sdk/dist/index.d.ts"],
13 | "@blobkit/sdk/*": ["../sdk/dist/*"]
14 | }
15 | },
16 | "include": ["src/**/*"],
17 | "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "../sdk/src/**/*"]
18 | }
19 |
--------------------------------------------------------------------------------
/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blobkit-scripts",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "BlobKit demo and utility scripts",
6 | "type": "module",
7 | "scripts": {
8 | "demo": "echo 'demo scripts removed' && exit 0",
9 | "integration": "echo 'integration demo removed' && exit 0"
10 | },
11 | "dependencies": {
12 | "@blobkit/sdk": "file:../packages/sdk",
13 | "ethers": "^6.7.1"
14 | },
15 | "devDependencies": {
16 | "@types/node": "^24.2.0",
17 | "tsx": "^4.20.3",
18 | "typescript": "^5.3.3"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: dev test lint lint-fix type-check build format format-check ci hooks
2 |
3 | dev:
4 | bash scripts/dev.sh
5 |
6 | test:
7 | npm run test --workspaces
8 |
9 | lint:
10 | npm run lint --workspaces
11 |
12 | lint-fix:
13 | npm run lint:fix --workspaces
14 |
15 | type-check:
16 | npm run type-check --workspaces
17 |
18 | build:
19 | npm run build --workspaces
20 |
21 | format:
22 | npm run format
23 |
24 | format-check:
25 | npm run format:check
26 |
27 | ci:
28 | npm ci && npm run type-check && npm run lint && npm run build && npm test
29 |
30 | hooks:
31 | git config core.hooksPath .githooks
32 |
--------------------------------------------------------------------------------
/website/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: "var(--background)",
13 | foreground: "var(--foreground)",
14 | },
15 | fontFamily: {
16 | mono: ['ui-monospace', 'SFMono-Regular', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
17 | },
18 | },
19 | },
20 | plugins: [],
21 | } satisfies Config;
22 |
--------------------------------------------------------------------------------
/website/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | {
15 | ignores: [
16 | "node_modules/**",
17 | ".next/**",
18 | "out/**",
19 | "build/**",
20 | "next-env.d.ts",
21 | ],
22 | },
23 | ];
24 |
25 | export default eslintConfig;
26 |
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/scripts/check-env.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import fs from 'fs';
3 |
4 | const REQUIRED = {
5 | root: [],
6 | sdk: ['BLOBKIT_RPC_URL', 'BLOBKIT_CHAIN_ID'],
7 | proxy: ['RPC_URL', 'CHAIN_ID', 'ESCROW_CONTRACT', 'REQUEST_SIGNING_SECRET']
8 | };
9 |
10 | const pkg = process.argv[2] || 'root';
11 | const vars = REQUIRED[pkg] || [];
12 | const missing = vars.filter(name => !process.env[name] || String(process.env[name]).trim() === '');
13 |
14 | if (missing.length) {
15 | console.warn(`[check-env] Missing env vars for ${pkg}: ${missing.join(', ')}`);
16 | process.exitCode = 0; // warn only
17 | } else {
18 | console.log(`[check-env] All required env vars present for ${pkg}`);
19 | }
20 |
--------------------------------------------------------------------------------
/website/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/sdk/test/env.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Test environment setup - sets up environment variables for testing
3 | */
4 |
5 | import * as dotenv from 'dotenv';
6 | import * as path from 'path';
7 |
8 | // Load environment variables from .env file
9 | dotenv.config({
10 | path: path.resolve(__dirname, '../.env')
11 | });
12 |
13 | // Set test defaults if not already defined
14 | process.env.NODE_ENV = 'test';
15 | process.env.BLOBKIT_RPC_URL = process.env.BLOBKIT_RPC_URL || 'http://localhost:8545';
16 | process.env.BLOBKIT_CHAIN_ID = process.env.BLOBKIT_CHAIN_ID || '31337';
17 | process.env.BLOBKIT_ESCROW_31337 = process.env.BLOBKIT_ESCROW_31337 || '0x1234567890123456789012345678901234567890';
18 | process.env.BLOBKIT_LOG_LEVEL = process.env.BLOBKIT_LOG_LEVEL || 'silent';
19 |
--------------------------------------------------------------------------------
/test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blobkit-tests",
3 | "version": "1.0.0",
4 | "private": true,
5 | "type": "module",
6 | "description": "Integration tests for BlobKit",
7 | "scripts": {
8 | "test": "jest",
9 | "test:watch": "jest --watch",
10 | "test:coverage": "jest --coverage"
11 | },
12 | "dependencies": {
13 | "@blobkit/sdk": "file:../packages/sdk",
14 | "@blobkit/proxy-server": "file:../packages/proxy-server",
15 | "@blobkit/contracts": "file:../packages/contracts",
16 | "ethers": "^6.15.0"
17 | },
18 | "devDependencies": {
19 | "@types/jest": "^30.0.0",
20 | "@types/node": "^24.2.1",
21 | "jest": "^30.0.5",
22 | "ts-jest": "^29.4.1",
23 | "typescript": "^5.3.3",
24 | "ts-node": "^10.9.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/website/app/globals.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 |
3 | .terminal-box {
4 | background-color: rgba(0, 0, 0, 0.8);
5 | border: 1px solid rgba(34, 197, 94, 0.3);
6 | padding: 1.5rem;
7 | position: relative;
8 | overflow: hidden;
9 | box-shadow: 0 0 20px rgba(0, 255, 0, 0.1);
10 | }
11 |
12 | .cyber-grid {
13 | background-image:
14 | linear-gradient(rgba(0, 255, 0, 0.03) 1px, transparent 1px),
15 | linear-gradient(90deg, rgba(0, 255, 0, 0.03) 1px, transparent 1px);
16 | background-size: 50px 50px;
17 | }
18 |
19 | .glow-text {
20 | text-shadow: 0 0 10px currentColor;
21 | }
22 |
23 | .text-magenta-400 {
24 | color: #f0f;
25 | }
26 |
27 | .text-magenta-300 {
28 | color: #f6f;
29 | }
30 |
31 | .hover\:text-magenta-300:hover {
32 | color: #f6f;
33 | }
--------------------------------------------------------------------------------
/packages/sdk/test/__unit__/blob-encoding.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | encodeBlob,
3 | decodeBlob
4 | } from '../../src/kzg';
5 |
6 | import {
7 | generateTestBlobData
8 | } from '../../../../test/utils';
9 |
10 |
11 | describe('Blob Encoding tests', () => {
12 | test('decoding encoded data should generate the same as original', async () => {
13 | // Write a blob first
14 | const originalData = generateTestBlobData('random', 1000);
15 |
16 | // Decode and verify data matches
17 | // Note: The data will be encoded in blob format, so we need to decode it
18 | const encodedBlob = encodeBlob(originalData);
19 | const decodedBlob = decodeBlob(encodedBlob);
20 | expect(decodedBlob.length).toEqual(originalData.length);
21 | expect(decodedBlob).toEqual(originalData);
22 | });
23 | })
--------------------------------------------------------------------------------
/test/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('jest').Config} */
2 | export default {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | extensionsToTreatAsEsm: [],
6 | moduleNameMapper: {
7 | '^(\\.{1,2}/.*)\\.js$': '$1',
8 | '^@blobkit/sdk$': '/../packages/sdk/src/index.ts'
9 | },
10 | transform: {
11 | '^.+\\.tsx?$': [
12 | 'ts-jest',
13 | {
14 | useESM: false,
15 | tsconfig: {
16 | module: 'commonjs',
17 | target: 'es2022',
18 | lib: ['es2022'],
19 | moduleResolution: 'node',
20 | allowSyntheticDefaultImports: true,
21 | esModuleInterop: true
22 | }
23 | }
24 | ]
25 | },
26 | testMatch: ['**/*.test.ts'],
27 | testTimeout: 120000, // 2 minutes for integration tests
28 | setupFilesAfterEnv: ['/setup.ts']
29 | };
30 |
--------------------------------------------------------------------------------
/packages/contracts/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | env:
9 | FOUNDRY_PROFILE: ci
10 |
11 | jobs:
12 | check:
13 | name: Foundry project
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | submodules: recursive
19 |
20 | - name: Install Foundry
21 | uses: foundry-rs/foundry-toolchain@v1
22 |
23 | - name: Show Forge version
24 | run: |
25 | forge --version
26 |
27 | - name: Run Forge fmt
28 | run: |
29 | forge fmt --check
30 | id: fmt
31 |
32 | - name: Run Forge build
33 | run: |
34 | forge build --sizes
35 | id: build
36 |
37 | - name: Run Forge tests
38 | run: |
39 | forge test -vvv
40 | id: test
41 |
--------------------------------------------------------------------------------
/ops/chart/templates/service-account.yaml:
--------------------------------------------------------------------------------
1 | {{- $name := include "name" . -}}
2 | {{- $chart := include "chart" . -}}
3 |
4 | {{- range $service, $val := $.Values.services }}
5 | {{- if and $val.serviceAccountName (not $val.disabled) }}
6 | apiVersion: v1
7 | kind: ServiceAccount
8 | metadata:
9 | name: {{ $val.serviceAccountName }}
10 | namespace: {{ $.Values.namespace }}
11 | annotations:
12 | iam.gke.io/gcp-service-account: "{{$val.serviceAccountName }}@{{ $.Values.projectId}}.iam.gserviceaccount.com"
13 | labels:
14 | app.kubernetes.io/instance: {{ $.Release.Name }}
15 | app.kubernetes.io/managed-by: {{ $.Release.Service }}
16 | app.kubernetes.io/version: {{ $.Values.version | quote }}
17 | app.kubernetes.io/component: {{ $service }}
18 | helm.sh/chart: {{ $chart }}
19 | {{- if .labels}}
20 | {{ toYaml .labels | nindent 4 }}
21 | {{- end }}
22 | ---
23 | {{ end }}
24 | {{- end }}
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules/
3 | .pnp
4 | .pnp.js
5 |
6 | # Build outputs
7 | dist/
8 | build/
9 | *.tsbuildinfo
10 | .rollup.cache/
11 |
12 | # Testing
13 | coverage/
14 | .nyc_output/
15 |
16 | # IDE
17 | .vscode/
18 | .idea/
19 | .claude/
20 | *.swp
21 | *.swo
22 | *.swn
23 | *.code-workspace
24 |
25 | # OS
26 | .DS_Store
27 | Thumbs.db
28 |
29 | # Logs
30 | logs/
31 | *.log
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 | lerna-debug.log*
36 |
37 | # Environment variables
38 | .env
39 | .env.local
40 | .env.development.local
41 | .env.test.local
42 | .env.production.local
43 |
44 | # Misc
45 | .cache/
46 | .temp/
47 | .tmp/
48 | *.bak
49 | *.origtest/**/*benchmark*.test.ts
50 |
51 | # Production artifacts
52 | *.tgz
53 | *.production-status
54 | production-check-results.json
55 | production-check.log
56 | *SUMMARY*.md
57 | *REMEDIATION*.md
58 | *REPORT*.md
59 |
60 | /packages/contracts/deployments/
61 | /test/shrek*
--------------------------------------------------------------------------------
/packages/proxy-server/src/middleware/rate-limit.ts:
--------------------------------------------------------------------------------
1 | import rateLimit from 'express-rate-limit';
2 | import type { Request, Response } from 'express';
3 | import { ProxyErrorCode } from '../types.js';
4 |
5 | /**
6 | * Creates rate limiting middleware
7 | */
8 | export const createRateLimit = (requests: number, windowMs: number) => {
9 | return rateLimit({
10 | windowMs: windowMs * 1000, // Convert to milliseconds
11 | max: requests,
12 | message: {
13 | error: ProxyErrorCode.RATE_LIMIT_EXCEEDED,
14 | message: `Too many requests, limit is ${requests} requests per ${windowMs} seconds`
15 | },
16 | standardHeaders: true,
17 | legacyHeaders: false,
18 | handler: (req: Request, res: Response) => {
19 | res.status(429).json({
20 | error: ProxyErrorCode.RATE_LIMIT_EXCEEDED,
21 | message: `Too many requests, limit is ${requests} requests per ${windowMs} seconds`
22 | });
23 | }
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/website/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/proxy-server/test/README.md:
--------------------------------------------------------------------------------
1 | # Proxy Server Test Suite
2 |
3 | This directory contains minimal tests for the BlobKit proxy server. The proxy server functionality is primarily tested through integration tests at the system level.
4 |
5 | ## Test Structure
6 |
7 | The proxy server tests have been minimized to focus on integration testing at the repository root level, as unit tests with fully mocked services provide limited value for a proxy service.
8 |
9 | ## Testing Strategy
10 |
11 | The proxy server is tested through:
12 |
13 | 1. Full system integration tests that exercise the complete flow
14 | 2. Contract tests that verify escrow payment and job completion
15 | 3. Manual testing during deployment
16 |
17 | ## Future Testing
18 |
19 | When infrastructure dependencies are available, tests should cover:
20 |
21 | - Redis job queue persistence and recovery
22 | - Circuit breaker behavior under load
23 | - Rate limiting enforcement
24 | - Signature verification with real contracts
25 | - Blob transaction submission to test networks
26 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blobkit-website",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "Official documentation website for BlobKit SDK",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint",
11 | "export": "next build && next export"
12 | },
13 | "dependencies": {
14 | "@types/react-syntax-highlighter": "^15.5.13",
15 | "autoprefixer": "^10.4.21",
16 | "lucide-react": "^0.544.0",
17 | "next": "15.5.3",
18 | "nextra": "^4.4.0",
19 | "nextra-theme-docs": "^4.4.0",
20 | "react": "19.1.0",
21 | "react-dom": "19.1.0",
22 | "react-syntax-highlighter": "^15.6.6"
23 | },
24 | "devDependencies": {
25 | "@eslint/eslintrc": "^3",
26 | "@tailwindcss/postcss": "^4.1.13",
27 | "@types/node": "^20",
28 | "@types/react": "^19",
29 | "@types/react-dom": "^19",
30 | "eslint": "^9",
31 | "eslint-config-next": "15.5.3",
32 | "tailwindcss": "^4",
33 | "typescript": "^5"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/sdk/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | const config: Config = {
4 | preset: 'ts-jest',
5 | testEnvironment: 'node',
6 | roots: ['/src', '/test'],
7 | testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
8 | transform: {
9 | '^.+\\.ts$': 'ts-jest'
10 | },
11 | collectCoverageFrom: [
12 | 'src/**/*.ts',
13 | '!src/**/*.d.ts',
14 | '!src/index.ts' // Main export file
15 | ],
16 | coverageDirectory: 'coverage',
17 | coverageReporters: ['text', 'lcov', 'html'],
18 | testTimeout: 30000,
19 | setupFilesAfterEnv: ['/test/setup.ts'],
20 | moduleNameMapper: {
21 | '^@/(.*)$': '/src/$1',
22 | '^(\\.{1,2}/.*)\\.js$': '$1'
23 | },
24 | extensionsToTreatAsEsm: ['.ts'],
25 | // Handle WASM imports
26 | moduleFileExtensions: ['ts', 'js', 'json'],
27 | testPathIgnorePatterns: ['/node_modules/', '/dist/'],
28 | verbose: true,
29 | // Environment variables for tests
30 | setupFiles: ['/test/env.ts']
31 | };
32 |
33 | export default config;
34 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/routes/metrics.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Metrics endpoint for Prometheus scraping
3 | */
4 |
5 | import { Router, Request, Response } from 'express';
6 | import { getPrometheusMetrics } from '../monitoring/prometheus-metrics.js';
7 | import { createLogger } from '../utils/logger.js';
8 |
9 | const logger = createLogger('MetricsRoute');
10 |
11 | export function createMetricsRouter(): Router {
12 | const router = Router();
13 | const metrics = getPrometheusMetrics();
14 |
15 | /**
16 | * GET /metrics
17 | * Prometheus metrics endpoint
18 | */
19 | router.get('/', async (req: Request, res: Response) => {
20 | try {
21 | const metricsData = await metrics.getMetrics();
22 | res.set('Content-Type', metrics.getContentType());
23 | res.end(metricsData);
24 | } catch (error) {
25 | logger.error('Failed to generate metrics', {
26 | error: error instanceof Error ? error.message : String(error)
27 | });
28 | res.status(500).json({ error: 'Failed to generate metrics' });
29 | }
30 | });
31 |
32 | return router;
33 | }
34 |
--------------------------------------------------------------------------------
/ops/chart/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/* vim: set filetype=mustache: */}}
2 | {{/*
3 | Expand the name of the chart.
4 | */}}
5 | {{- define "name" -}}
6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
7 | {{- end -}}
8 |
9 | {{/*
10 | Create a default fully qualified app name.
11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
12 | If release name contains chart name it will be used as a full name.
13 | */}}
14 | {{- define "fullname" -}}
15 | {{- if .Values.fullnameOverride -}}
16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
17 | {{- else -}}
18 | {{- $name := default .Chart.Name .Values.nameOverride -}}
19 | {{- if contains $name .Release.Name -}}
20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}}
21 | {{- else -}}
22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
23 | {{- end -}}
24 | {{- end -}}
25 | {{- end -}}
26 |
27 | {{/*
28 | Create chart name and version as used by the chart label.
29 | */}}
30 | {{- define "chart" -}}
31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
32 | {{- end -}}
33 |
--------------------------------------------------------------------------------
/packages/proxy-server/test/env.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Test environment setup
3 | *
4 | * Configures the test environment with necessary variables and mocks
5 | */
6 |
7 | // Set test environment variables
8 | process.env.NODE_ENV = 'test';
9 | process.env.LOG_LEVEL = 'silent';
10 | process.env.PROXY_FEE_PERCENT = '0';
11 | process.env.MAX_BLOB_SIZE = '131072';
12 | process.env.JOB_TIMEOUT = '300';
13 |
14 | // Mock crypto if not available (for tests that don't need real crypto)
15 | if (typeof globalThis.crypto === 'undefined') {
16 | const crypto = require('crypto');
17 | globalThis.crypto = {
18 | getRandomValues: (array: Uint8Array) => {
19 | const bytes = crypto.randomBytes(array.length);
20 | array.set(bytes);
21 | return array;
22 | },
23 | randomUUID: () => crypto.randomUUID(),
24 | subtle: {} as any
25 | } as any;
26 | }
27 |
28 | // Suppress console output during tests unless DEBUG is set
29 | if (!process.env.DEBUG) {
30 | global.console.log = jest.fn();
31 | global.console.info = jest.fn();
32 | global.console.warn = jest.fn();
33 | global.console.debug = jest.fn();
34 | }
35 |
36 | // Export to make TypeScript happy
37 | export {};
38 |
--------------------------------------------------------------------------------
/packages/proxy-server/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | const config: Config = {
4 | preset: 'ts-jest',
5 | testEnvironment: 'node',
6 | roots: ['/src', '/test'],
7 | testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
8 | transform: {
9 | '^.+\\.ts$': [
10 | 'ts-jest',
11 | {
12 | tsconfig: {
13 | module: 'commonjs',
14 | esModuleInterop: true,
15 | allowSyntheticDefaultImports: true
16 | }
17 | }
18 | ]
19 | },
20 | collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/index.ts', '!src/cli.ts'],
21 | coverageDirectory: 'coverage',
22 | coverageReporters: ['text', 'lcov', 'html'],
23 | testTimeout: 30000,
24 | moduleNameMapper: {
25 | '^@/(.*)$': '/src/$1',
26 | '^(\\.{1,2}/.*)\\.js$': '$1',
27 | '^@blobkit/sdk$': '/../sdk/dist/index.cjs'
28 | },
29 | moduleDirectories: ['node_modules', '/../sdk/node_modules'],
30 | testPathIgnorePatterns: ['/node_modules/', '/dist/'],
31 | verbose: true,
32 | setupFiles: ['/test/env.ts'],
33 | passWithNoTests: false
34 | };
35 |
36 | export default config;
37 |
--------------------------------------------------------------------------------
/ops/chart/templates/service.yaml:
--------------------------------------------------------------------------------
1 | {{- $chart := include "chart" . }}
2 | {{- $name := include "name" . -}}
3 |
4 | {{- range $service, $val := .Values.services }}
5 | {{- $serviceName := printf "%s-%s" $name $service -}}
6 | {{- if and .paths (not .disabled) }}
7 | apiVersion: v1
8 | kind: Service
9 | metadata:
10 | name: {{ $serviceName }}
11 | namespace: {{ $.Values.namespace }}
12 | labels:
13 | app.kubernetes.io/name: {{ $serviceName }}
14 | helm.sh/chart: {{ $chart }}
15 | app.kubernetes.io/instance: {{ $.Release.Name }}
16 | app.kubernetes.io/managed-by: {{ $.Release.Service }}
17 | app.kubernetes.io/version: {{ $.Values.version | quote }}
18 | app.kubernetes.io/component: {{ $service }}
19 | {{- if .labels}}
20 | {{ toYaml .labels | nindent 4 }}
21 | {{- end }}
22 | spec:
23 | type: {{ $.Values.service.type }}
24 | ports:
25 | - port: {{ $.Values.service.port }}
26 | targetPort: {{ $.Values.service.targetPort }}
27 | protocol: {{ $.Values.service.protocol }}
28 | name: http
29 | selector:
30 | app.kubernetes.io/name: {{ $serviceName }}
31 | app.kubernetes.io/instance: {{ $.Release.Name }}
32 | ---
33 | {{- end }}
34 | {{- end }}
35 |
--------------------------------------------------------------------------------
/test/e2e-integration.test.ts:
--------------------------------------------------------------------------------
1 | // Minimal integration smoke test (skips fast if Anvil is not running)
2 | import { JsonRpcProvider } from 'ethers';
3 |
4 | async function isRpcReachable(url: string, timeoutMs: number = 2000): Promise {
5 | const controller = new AbortController();
6 | const timeout = setTimeout(() => controller.abort(), timeoutMs);
7 | try {
8 | // Any response means the port is listening; method doesn't matter
9 | await fetch(url, { method: 'GET', signal: controller.signal });
10 | return true;
11 | } catch {
12 | return false;
13 | } finally {
14 | clearTimeout(timeout);
15 | }
16 | }
17 |
18 | describe('Integration smoke test', () => {
19 | const rpcUrl = process.env.BLOBKIT_RPC_URL || 'http://localhost:8545';
20 |
21 | it('connects to Anvil and gets a block number', async () => {
22 | if (!(await isRpcReachable(rpcUrl))) {
23 | // Skip quickly when Anvil is not running locally
24 | return;
25 | }
26 |
27 | const provider = new JsonRpcProvider(rpcUrl);
28 | const blockNumber = await provider.getBlockNumber();
29 | expect(typeof blockNumber).toBe('number');
30 | expect(blockNumber).toBeGreaterThanOrEqual(0);
31 | });
32 | });
33 |
34 |
--------------------------------------------------------------------------------
/packages/contracts/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = "src"
3 | out = "out"
4 | libs = ["lib"]
5 | test = "test"
6 | cache_path = "cache"
7 | fs_permissions = [
8 | { access = "read", path = "./" },
9 | { access = "write", path = "./" }
10 | ]
11 |
12 | # Compiler settings
13 | solc_version = "0.8.20"
14 | optimizer = true
15 | optimizer_runs = 200
16 | via_ir = false
17 |
18 | # Test settings
19 | verbosity = 2
20 | gas_reports = ["*"]
21 | auto_detect_solc = false
22 |
23 | # Formatter settings
24 | line_length = 100
25 | tab_width = 4
26 | bracket_spacing = true
27 | int_types = "long"
28 |
29 | # Documentation
30 | doc_title = "BlobKit Contracts"
31 | doc_description = "Smart contracts for BlobKit escrow and payment system"
32 |
33 | [profile.ci]
34 | fuzz = { runs = 10000 }
35 | invariant = { runs = 1000 }
36 |
37 | [profile.lite]
38 | fuzz = { runs = 50 }
39 | invariant = { runs = 10 }
40 |
41 | # Dependencies
42 | [dependencies]
43 | forge-std = "github.com/foundry-rs/forge-std"
44 | openzeppelin-contracts = "github.com/OpenZeppelin/openzeppelin-contracts@v4.9.3"
45 |
46 | # Remappings
47 | remappings = [
48 | "@forge-std/=lib/forge-std/src/",
49 | "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
50 | ]
51 |
52 |
--------------------------------------------------------------------------------
/ops/chart/templates/autoscale.yaml:
--------------------------------------------------------------------------------
1 | {{- $name := include "name" . -}}
2 | {{- $chart := include "chart" . -}}
3 |
4 | {{- range $service, $val := $.Values.services }}
5 | {{- if and (not .disabled) (or .maxReplicas .targetCPUUtil) }}
6 | {{- $serviceName := printf "%s-%s" $name $service -}}
7 |
8 | apiVersion: autoscaling/v2
9 | kind: HorizontalPodAutoscaler
10 | metadata:
11 | name: {{ $serviceName }}
12 | namespace: {{ $.Values.namespace }}
13 | labels:
14 | app.kubernetes.io/name: {{ $serviceName }}
15 | app.kubernetes.io/instance: {{ $.Release.Name }}
16 | app.kubernetes.io/managed-by: {{ $.Release.Service }}
17 | app.kubernetes.io/version: {{ $.Values.version | quote }}
18 | app.kubernetes.io/component: {{ $service }}
19 | helm.sh/chart: {{ $chart }}
20 | {{- if .labels}}
21 | {{ toYaml .labels | nindent 4 }}
22 | {{- end }}
23 | spec:
24 | minReplicas: {{ default .replicas .minReplicas }}
25 | maxReplicas: {{ default 10 .maxReplicas }}
26 |
27 | scaleTargetRef:
28 | apiVersion: apps/v1
29 | kind: Deployment
30 | name: {{ $serviceName }}
31 | metrics:
32 | - type: Resource
33 | resource:
34 | name: cpu
35 | target:
36 | type: Utilization
37 | averageUtilization: {{ default 70 .targetCPUUtil }}
38 |
39 | ---
40 | {{- end }}
41 | {{- end }}
--------------------------------------------------------------------------------
/website/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | # BlobKit Documentation Website
2 |
3 | Official documentation website for BlobKit SDK, built with Next.js and Nextra.
4 |
5 | ## Development
6 |
7 | ```bash
8 | # Install dependencies
9 | npm install
10 |
11 | # Start development server
12 | npm run dev
13 |
14 | # Build for production
15 | npm run build
16 |
17 | # Start production server
18 | npm start
19 | ```
20 |
21 | ## Deployment
22 |
23 | This website is deployed on Vercel and automatically builds from the `website` branch.
24 |
25 | - **Production URL**: https://blobkit.org
26 | - **Preview URL**: Auto-generated for pull requests
27 |
28 | ## Technology Stack
29 |
30 | - **Next.js 15** - React framework
31 | - **Nextra** - Documentation site generator
32 | - **TypeScript** - Type safety
33 | - **Tailwind CSS** - Styling
34 | - **Vercel** - Hosting and deployment
35 |
36 | ## Content
37 |
38 | The documentation covers:
39 |
40 | - Quick start guide
41 | - Installation instructions
42 | - Complete API reference
43 | - Real-world use cases
44 | - Advanced configuration
45 | - Contributing guidelines
46 |
47 | ## Contributing
48 |
49 | To update documentation:
50 |
51 | 1. Edit the `.mdx` files in `src/pages/`
52 | 2. Test locally with `npm run dev`
53 | 3. Submit a pull request
54 |
55 | The site will automatically rebuild and deploy when changes are merged to the `website` branch.
--------------------------------------------------------------------------------
/website/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next'
2 | import './globals.css'
3 |
4 | export const metadata: Metadata = {
5 | title: 'BlobKit - TypeScript SDK for Ethereum blob transactions',
6 | description: 'TypeScript SDK for working with Ethereum blob transactions (EIP-4844). Blob space is useful for temporary data storage with cryptographic guarantees.',
7 | authors: [{ name: 'BlobKit Contributors' }],
8 | keywords: ['ethereum', 'blob', 'eip-4844', 'typescript', 'sdk', 'blockchain'],
9 | creator: 'BlobKit',
10 | publisher: 'BlobKit',
11 | robots: 'index, follow',
12 | openGraph: {
13 | title: 'BlobKit - TypeScript SDK for Ethereum blob transactions',
14 | description: 'TypeScript SDK for working with Ethereum blob transactions (EIP-4844)',
15 | url: 'https://blobkit.org/',
16 | siteName: 'BlobKit',
17 | locale: 'en_US',
18 | type: 'website',
19 | },
20 | twitter: {
21 | card: 'summary_large_image',
22 | title: 'BlobKit - TypeScript SDK for Ethereum blob transactions',
23 | description: 'TypeScript SDK for working with Ethereum blob transactions (EIP-4844)',
24 | },
25 | }
26 |
27 | export default function RootLayout({
28 | children,
29 | }: {
30 | children: React.ReactNode
31 | }) {
32 | return (
33 |
34 | {children}
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/packages/sdk/build.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { build } from 'esbuild';
3 | import { readFile } from 'fs/promises';
4 |
5 | const pkg = JSON.parse(await readFile('./package.json', 'utf8'));
6 | const external = Object.keys(pkg.dependencies || {});
7 | const externalNodeBuiltins = ['crypto', 'fs'];
8 |
9 | // Main build (ESM)
10 | await build({
11 | entryPoints: ['src/index.ts'],
12 | bundle: true,
13 | format: 'esm',
14 | platform: 'node',
15 | target: 'es2020',
16 | outfile: 'dist/index.js',
17 | external,
18 | sourcemap: true
19 | });
20 |
21 | // Main build (CJS)
22 | await build({
23 | entryPoints: ['src/index.ts'],
24 | bundle: true,
25 | format: 'cjs',
26 | platform: 'node',
27 | target: 'es2020',
28 | outfile: 'dist/index.cjs',
29 | external,
30 | sourcemap: true
31 | });
32 |
33 | // Browser build (ESM)
34 | await build({
35 | entryPoints: ['src/browser.ts'],
36 | bundle: true,
37 | format: 'esm',
38 | platform: 'browser',
39 | target: 'es2020',
40 | outfile: 'dist/browser.js',
41 | external: [...external, ...externalNodeBuiltins],
42 | sourcemap: true
43 | });
44 |
45 | // Browser build (CJS)
46 | await build({
47 | entryPoints: ['src/browser.ts'],
48 | bundle: true,
49 | format: 'cjs',
50 | platform: 'browser',
51 | target: 'es2020',
52 | outfile: 'dist/browser.cjs',
53 | external: [...external, ...externalNodeBuiltins],
54 | sourcemap: true
55 | });
56 |
57 | console.log('Build complete!');
58 |
--------------------------------------------------------------------------------
/ops/chart/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- $chart := include "chart" . }}
2 | {{- $name := include "name" . -}}
3 |
4 | {{- range $service, $val := $.Values.services }}
5 | {{- if and .paths (not .disabled) }}
6 | {{- $serviceName := printf "%s-%s" $name $service -}}
7 | apiVersion: networking.k8s.io/v1
8 | kind: Ingress
9 | metadata:
10 | name: {{ $serviceName }}
11 | namespace: {{ $.Values.namespace }}
12 | labels:
13 | app.kubernetes.io/name: {{ $serviceName }}
14 | app.kubernetes.io/instance: {{ $.Release.Name }}
15 | app.kubernetes.io/managed-by: {{ $.Release.Service }}
16 | app.kubernetes.io/version: {{ $.Values.version | quote }}
17 | app.kubernetes.io/component: {{ $service }}
18 | helm.sh/chart: {{ $chart }}
19 | {{- if .labels}}
20 | {{ toYaml .labels | nindent 4 }}
21 | {{- end }}
22 | {{- if .ingress.annotations }}
23 | {{- with .ingress.annotations }}
24 | annotations:
25 | {{ toYaml . | nindent 4 }}
26 | {{- end }}
27 | {{- end }}
28 | spec:
29 | tls:
30 | - hosts:
31 | - {{ .ingress.domain | quote }}
32 | secretName: {{ .ingress.tlsSecret }}
33 |
34 | rules:
35 | - host: {{ .ingress.domain | quote }}
36 | http:
37 | paths:
38 | {{- range .paths }}
39 | - path: {{ . }}
40 | pathType: Prefix
41 | backend:
42 | service:
43 | name: {{ $serviceName }}
44 | port:
45 | number: {{ $.Values.service.port }}
46 | {{- end }}
47 | ---
48 | {{- end }}
49 | {{- end }}
50 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowImportingTsExtensions": false,
8 | "resolveJsonModule": true,
9 | "isolatedModules": true,
10 | "noEmit": false,
11 |
12 | // Strict type checking (balanced for production)
13 | "strict": true,
14 | "noImplicitAny": true,
15 | "strictNullChecks": true,
16 | "strictFunctionTypes": true,
17 | "strictBindCallApply": true,
18 | "strictPropertyInitialization": true,
19 | "noImplicitReturns": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedIndexedAccess": false,
22 | "exactOptionalPropertyTypes": false,
23 |
24 | // Additional checks
25 | "noImplicitOverride": true,
26 | "noPropertyAccessFromIndexSignature": false,
27 | "noImplicitThis": true,
28 | "useUnknownInCatchVariables": true,
29 |
30 | // Module system
31 | "declaration": true,
32 | "declarationMap": true,
33 | "sourceMap": true,
34 | "outDir": "./dist",
35 | "removeComments": false,
36 | "importHelpers": true,
37 | "skipLibCheck": true,
38 | "allowSyntheticDefaultImports": true,
39 | "esModuleInterop": true,
40 | "forceConsistentCasingInFileNames": true,
41 |
42 | // Path mapping
43 | "baseUrl": ".",
44 | "paths": {
45 | "@blobkit/sdk": ["./packages/sdk/src"],
46 | "@blobkit/contracts": ["./packages/contracts/src"],
47 | "@blobkit/proxy-server": ["./packages/proxy-server/src"]
48 | }
49 | },
50 | "exclude": ["node_modules", "dist", "lib", "**/*.test.ts", "**/*.spec.ts"]
51 | }
52 |
--------------------------------------------------------------------------------
/packages/contracts/README.md:
--------------------------------------------------------------------------------
1 | # @blobkit/contracts
2 |
3 | Solidity smart contracts for trustless escrow payments in the BlobKit system.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install @blobkit/contracts
9 | ```
10 |
11 | ## Usage
12 |
13 | ```typescript
14 | import { ethers } from 'ethers';
15 | import { BlobKitEscrowABI, getContractAddress } from '@blobkit/contracts';
16 |
17 | const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_PROJECT_ID');
18 | const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
19 |
20 | const escrowAddress = getContractAddress(1);
21 | const escrow = new ethers.Contract(escrowAddress, BlobKitEscrowABI, signer);
22 |
23 | // Deposit for blob job
24 | const jobId = ethers.keccak256(ethers.toUtf8Bytes('unique-job-id'));
25 | const tx = await escrow.depositForBlob(jobId, { value: ethers.parseEther('0.01') });
26 | await tx.wait();
27 |
28 | // Check job status
29 | const job = await escrow.getJob(jobId);
30 | console.log('Job completed:', job.completed);
31 | ```
32 |
33 | ## Configuration
34 |
35 | ```bash
36 | # Environment variables for deployed contracts
37 | BLOBKIT_ESCROW_1=0x... # Mainnet
38 | BLOBKIT_ESCROW_11155111=0x... # Sepolia
39 | BLOBKIT_ESCROW_17000=0x... # Holesky
40 | ```
41 |
42 | ## Testing and Development
43 |
44 | ```bash
45 | forge build # Build contracts
46 | forge test # Run tests
47 | forge test --coverage # Run with coverage
48 | forge fmt # Format code
49 | npm run lint # Check formatting
50 | npm run type-check # Type check TypeScript
51 | ```
52 |
53 | ## Documentation
54 |
55 | See [/docs/contracts/](../../docs/contracts/) for contract documentation.
56 |
--------------------------------------------------------------------------------
/packages/contracts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@blobkit/contracts",
3 | "version": "1.0.0",
4 | "description": "Smart contracts for BlobKit escrow and payment system",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "types": "dist/index.d.ts",
8 | "files": [
9 | "dist",
10 | "artifacts",
11 | "src",
12 | "README.md",
13 | "LICENSE"
14 | ],
15 | "scripts": {
16 | "build": "forge build && tsc",
17 | "test": "forge test",
18 | "test:coverage": "forge coverage",
19 | "type-check": "tsc --noEmit",
20 | "deploy:local": "forge script script/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast",
21 | "deploy:sepolia": "forge script script/Deploy.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast --verify",
22 | "deploy:mainnet": "forge script script/Deploy.s.sol --rpc-url $MAINNET_RPC_URL --broadcast --verify",
23 | "lint": "forge fmt --check",
24 | "lint:fix": "forge fmt",
25 | "clean": "forge clean && rm -rf dist",
26 | "compile": "forge build",
27 | "size": "forge build --sizes"
28 | },
29 | "keywords": [
30 | "ethereum",
31 | "solidity",
32 | "smart-contracts",
33 | "escrow",
34 | "blob",
35 | "eip-4844"
36 | ],
37 | "author": {
38 | "name": "Ethereum Community Foundation",
39 | "email": "engineering@ethcf.org",
40 | "url": "https://ethcf.org/"
41 | },
42 | "license": "Apache-2.0",
43 | "devDependencies": {
44 | "@types/node": "^24.2.0",
45 | "typescript": "^5.2.0"
46 | },
47 | "repository": {
48 | "type": "git",
49 | "url": "https://github.com/blobkit/blobkit.git",
50 | "directory": "packages/contracts"
51 | },
52 | "bugs": {
53 | "url": "https://github.com/blobkit/blobkit/issues"
54 | },
55 | "homepage": "https://github.com/blobkit/blobkit/tree/main/packages/contracts#readme"
56 | }
57 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # BlobKit Configuration
2 | # Copy this file to .env and fill in your values
3 |
4 | # SDK Configuration
5 | BLOBKIT_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
6 | BLOBKIT_CHAIN_ID=1
7 | BLOBKIT_PROXY_URL=https://proxy.blobkit.org
8 | BLOBKIT_ESCROW_1=0x... # Mainnet escrow contract
9 | BLOBKIT_ESCROW_11155111=0x... # Sepolia escrow contract
10 | BLOBKIT_ESCROW_17000=0x... # Holesky escrow contract
11 | BLOBKIT_LOG_LEVEL=info
12 |
13 | # Proxy Server Configuration
14 | RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
15 | ETHEREUM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Fallback
16 | CHAIN_ID=1
17 | ESCROW_CONTRACT=0x... # Escrow contract address
18 | PRIVATE_KEY=0x... # Proxy operator private key (keep secure!)
19 | REQUEST_SIGNING_SECRET=your-secret-minimum-32-characters
20 |
21 | # Proxy Server Options
22 | PORT=3000
23 | HOST=0.0.0.0
24 | PROXY_FEE_PERCENT=0 # Default 0% (public good)
25 | MAX_BLOB_SIZE=131072 # 128KB
26 | RATE_LIMIT_REQUESTS=10
27 | RATE_LIMIT_WINDOW=60000 # milliseconds
28 | JOB_TIMEOUT=300000 # 5 minutes in milliseconds
29 | LOG_LEVEL=info
30 | CORS_ORIGIN=* # Configure for production
31 |
32 | # Optional Services
33 | REDIS_URL=redis://localhost:6379 # For persistent job queue
34 |
35 | # Key Management Services (Production)
36 | AWS_KMS_KEY_ID=arn:aws:kms:region:account:key/id
37 | AWS_REGION=us-east-1
38 | GCP_KMS_KEY_ID=projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME
39 | GCP_PROJECT_ID=your-project-id
40 | HSM_KEY_ID=hsm-key-id
41 | HSM_PIN=hsm-pin
42 |
43 | # Development Overrides
44 | OVERRIDE_BLOBKIT_ENVIRONMENT=node # Force environment detection
45 | NODE_ENV=development
46 |
47 | # Deployment Detection (Auto-set by platforms)
48 | VERCEL=1
49 | NETLIFY=1
50 | AWS_LAMBDA_FUNCTION_NAME=function-name
51 | FUNCTIONS_WORKER=1
52 | CF_PAGES=1
53 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | # Local Ethereum node (Anvil)
5 | anvil:
6 | image: ghcr.io/foundry-rs/foundry:latest
7 | command: anvil --host 0.0.0.0 --port 8545 --chain-id 31337 --accounts 10 --balance 10000 --block-time 1
8 | ports:
9 | - '8545:8545'
10 | healthcheck:
11 | test: ['CMD', 'cast', 'block-number', '--rpc-url', 'http://localhost:8545']
12 | interval: 5s
13 | timeout: 3s
14 | retries: 5
15 |
16 | # Redis for job queues (optional)
17 | redis:
18 | image: redis:7-alpine
19 | ports:
20 | - '6379:6379'
21 | volumes:
22 | - redis-data:/data
23 | healthcheck:
24 | test: ['CMD', 'redis-cli', 'ping']
25 | interval: 5s
26 | timeout: 3s
27 | retries: 5
28 |
29 | # BlobKit Proxy Server
30 | proxy:
31 | build: .
32 | ports:
33 | - '3000:3000'
34 | - '9090:9090'
35 | environment:
36 | - NODE_ENV=development
37 | - PORT=3000
38 | - HOST=0.0.0.0
39 | - RPC_URL=http://anvil:8545
40 | - CHAIN_ID=31337
41 | - REDIS_URL=redis://redis:6379
42 | - LOG_LEVEL=debug
43 | - RATE_LIMIT_REQUESTS=1000
44 | - RATE_LIMIT_WINDOW_MS=60000
45 | - PROXY_FEE_PERCENT=0
46 | - MAX_BLOB_SIZE=131072
47 | - JOB_TIMEOUT_SECONDS=300
48 | - METRICS_ENABLED=true
49 | - METRICS_PORT=9090
50 | env_file:
51 | - .env
52 | depends_on:
53 | anvil:
54 | condition: service_healthy
55 | redis:
56 | condition: service_healthy
57 | healthcheck:
58 | test:
59 | [
60 | 'CMD',
61 | 'wget',
62 | '--no-verbose',
63 | '--tries=1',
64 | '--spider',
65 | 'http://localhost:3000/api/v1/health'
66 | ]
67 | interval: 10s
68 | timeout: 5s
69 | retries: 3
70 |
71 | volumes:
72 | redis-data:
73 |
--------------------------------------------------------------------------------
/packages/sdk/test/setup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Jest test setup - Configuration only, no logic
3 | *
4 | * This file configures Jest globals and test environment.
5 | * For test utilities and helpers, see utils.ts
6 | */
7 |
8 | import { jest } from '@jest/globals';
9 |
10 | // Configure test timeout
11 | jest.setTimeout(30000);
12 |
13 | // Extend global test environment types
14 | declare global {
15 | namespace jest {
16 | interface Matchers {
17 | toBeValidHex(length?: number): R;
18 | toBeValidAddress(): R;
19 | toBeValidBlobHash(): R;
20 | }
21 | }
22 | }
23 |
24 | // Add custom matchers for common validations
25 | expect.extend({
26 | toBeValidHex(received: unknown, length?: number) {
27 | const hexRegex = length ? new RegExp(`^0x[a-fA-F0-9]{${length}}$`) : /^0x[a-fA-F0-9]+$/;
28 |
29 | const pass = typeof received === 'string' && hexRegex.test(received);
30 |
31 | return {
32 | pass,
33 | message: () =>
34 | pass
35 | ? `expected ${received} not to be a valid hex string`
36 | : `expected ${received} to be a valid hex string${length ? ` of length ${length}` : ''}`
37 | };
38 | },
39 |
40 | toBeValidAddress(received: unknown) {
41 | const pass = typeof received === 'string' && /^0x[a-fA-F0-9]{40}$/.test(received);
42 |
43 | return {
44 | pass,
45 | message: () =>
46 | pass
47 | ? `expected ${received} not to be a valid Ethereum address`
48 | : `expected ${received} to be a valid Ethereum address`
49 | };
50 | },
51 |
52 | toBeValidBlobHash(received: unknown) {
53 | const pass = typeof received === 'string' && /^0x01[a-fA-F0-9]{62}$/.test(received); // Version 0x01 + 62 hex chars
54 |
55 | return {
56 | pass,
57 | message: () =>
58 | pass
59 | ? `expected ${received} not to be a valid blob hash`
60 | : `expected ${received} to be a valid blob hash (should start with 0x01)`
61 | };
62 | }
63 | });
64 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage
2 | FROM node:22-alpine AS builder
3 |
4 | WORKDIR /app
5 |
6 | # Copy package files
7 | COPY package*.json tsconfig*.json ./
8 | # COPY packages/contracts/package*.json ./packages/contracts/
9 | COPY packages/proxy-server/package*.json ./packages/proxy-server/
10 | COPY packages/sdk/package*.json ./packages/sdk/
11 |
12 | # Install dependencies
13 | RUN npm ci --workspace=packages/proxy-server --workspace=packages/sdk
14 |
15 | # Copy source code
16 | # COPY packages/contracts ./packages/contracts
17 |
18 | COPY packages/proxy-server ./packages/proxy-server
19 | COPY packages/sdk ./packages/sdk
20 |
21 | # Build packages
22 | WORKDIR /app/packages/sdk
23 | RUN npm run build
24 | WORKDIR /app
25 | RUN npm run build --workspace=packages/proxy-server
26 |
27 | # Production stage
28 | FROM node:22-alpine
29 |
30 | WORKDIR /app
31 |
32 | # Install production dependencies only
33 | COPY package*.json ./
34 | COPY packages/proxy-server/package*.json ./packages/proxy-server/
35 | COPY packages/sdk/package*.json ./packages/sdk/
36 | COPY packages/proxy-server/openapi.yaml .
37 |
38 | RUN npm ci --workspace=packages/proxy-server --workspace=packages/sdk --production
39 |
40 | # Copy built artifacts
41 | COPY --from=builder /app/packages/sdk/dist ./packages/sdk/dist
42 | COPY --from=builder /app/packages/proxy-server/dist ./packages/proxy-server/dist
43 |
44 | # Copy necessary runtime files
45 | COPY packages/proxy-server/openapi.yaml ./packages/proxy-server/
46 |
47 | # Create non-root user
48 | RUN addgroup -g 1001 -S nodejs && \
49 | adduser -S nodejs -u 1001
50 |
51 | # RUN chown -R nodejs:nodejs /app
52 | USER nodejs
53 |
54 | # Expose ports
55 | EXPOSE 3000 9090
56 |
57 | # Health check
58 | HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
59 | CMD node -e "require('http').get('http://localhost:3000/api/v1/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1))"
60 |
61 | # Start proxy server
62 | CMD ["node", "packages/proxy-server/dist/index.js"]
63 |
--------------------------------------------------------------------------------
/docs/distributed-tracing.md:
--------------------------------------------------------------------------------
1 | # Distributed Tracing
2 |
3 | BlobKit Proxy Server includes basic distributed tracing support for request correlation and debugging.
4 |
5 | ## Overview
6 |
7 | The tracing system provides:
8 |
9 | - Automatic trace ID generation for each request
10 | - Request correlation in logs
11 | - Basic performance timing
12 | - Error tracking with trace context
13 |
14 | ## HTTP Headers
15 |
16 | The proxy server uses these headers for trace propagation:
17 |
18 | - `X-Trace-Id`: Unique identifier for the request
19 | - `X-Span-Id`: Identifier for the current operation
20 |
21 | ### Example Request
22 |
23 | ```bash
24 | curl -X POST http://localhost:3000/api/v1/blob/write \
25 | -H "Content-Type: application/json" \
26 | -H "X-BlobKit-Signature: v1:..." \
27 | -d @blob-request.json
28 | ```
29 |
30 | The server will automatically generate trace IDs if not provided.
31 |
32 | ## Trace Context in Logs
33 |
34 | All log entries include trace context:
35 |
36 | ```json
37 | {
38 | "timestamp": "2024-01-15T10:30:00.000Z",
39 | "level": "info",
40 | "service": "BlobRoute",
41 | "message": "Blob write completed successfully",
42 | "traceId": "550e8400-e29b-41d4-a716-446655440000",
43 | "spanId": "661e8400-e29b-41d4-a716-446655440001",
44 | "jobId": "0x123..."
45 | }
46 | ```
47 |
48 | ## Configuration
49 |
50 | Tracing is automatically enabled. To disable or adjust logging:
51 |
52 | ```bash
53 | LOG_LEVEL=debug # Options: debug, info, warn, error
54 | ```
55 |
56 | ## Integration with Monitoring
57 |
58 | The proxy server exposes Prometheus metrics that can be correlated with traces:
59 |
60 | - `blobkit_http_request_duration_seconds` - Request duration by endpoint
61 | - `blobkit_blob_submissions_total` - Blob submission counts
62 | - `blobkit_errors_total` - Error counts by type
63 |
64 | Access metrics at: `http://localhost:3000/metrics`
65 |
66 | ## Future Enhancements
67 |
68 | The current implementation provides basic tracing. Future versions may add:
69 |
70 | - OpenTelemetry export support
71 | - Distributed tracing across SDK and proxy
72 | - Trace sampling configuration
73 | - Integration with APM providers (Datadog, New Relic, etc.)
74 |
--------------------------------------------------------------------------------
/packages/sdk/rollup.config.ts:
--------------------------------------------------------------------------------
1 | import type { RollupOptions } from 'rollup';
2 | import typescript from '@rollup/plugin-typescript';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import commonjs from '@rollup/plugin-commonjs';
5 | import dts from 'rollup-plugin-dts';
6 |
7 | const external = ['kzg-wasm', 'ethers', 'crypto'];
8 |
9 | const config: RollupOptions[] = [
10 | // Main build
11 | {
12 | input: 'src/index.ts',
13 | output: [
14 | {
15 | dir: 'dist',
16 | format: 'cjs',
17 | sourcemap: true,
18 | entryFileNames: 'index.js',
19 | preserveModules: true,
20 | preserveModulesRoot: 'src'
21 | },
22 | {
23 | dir: 'dist',
24 | format: 'es',
25 | sourcemap: true,
26 | entryFileNames: 'index.esm.js',
27 | preserveModules: false
28 | }
29 | ],
30 | plugins: [
31 | resolve(),
32 | commonjs(),
33 | typescript({
34 | tsconfig: './tsconfig.json',
35 | exclude: ['**/*.test.ts', '**/*.spec.ts']
36 | })
37 | ],
38 | external
39 | },
40 | // Browser build
41 | {
42 | input: 'src/browser.ts',
43 | output: [
44 | {
45 | file: 'dist/browser.js',
46 | format: 'iife',
47 | name: 'BlobKit',
48 | sourcemap: true,
49 | inlineDynamicImports: true
50 | },
51 | {
52 | file: 'dist/browser.esm.js',
53 | format: 'es',
54 | sourcemap: true,
55 | inlineDynamicImports: true
56 | }
57 | ],
58 | plugins: [
59 | resolve(),
60 | commonjs(),
61 | typescript({
62 | tsconfig: './tsconfig.json',
63 | exclude: ['**/*.test.ts', '**/*.spec.ts']
64 | })
65 | ],
66 | external: ['ethers']
67 | },
68 | // Type definitions
69 | {
70 | input: 'src/index.ts',
71 | output: {
72 | file: 'dist/index.d.ts',
73 | format: 'es'
74 | },
75 | plugins: [dts()]
76 | },
77 | {
78 | input: 'src/browser.ts',
79 | output: {
80 | file: 'dist/browser.d.ts',
81 | format: 'es'
82 | },
83 | plugins: [dts()]
84 | }
85 | ];
86 |
87 | export default config;
88 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/middleware/error-handler.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | import { ProxyError, ProxyErrorCode, ErrorResponse } from '../types.js';
3 | import { createLogger } from '../utils/logger.js';
4 |
5 | const logger = createLogger('ErrorHandler');
6 |
7 | /**
8 | * Global error handling middleware
9 | */
10 | export const errorHandler = (
11 | err: Error | ProxyError,
12 | req: Request,
13 | res: Response,
14 | _next: NextFunction
15 | ): Response => {
16 | // Log the error
17 | logger.error('Request error:', {
18 | error: err.message,
19 | stack: err.stack,
20 | url: req.url,
21 | method: req.method,
22 | ip: req.ip,
23 | userAgent: req.get('User-Agent')
24 | });
25 |
26 | // Handle ProxyError instances
27 | if (err instanceof ProxyError) {
28 | const response: ErrorResponse = {
29 | error: err.code,
30 | message: err.message,
31 | details: err.details
32 | };
33 |
34 | return res.status(err.statusCode).json(response);
35 | }
36 |
37 | // Handle validation errors from express-validator
38 | if (err.name === 'ValidationError') {
39 | const response: ErrorResponse = {
40 | error: ProxyErrorCode.INVALID_REQUEST,
41 | message: err.message
42 | };
43 |
44 | return res.status(400).json(response);
45 | }
46 |
47 | // Handle other known error types
48 | if (err.name === 'SyntaxError' && 'body' in err) {
49 | const response: ErrorResponse = {
50 | error: ProxyErrorCode.INVALID_REQUEST,
51 | message: 'Invalid JSON in request body'
52 | };
53 |
54 | return res.status(400).json(response);
55 | }
56 |
57 | // Default internal server error
58 | const response: ErrorResponse = {
59 | error: ProxyErrorCode.INTERNAL_ERROR,
60 | message: 'Internal server error'
61 | };
62 |
63 | return res.status(500).json(response);
64 | };
65 |
66 | /**
67 | * 404 handler for unknown routes
68 | */
69 | export const notFoundHandler = (req: Request, res: Response) => {
70 | const response: ErrorResponse = {
71 | error: ProxyErrorCode.INVALID_REQUEST,
72 | message: `Route ${req.method} ${req.path} not found`
73 | };
74 |
75 | res.status(404).json(response);
76 | };
77 |
--------------------------------------------------------------------------------
/packages/sdk/rollup.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import typescript from '@rollup/plugin-typescript';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import commonjs from '@rollup/plugin-commonjs';
5 | import dts from 'rollup-plugin-dts';
6 |
7 | const external = ['kzg-wasm', 'ethers', 'axios', 'crypto'];
8 |
9 | /** @type {import('rollup').RollupOptions[]} */
10 | const config = [
11 | // Main build
12 | {
13 | input: 'src/index.ts',
14 | output: [
15 | {
16 | file: 'dist/index.js',
17 | format: 'es',
18 | sourcemap: true
19 | },
20 | {
21 | file: 'dist/index.cjs',
22 | format: 'cjs',
23 | sourcemap: true,
24 | exports: 'named'
25 | }
26 | ],
27 | plugins: [
28 | resolve({
29 | preferBuiltins: true
30 | }),
31 | commonjs(),
32 | typescript({
33 | tsconfig: './tsconfig.json',
34 | exclude: ['**/*.test.ts', '**/*.spec.ts'],
35 | module: 'esnext',
36 | declaration: false,
37 | declarationMap: false
38 | })
39 | ],
40 | external
41 | },
42 | // Browser build
43 | {
44 | input: 'src/browser.ts',
45 | output: [
46 | {
47 | file: 'dist/browser.js',
48 | format: 'es',
49 | sourcemap: true
50 | },
51 | {
52 | file: 'dist/browser.cjs',
53 | format: 'cjs',
54 | sourcemap: true,
55 | exports: 'named'
56 | }
57 | ],
58 | plugins: [
59 | resolve({
60 | preferBuiltins: false,
61 | browser: true
62 | }),
63 | commonjs(),
64 | typescript({
65 | tsconfig: './tsconfig.json',
66 | exclude: ['**/*.test.ts', '**/*.spec.ts'],
67 | module: 'esnext',
68 | declaration: false,
69 | declarationMap: false
70 | })
71 | ],
72 | external
73 | },
74 | // Type definitions
75 | {
76 | input: 'src/index.ts',
77 | output: {
78 | file: 'dist/index.d.ts',
79 | format: 'es'
80 | },
81 | plugins: [dts()]
82 | },
83 | {
84 | input: 'src/browser.ts',
85 | output: {
86 | file: 'dist/browser.d.ts',
87 | format: 'es'
88 | },
89 | plugins: [dts()]
90 | }
91 | ];
92 |
93 | export default config;
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blobkit-monorepo",
3 | "version": "1.0.0",
4 | "description": "BlobKit monorepo - comprehensive blob storage solution for Ethereum",
5 | "type": "module",
6 | "private": true,
7 | "author": {
8 | "name": "Ethereum Community Foundation",
9 | "email": "engineering@ethcf.org",
10 | "url": "https://ethcf.org/"
11 | },
12 | "license": "Apache-2.0",
13 | "contributors": [
14 | {
15 | "name": "Zak Cole",
16 | "email": "zcole@linux.com",
17 | "url": "https://x.com/0xzak"
18 | }
19 | ],
20 | "workspaces": [
21 | "packages/*"
22 | ],
23 | "scripts": {
24 | "build": "npm run build --workspaces",
25 | "test": "npm run test --workspaces",
26 | "lint": "npm run lint --workspaces",
27 | "lint:fix": "npm run lint:fix --workspaces",
28 | "format": "prettier --write \"**/*.{js,ts,json,md,yml,yaml}\"",
29 | "format:check": "prettier --check \"**/*.{js,ts,json,md,yml,yaml}\"",
30 | "clean": "npm run clean --workspaces",
31 | "type-check": "npm run type-check --workspaces",
32 | "docs:lint": "markdownlint docs/**/*.md README.md",
33 | "docs:lint:fix": "markdownlint docs/**/*.md README.md --fix",
34 | "changeset": "changeset",
35 | "version-packages": "changeset version",
36 | "release": "npm run build && changeset publish",
37 | "dev": "bash scripts/dev.sh",
38 | "dev:anvil": "anvil --host 0.0.0.0 --port 8545",
39 | "precommit": "npm run format:check && npm run lint && npm run type-check",
40 | "hooks:setup": "git config core.hooksPath .githooks",
41 | "env:check": "node scripts/check-env.mjs",
42 | "env:check:sdk": "node scripts/check-env.mjs sdk",
43 | "env:check:proxy": "node scripts/check-env.mjs proxy"
44 | },
45 | "devDependencies": {
46 | "@changesets/cli": "^2.29.6",
47 | "@types/node": "^24.3.0",
48 | "@typescript-eslint/eslint-plugin": "^8.41.0",
49 | "@typescript-eslint/parser": "^8.41.0",
50 | "markdownlint-cli": "0.45.0",
51 | "prettier": "^3.6.2",
52 | "ts-node": "^10.9.2"
53 | },
54 | "engines": {
55 | "node": ">=20.0.0",
56 | "npm": ">=9.0.0"
57 | },
58 | "repository": {
59 | "type": "git",
60 | "url": "https://github.com/blobkit/blobkit.git"
61 | },
62 | "bugs": {
63 | "url": "https://github.com/blobkit/blobkit/issues"
64 | },
65 | "homepage": "https://github.com/blobkit/blobkit#readme"
66 | }
67 |
--------------------------------------------------------------------------------
/packages/sdk/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@blobkit/sdk",
3 | "version": "3.1.5",
4 | "description": "TypeScript SDK for blob storage with EIP-4844 support and browser compatibility",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "types": "dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/index.d.ts",
11 | "import": "./dist/index.js",
12 | "require": "./dist/index.cjs"
13 | },
14 | "./browser": {
15 | "types": "./dist/browser.d.ts",
16 | "import": "./dist/browser.js",
17 | "require": "./dist/browser.cjs"
18 | }
19 | },
20 | "files": [
21 | "dist",
22 | "README.md",
23 | "LICENSE"
24 | ],
25 | "scripts": {
26 | "build": "tsc --emitDeclarationOnly && node ./build.js",
27 | "dev": "rollup -c -w",
28 | "test": "jest",
29 | "test:coverage": "jest --coverage",
30 | "lint": "eslint src",
31 | "lint:fix": "eslint src --fix",
32 | "type-check": "tsc --noEmit",
33 | "clean": "rm -rf dist",
34 | "docs": "typedoc",
35 | "docs:serve": "npx http-server docs -p 8080 -o"
36 | },
37 | "keywords": [
38 | "ethereum",
39 | "blob",
40 | "eip-4844",
41 | "web3",
42 | "storage",
43 | "typescript"
44 | ],
45 | "author": {
46 | "name": "Ethereum Community Foundation",
47 | "email": "engineering@ethcf.org",
48 | "url": "https://ethcf.org/"
49 | },
50 | "license": "Apache-2.0",
51 | "dependencies": {
52 | "@blobkit/kzg-wasm": "2.2.21",
53 | "ethers": "^6.15.0"
54 | },
55 | "devDependencies": {
56 | "@jest/globals": "^30.0.5",
57 | "@rollup/plugin-commonjs": "^28.0.6",
58 | "@rollup/plugin-node-resolve": "^16.0.1",
59 | "@rollup/plugin-typescript": "^12.1.4",
60 | "@types/jest": "^30.0.0",
61 | "@types/node": "^24.2.1",
62 | "eslint": "^9.33.0",
63 | "jest": "^30.0.5",
64 | "rollup": "^4.46.2",
65 | "rollup-plugin-dts": "^6.2.1",
66 | "ts-jest": "^29.4.1",
67 | "typedoc": "^0.28.10",
68 | "typedoc-plugin-markdown": "^4.8.1",
69 | "typescript": "^5.9.2"
70 | },
71 | "repository": {
72 | "type": "git",
73 | "url": "git+https://github.com/blobkit/blobkit.git",
74 | "directory": "packages/sdk"
75 | },
76 | "bugs": {
77 | "url": "https://github.com/blobkit/blobkit/issues"
78 | },
79 | "homepage": "https://github.com/blobkit/blobkit/tree/main/packages/sdk#readme"
80 | }
81 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | BlobKit provides TypeScript tools for EIP-4844 blob storage on Ethereum, including browser support via proxy servers.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install @blobkit/sdk ethers
9 | ```
10 |
11 | ## Browser Usage
12 |
13 | ```typescript
14 | import { BlobKit } from '@blobkit/sdk';
15 | import { ethers } from 'ethers';
16 |
17 | const provider = new ethers.BrowserProvider(window.ethereum);
18 | const signer = await provider.getSigner();
19 |
20 | const blobkit = new BlobKit(
21 | {
22 | rpcUrl: process.env.BLOBKIT_RPC_URL!,
23 | chainId: 1,
24 | proxyUrl: 'https://proxy.example.com',
25 | requestSigningSecret: 'shared-secret-with-proxy'
26 | },
27 | signer
28 | );
29 |
30 | const result = await blobkit.writeBlob({ message: 'Hello world' }, { appId: 'my-app' });
31 |
32 | console.log('Blob hash:', result.blobHash);
33 | console.log('Transaction:', result.blobTxHash);
34 | ```
35 |
36 | ## Node.js Usage
37 |
38 | ```typescript
39 | import { BlobKit } from '@blobkit/sdk';
40 | import { ethers } from 'ethers';
41 |
42 | const provider = new ethers.JsonRpcProvider(process.env.BLOBKIT_RPC_URL!);
43 | const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
44 |
45 | const blobkit = new BlobKit(
46 | {
47 | rpcUrl: process.env.BLOBKIT_RPC_URL!,
48 | chainId: 1
49 | },
50 | signer
51 | );
52 |
53 | const result = await blobkit.writeBlob(data);
54 | console.log('Blob hash:', result.blobHash);
55 | ```
56 |
57 | ## Components
58 |
59 | - **[@blobkit/sdk](../packages/sdk/)** - TypeScript SDK for blob storage
60 | - **[@blobkit/proxy-server](../packages/proxy-server/)** - Proxy server for browser environments
61 | - **[@blobkit/contracts](../packages/contracts/)** - Smart contracts for trustless payments
62 |
63 | ## Payment Flow
64 |
65 | ```mermaid
66 | sequenceDiagram
67 | participant Browser
68 | participant SDK
69 | participant Escrow
70 | participant Proxy
71 | participant Ethereum
72 |
73 | Browser->>SDK: writeBlob(data)
74 | SDK->>SDK: estimateCost()
75 | SDK->>Browser: Show cost estimate
76 | Browser->>Escrow: Pay via MetaMask
77 | SDK->>Proxy: Submit blob + jobId
78 | Proxy->>Escrow: Verify payment
79 | Proxy->>Ethereum: Execute blob transaction
80 | Proxy->>Escrow: Complete job, claim fee
81 | Proxy->>SDK: Return blob receipt
82 | SDK->>Browser: Show success
83 | ```
84 |
85 | ## Next Steps
86 |
87 | - [SDK Documentation](sdk/)
88 | - [Proxy Documentation](proxy/)
89 | - [Contract Documentation](contracts/)
90 | - [Architecture](architecture.md)
91 |
--------------------------------------------------------------------------------
/packages/proxy-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@blobkit/proxy-server",
3 | "version": "1.0.4",
4 | "description": "Express.js proxy server for blob transaction execution with payment verification",
5 | "type": "module",
6 | "main": "dist/index.js",
7 | "types": "dist/index.d.ts",
8 | "bin": {
9 | "blobkit-proxy": "dist/cli.js"
10 | },
11 | "files": [
12 | "dist",
13 | "openapi.yaml",
14 | "README.md",
15 | "LICENSE"
16 | ],
17 | "scripts": {
18 | "build": "tsc",
19 | "dev": "tsx src/cli.ts dev-proxy",
20 | "start": "node dist/index.js",
21 | "test": "jest",
22 | "test:coverage": "jest --coverage",
23 | "lint": "eslint src",
24 | "lint:fix": "eslint src --fix",
25 | "type-check": "tsc --noEmit",
26 | "clean": "rm -rf dist",
27 | "docker:build": "docker build -t blobkit-proxy .",
28 | "docker:run": "docker run -p 3000:3000 blobkit-proxy"
29 | },
30 | "keywords": [
31 | "ethereum",
32 | "blob",
33 | "proxy",
34 | "eip-4844",
35 | "server",
36 | "express"
37 | ],
38 | "author": {
39 | "name": "Ethereum Community Foundation",
40 | "email": "engineering@ethcf.org",
41 | "url": "https://ethcf.org/"
42 | },
43 | "license": "Apache-2.0",
44 | "dependencies": {
45 | "@aws-sdk/client-kms": "^3.876.0",
46 | "@google-cloud/kms": "^5.1.0",
47 | "@blobkit/sdk": "^2.0.0",
48 | "@opentelemetry/api": "^1.7.0",
49 | "commander": "^14.0.0",
50 | "cors": "^2.8.5",
51 | "dotenv": "^17.2.1",
52 | "ethers": "^6.7.1",
53 | "express": "^5.1.0",
54 | "express-rate-limit": "^8.0.1",
55 | "express-validator": "^7.0.1",
56 | "helmet": "^8.1.0",
57 | "prom-client": "^15.0.0",
58 | "redis": "^5.8.2",
59 | "swagger-ui-express": "^5.0.0",
60 | "uuid": "^11.1.0",
61 | "winston": "^3.17.0",
62 | "yamljs": "^0.3.0"
63 | },
64 | "devDependencies": {
65 | "@types/cors": "^2.8.16",
66 | "@types/express": "^5.0.3",
67 | "@types/jest": "^30.0.0",
68 | "@types/node": "^24.3.0",
69 | "@types/supertest": "^6.0.3",
70 | "@types/swagger-ui-express": "^4.1.6",
71 | "@types/uuid": "^10.0.0",
72 | "@types/yamljs": "^0.2.34",
73 | "eslint": "^9.34.0",
74 | "jest": "^30.0.5",
75 | "supertest": "^7.1.4",
76 | "tsx": "^4.20.5",
77 | "typescript": "^5.3.3"
78 | },
79 | "repository": {
80 | "type": "git",
81 | "url": "https://github.com/blobkit/blobkit.git",
82 | "directory": "packages/proxy-server"
83 | },
84 | "bugs": {
85 | "url": "https://github.com/blobkit/blobkit/issues"
86 | },
87 | "homepage": "https://github.com/blobkit/blobkit/tree/main/packages/proxy-server#readme"
88 | }
89 |
--------------------------------------------------------------------------------
/packages/sdk/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * BlobKit SDK - Main Entry Point
3 | *
4 | * This module exports the public API for the BlobKit SDK.
5 | */
6 |
7 | // Main SDK class
8 | export { BlobKit } from './blobkit.js';
9 | import { BlobKit } from './blobkit.js';
10 | import type { Signer, BlobKitConfig, ProcessEnv } from './types.js';
11 |
12 | // Component modules
13 | export { PaymentManager } from './payment.js';
14 | export { ProxyClient } from './proxy-client.js';
15 | export { BlobSubmitter } from './blob-submitter.js';
16 | export { BlobReader } from './blob-reader.js';
17 |
18 | // Core types and interfaces
19 | export type {
20 | BlobKitConfig,
21 | BlobKitEnvironment,
22 | BlobMeta,
23 | BlobReceipt,
24 | BlobPaymentResult,
25 | BlobReadResult,
26 | CostEstimate,
27 | JobStatus,
28 | ProxyHealthResponse,
29 | Signer,
30 | TransactionRequest,
31 | TransactionResponse,
32 | TransactionReceipt,
33 | Provider,
34 | FeeData,
35 | ProcessEnv,
36 | } from './types.js';
37 |
38 | // Error handling
39 | export { BlobKitError, BlobKitErrorCode } from './types.js';
40 |
41 | // Environment utilities
42 | export { detectEnvironment, getEnvironmentCapabilities } from './environment.js';
43 |
44 | // Codec system
45 | export { defaultCodecRegistry, JsonCodec, RawCodec, TextCodec } from './codecs/index.js';
46 |
47 | // Essential utilities (only public-facing ones)
48 | export {
49 | generateJobId,
50 | calculatePayloadHash,
51 | formatEther,
52 | parseEther,
53 | isValidAddress,
54 | validateBlobSize,
55 | bytesToHex,
56 | hexToBytes
57 | } from './utils.js';
58 |
59 | // KZG utilities
60 | export {
61 | initializeKzg,
62 | encodeBlob,
63 | decodeBlob,
64 | blobToKzgCommitment,
65 | computeKzgProofs,
66 | commitmentToVersionedHash,
67 | FIELD_ELEMENTS_PER_BLOB,
68 | BYTES_PER_FIELD_ELEMENT,
69 | BLOB_SIZE
70 | } from './kzg.js';
71 |
72 | export { EscrowContractABI } from './abi/index.js';
73 |
74 | /**
75 | * Convenience function to create BlobKit instance from environment variables
76 | * @param signer Optional signer for transactions
77 | * @returns Configured BlobKit instance
78 | */
79 | export function createFromEnv(signer?: Signer): BlobKit {
80 | const env = process.env as ProcessEnv;
81 |
82 | const config: BlobKitConfig = {
83 | rpcUrl: env.BLOBKIT_RPC_URL || 'http://localhost:8545',
84 | archiveUrl: env.BLOBKIT_ARCHIVE_URL || 'https://api.blobscan.com',
85 | chainId: env.BLOBKIT_CHAIN_ID ? parseInt(env.BLOBKIT_CHAIN_ID, 10) : 31337,
86 | proxyUrl: env.BLOBKIT_PROXY_URL,
87 | logLevel: env.BLOBKIT_LOG_LEVEL || 'info'
88 | };
89 |
90 | return new BlobKit(config, signer);
91 | }
92 |
93 | /**
94 | * Package version
95 | */
96 | export const VERSION = '3.0.0';
97 |
--------------------------------------------------------------------------------
/packages/sdk/src/browser.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * BlobKit SDK - Browser Entry Point
3 | * Optimized build for browser environments with MetaMask integration
4 | * Exposes only browser-safe APIs (no Node.js built-ins like 'crypto' or 'fs')
5 | */
6 |
7 | import type { BlobKitConfig } from './types.js';
8 | import type { Signer } from 'ethers';
9 | export type {
10 | BlobKitConfig,
11 | BlobKitEnvironment,
12 | BlobMeta,
13 | BlobReceipt,
14 | BlobReadResult,
15 | CostEstimate,
16 | JobStatus,
17 | ProxyHealthResponse,
18 | TransactionRequest,
19 | TransactionResponse,
20 | TransactionReceipt,
21 | Provider,
22 | FeeData,
23 | } from './types.js';
24 |
25 | export { BlobKit } from './blobkit.js';
26 | export { BlobReader } from './blob-reader.js';
27 | export { detectEnvironment, getEnvironmentCapabilities } from './environment.js';
28 | export { defaultCodecRegistry, JsonCodec, RawCodec, TextCodec } from './codecs/index.js';
29 | export {
30 | generateJobId,
31 | calculatePayloadHash,
32 | formatEther,
33 | parseEther,
34 | isValidAddress,
35 | validateBlobSize,
36 | bytesToHex,
37 | hexToBytes
38 | } from './utils.js';
39 |
40 | // Browser-specific utilities
41 | // Window type extension for MetaMask
42 | declare global {
43 | interface Window {
44 | ethereum?: {
45 | request: (args: { method: string; params?: unknown[] }) => Promise;
46 | isMetaMask?: boolean;
47 | };
48 | }
49 | }
50 |
51 | export const connectMetaMask = async (): Promise => {
52 | if (typeof window === 'undefined' || !window.ethereum) {
53 | throw new Error(
54 | 'MetaMask not detected. Please install MetaMask to use BlobKit in the browser.'
55 | );
56 | }
57 |
58 | const ethereum = window.ethereum;
59 |
60 | try {
61 | // Request account access
62 | await ethereum.request({ method: 'eth_requestAccounts' });
63 |
64 | // Create ethers provider and signer
65 | const { ethers } = await import('ethers');
66 | const provider = new ethers.BrowserProvider(ethereum);
67 | const signer = await provider.getSigner();
68 |
69 | return signer;
70 | } catch (error) {
71 | throw new Error(
72 | `Failed to connect to MetaMask: ${error instanceof Error ? error.message : 'Unknown error'}`
73 | );
74 | }
75 | };
76 |
77 | /**
78 | * Creates a BlobKit instance with MetaMask integration
79 | * @param config BlobKit configuration
80 | * @returns BlobKit Ready-to-use BlobKit instance
81 | */
82 | export const createWithMetaMask = async (config: BlobKitConfig) => {
83 | const { BlobKit } = await import('./blobkit.js');
84 | const signer = await connectMetaMask();
85 | // Signer from ethers is now directly compatible
86 | return new BlobKit(config, signer as any);
87 | };
88 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/config.ts:
--------------------------------------------------------------------------------
1 | import { ProxyConfig } from './types.js';
2 |
3 | /**
4 | * Loads configuration from environment variables with defaults
5 | */
6 | export const loadConfig = (): ProxyConfig => {
7 | const rpcUrl = process.env.RPC_URL || process.env.ETHEREUM_RPC_URL;
8 | if (!rpcUrl) {
9 | throw new Error('RPC_URL environment variable is required');
10 | }
11 |
12 | // Private key is now optional - handled by secure signer
13 | const privateKey = process.env.PRIVATE_KEY || '';
14 |
15 | const escrowContract = process.env.ESCROW_CONTRACT;
16 | if (!escrowContract) {
17 | throw new Error('ESCROW_CONTRACT environment variable is required');
18 | }
19 |
20 |
21 | const httpProxyCount = parseInt(process.env.HTTP_PROXY_COUNT || '0');
22 |
23 | return {
24 | port: parseInt(process.env.PORT || '3000'),
25 | host: process.env.HOST || '0.0.0.0',
26 | rpcUrl,
27 | chainId: parseInt(process.env.CHAIN_ID || '1'),
28 | escrowContract,
29 | privateKey,
30 | proxyFeePercent: parseInt(process.env.PROXY_FEE_PERCENT || '0'),
31 | maxBlobSize: parseInt(process.env.MAX_BLOB_SIZE || '131072'), // 128KB
32 | rateLimitRequests: parseInt(process.env.RATE_LIMIT_REQUESTS || '10'),
33 | rateLimitWindow: parseInt(process.env.RATE_LIMIT_WINDOW || '60000'), // 60 seconds in milliseconds
34 | jobTimeout: parseInt(process.env.JOB_TIMEOUT || '300000'), // 5 minutes in milliseconds
35 | logLevel: (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') || 'info',
36 | httpProxyCount
37 | };
38 | };
39 |
40 | /**
41 | * Validates the configuration
42 | */
43 | export const validateConfig = (config: ProxyConfig): void => {
44 | if (config.port <= 0 || config.port > 65535) {
45 | throw new Error('Invalid port number');
46 | }
47 |
48 | if (config.proxyFeePercent < 0 || config.proxyFeePercent > 10) {
49 | throw new Error('Proxy fee percent must be between 0 and 10');
50 | }
51 |
52 | if (config.maxBlobSize <= 0 || config.maxBlobSize > 131072) {
53 | throw new Error('Max blob size must be between 1 and 131072 bytes');
54 | }
55 |
56 | // Private key validation is now handled by secure signer
57 | // Only validate if provided
58 | if (config.privateKey && !/^0x[a-fA-F0-9]{64}$/.test(config.privateKey)) {
59 | throw new Error('Invalid private key format');
60 | }
61 |
62 | if (!/^0x[a-fA-F0-9]{40}$/.test(config.escrowContract)) {
63 | throw new Error('Invalid escrow contract address');
64 | }
65 |
66 | if (config.rateLimitWindow < 1000 || config.rateLimitWindow > 3600000) {
67 | throw new Error('Rate limit window must be between 1 second and 1 hour (in milliseconds)');
68 | }
69 |
70 | if (config.jobTimeout < 60000 || config.jobTimeout > 86400000) {
71 | throw new Error('Job timeout must be between 1 minute and 24 hours (in milliseconds)');
72 | }
73 |
74 | };
75 |
--------------------------------------------------------------------------------
/ops/chart/values.yaml:
--------------------------------------------------------------------------------
1 | version: latest
2 | image: docker-image
3 | namespace: default
4 | projectId: gcp-project-id
5 |
6 | services:
7 | server:
8 | replicas: 1
9 | command: node
10 | args: ["packages/proxy-server/dist/index.js"]
11 | serviceAccountName: blobkit-proxy
12 | resources:
13 | cpu: 500m
14 | memory: 512Mi
15 | paths:
16 | - /
17 | healthPath: /api/v1/health
18 | maxReplicas: 3
19 | targetCPUUtil: 70
20 | ingress:
21 | domain: proxy.blobkit.org
22 | tlsSecret: blobkit-tls
23 | annotations:
24 | kubernetes.io/ingress.class: nginx
25 | nginx.org/mergeable-ingress-type: minion
26 | environment:
27 | - name: ESCROW_CONTRACT
28 | value: "0x2e8e414bc5c6B0b8339853CEDf965B4A28FB4838" # Mainnet escrow
29 | - name: ETH_RPC_URL
30 | valueFrom:
31 | configMapKeyRef:
32 | name: blobkit-proxy
33 | key: eth-rpc-url
34 | - name: RPC_URL
35 | valueFrom:
36 | configMapKeyRef:
37 | name: blobkit-proxy
38 | key: eth-rpc-url
39 | - name: CHAIN_ID
40 | value: "1" # Mainnet chain ID
41 |
42 | server-sepolia:
43 | replicas: 1
44 | command: node
45 | args: ["packages/proxy-server/dist/index.js"]
46 | serviceAccountName: blobkit-proxy-sepolia
47 | resources:
48 | cpu: 500m
49 | memory: 512Mi
50 | paths:
51 | - /
52 | healthPath: /api/v1/health
53 | maxReplicas: 1
54 | targetCPUUtil: 70
55 |
56 | ingress:
57 | domain: proxy-sepolia.blobkit.org
58 | tlsSecret: blobkit-tls
59 | annotations:
60 | kubernetes.io/ingress.class: nginx
61 | nginx.org/mergeable-ingress-type: minion
62 | environment:
63 | - name: ESCROW_CONTRACT
64 | value: "0x1B345402377A44F674376d6e0f6212e3B9991798" # Sepolia escrow
65 | - name: ETH_RPC_URL
66 | valueFrom:
67 | configMapKeyRef:
68 | name: blobkit-proxy
69 | key: sepolia-rpc-url
70 | - name: RPC_URL
71 | valueFrom:
72 | configMapKeyRef:
73 | name: blobkit-proxy
74 | key: sepolia-rpc-url
75 | - name: CHAIN_ID
76 | value: "11155111" # Sepolia chain ID
77 |
78 |
79 | service:
80 | type: ClusterIP
81 | port: 80
82 | protocol: TCP
83 | targetPort: 8080
84 |
85 | verbosity: DEBUG
86 |
87 | # Shared environment variables
88 | environment:
89 | - name: PORT
90 | value: "8080"
91 | - name: HTTP_PROXY_COUNT
92 | value: "1"
93 | - name: PROXY_FEE_PERCENT
94 | value: "0" # 0% fee
95 | - name: MAX_BLOB_SIZE
96 | value: "131072" # 128 KiB
97 | - name: JOB_TIMEOUT_SECONDS
98 | value: "300" # 5 minutes
99 | - name: GCP_KMS_KEY_NAME
100 | valueFrom:
101 | configMapKeyRef:
102 | name: blobkit-proxy
103 | key: kms-key-id
104 | - name: REDIS_URL
105 | valueFrom:
106 | secretKeyRef:
107 | name: blobkit-proxy
108 | key: redis-url
109 |
--------------------------------------------------------------------------------
/packages/proxy-server/test/setup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Jest test setup for proxy server - Configuration only
3 | *
4 | * This file configures Jest globals and test environment.
5 | * For test utilities and helpers, see utils.ts
6 | */
7 |
8 | import { jest } from '@jest/globals';
9 |
10 | // Configure test timeout for proxy server tests
11 | jest.setTimeout(30000);
12 |
13 | // Extend global test environment types for proxy-specific matchers
14 | declare global {
15 | namespace jest {
16 | interface Matchers {
17 | toBeValidProxyResponse(): R;
18 | toBeValidHealthResponse(): R;
19 | toBeValidJobStatus(): R;
20 | }
21 | }
22 | }
23 |
24 | // Add custom matchers for proxy response validation
25 | expect.extend({
26 | toBeValidProxyResponse(received: unknown) {
27 | const hasRequiredFields =
28 | received &&
29 | typeof received === 'object' &&
30 | 'success' in received &&
31 | typeof (received as any).success === 'boolean' &&
32 | ((received as any).success
33 | ? 'blobHash' in received && 'blobTxHash' in received && 'jobId' in received
34 | : 'error' in received && 'message' in received);
35 |
36 | return {
37 | message: () =>
38 | hasRequiredFields
39 | ? `expected ${JSON.stringify(received)} not to be a valid proxy response`
40 | : `expected ${JSON.stringify(received)} to be a valid proxy response with success, blobHash/error fields`,
41 | pass: Boolean(hasRequiredFields)
42 | };
43 | },
44 |
45 | toBeValidHealthResponse(received: unknown) {
46 | const isValid =
47 | received &&
48 | typeof received === 'object' &&
49 | 'healthy' in received &&
50 | typeof (received as any).healthy === 'boolean' &&
51 | 'chainId' in received &&
52 | typeof (received as any).chainId === 'number' &&
53 | 'feePercent' in received &&
54 | typeof (received as any).feePercent === 'number' &&
55 | 'escrowContract' in received &&
56 | typeof (received as any).escrowContract === 'string';
57 |
58 | return {
59 | message: () =>
60 | isValid
61 | ? `expected ${JSON.stringify(received)} not to be a valid health response`
62 | : `expected ${JSON.stringify(received)} to be a valid health response with healthy, chainId, feePercent, escrowContract fields`,
63 | pass: Boolean(isValid)
64 | };
65 | },
66 |
67 | toBeValidJobStatus(received: unknown) {
68 | const isValid =
69 | received &&
70 | typeof received === 'object' &&
71 | 'exists' in received &&
72 | 'completed' in received &&
73 | 'user' in received &&
74 | 'amount' in received &&
75 | 'timestamp' in received;
76 |
77 | return {
78 | message: () =>
79 | isValid
80 | ? `expected ${JSON.stringify(received)} not to be a valid job status`
81 | : `expected ${JSON.stringify(received)} to be a valid job status with exists, completed, user, amount, timestamp fields`,
82 | pass: Boolean(isValid)
83 | };
84 | }
85 | });
86 |
--------------------------------------------------------------------------------
/packages/sdk/src/serialize.ts:
--------------------------------------------------------------------------------
1 | import {Signature, ZeroAddress, getBigInt, toBeArray, getNumber, assertArgument, accessListify, isHexString, concat, encodeRlp} from 'ethers';
2 | import type {BigNumberish, AccessListish} from 'ethers';
3 | import type { TransactionRequest, BlobTxData } from './types.js';
4 |
5 | const BN_0 = BigInt(0);
6 | const BN_2 = BigInt(2);
7 | const BN_27 = BigInt(27)
8 | const BN_28 = BigInt(28)
9 | const BN_35 = BigInt(35);
10 | const BN_MAX_UINT = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
11 |
12 | const BLOB_SIZE = 4096 * 32;
13 |
14 | function formatNumber(_value: BigNumberish, name: string): Uint8Array {
15 | const value = getBigInt(_value, name);
16 | const result = toBeArray(value);
17 | assertArgument(result.length <= 32, `value too large`, `tx.${ name }`, value);
18 | return result;
19 | }
20 |
21 | function handleNumber(_value: string, param: string): number {
22 | if (_value === "0x") { return 0; }
23 | return getNumber(_value, param);
24 | }
25 |
26 | function handleUint(_value: string, param: string): bigint {
27 | if (_value === "0x") { return BN_0; }
28 | const value = getBigInt(_value, param);
29 | assertArgument(value <= BN_MAX_UINT, "value exceeds uint size", param, value);
30 | return value;
31 | }
32 |
33 |
34 | function formatAccessList(value: AccessListish): Array<[ string, Array ]> {
35 | return accessListify(value).map((set) => [ set.address, set.storageKeys ]);
36 | }
37 |
38 | function formatHashes(value: Array, param: string): Array {
39 | assertArgument(Array.isArray(value), `invalid ${ param }`, "value", value);
40 | for (let i = 0; i < value.length; i++) {
41 | assertArgument(isHexString(value[i], 32), "invalid ${ param } hash", `value[${ i }]`, value[i]);
42 | }
43 | return value;
44 | }
45 |
46 | export function SerializeEIP7495(tx: TransactionRequest, sig: null | Signature, blobs: null | Array): string {
47 | const fields: Array = [
48 | formatNumber(tx.chainId!, "chainId"),
49 | formatNumber(tx.nonce!, "nonce"),
50 | formatNumber(tx.maxPriorityFeePerGas || 0, "maxPriorityFeePerGas"),
51 | formatNumber(tx.maxFeePerGas || 0, "maxFeePerGas"),
52 | formatNumber(tx.gasLimit!, "gasLimit"),
53 | (tx.to || ZeroAddress),
54 | formatNumber(tx.value!, "value"),
55 | tx.data,
56 | formatAccessList(tx.accessList || [ ]),
57 | formatNumber(tx.maxFeePerBlobGas || 0, "maxFeePerBlobGas"),
58 | formatHashes(tx.blobVersionedHashes || [ ], "blobVersionedHashes")
59 | ];
60 |
61 | if (!!sig) {
62 | fields.push(formatNumber(sig.yParity, "yParity"));
63 | fields.push(toBeArray(sig.r));
64 | fields.push(toBeArray(sig.s));
65 |
66 | // We have blobs; return the network wrapped format
67 | if (blobs) {
68 | return concat([
69 | "0x03",
70 | encodeRlp([
71 | fields,
72 | formatNumber(tx.wrapperVersion || 1, "wrapperVersion"),
73 | blobs.map((b) => b.blob),
74 | blobs.map((b) => b.commitment),
75 | blobs.reduce((acc, b) => acc.concat(b.proofs), [] as Array),
76 | ])
77 | ]);
78 | }
79 |
80 | }
81 |
82 | return concat([ "0x03", encodeRlp(fields)]);
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/packages/sdk/src/environment.ts:
--------------------------------------------------------------------------------
1 | import { BlobKitEnvironment } from './types.js';
2 |
3 | /**
4 | * Detects the current execution environment
5 | * @returns The detected environment type
6 | */
7 | export const detectEnvironment = (): BlobKitEnvironment => {
8 | // Check for browser environment first (before accessing process)
9 | if (typeof window !== 'undefined' && typeof document !== 'undefined') {
10 | return 'browser';
11 | }
12 |
13 | // Check for Node.js environment
14 | if (typeof process !== 'undefined' && process.versions && process.versions.node) {
15 | // Check for override environment variable with validation
16 | if (process.env.OVERRIDE_BLOBKIT_ENVIRONMENT) {
17 | const override = process.env.OVERRIDE_BLOBKIT_ENVIRONMENT;
18 | const validEnvironments: BlobKitEnvironment[] = ['browser', 'node', 'serverless'];
19 |
20 | if (validEnvironments.includes(override as BlobKitEnvironment)) {
21 | return override as BlobKitEnvironment;
22 | } else {
23 | throw new Error(
24 | `Invalid OVERRIDE_BLOBKIT_ENVIRONMENT value: '${override}'. ` +
25 | `Must be one of: ${validEnvironments.join(', ')}`
26 | );
27 | }
28 | }
29 |
30 | // Check for serverless environments
31 | if (
32 | process.env.VERCEL ||
33 | process.env.NETLIFY ||
34 | process.env.AWS_LAMBDA_FUNCTION_NAME ||
35 | process.env.FUNCTIONS_WORKER ||
36 | process.env.CF_PAGES
37 | ) {
38 | return 'serverless';
39 | }
40 | return 'node';
41 | }
42 |
43 | // Fallback to serverless for unknown environments
44 | return 'serverless';
45 | };
46 |
47 | /**
48 | * Checks if the current environment is a browser
49 | */
50 | export const isBrowser = (): boolean => {
51 | return detectEnvironment() === 'browser';
52 | };
53 |
54 | /**
55 | * Checks if the current environment is Node.js
56 | */
57 | export const isNode = (): boolean => {
58 | return detectEnvironment() === 'node';
59 | };
60 |
61 | /**
62 | * Checks if the current environment is serverless
63 | */
64 | export const isServerless = (): boolean => {
65 | return detectEnvironment() === 'serverless';
66 | };
67 |
68 | /**
69 | * Gets environment-specific features and capabilities
70 | */
71 | export const getEnvironmentCapabilities = (env: BlobKitEnvironment) => {
72 | switch (env) {
73 | case 'browser':
74 | return {
75 | supportsDirectTransactions: false,
76 | requiresProxy: true,
77 | hasFileSystem: false,
78 | hasWebCrypto: typeof crypto !== 'undefined',
79 | hasMetaMask: typeof window !== 'undefined' && !!window.ethereum,
80 | canSubmitBlobs: false
81 | };
82 |
83 | case 'node':
84 | return {
85 | supportsDirectTransactions: true,
86 | requiresProxy: false,
87 | hasFileSystem: true,
88 | hasWebCrypto: false,
89 | hasMetaMask: false,
90 | canSubmitBlobs: true
91 | };
92 |
93 | case 'serverless':
94 | return {
95 | supportsDirectTransactions: true,
96 | requiresProxy: false,
97 | hasFileSystem: false,
98 | hasWebCrypto: typeof crypto !== 'undefined',
99 | hasMetaMask: false,
100 | canSubmitBlobs: false
101 | };
102 |
103 | default:
104 | throw new Error(`Unknown environment: ${env}`);
105 | }
106 | };
107 |
--------------------------------------------------------------------------------
/docs/proxy/README.md:
--------------------------------------------------------------------------------
1 | # Proxy Server Documentation
2 |
3 | Deployment and operation guide for @blobkit/proxy-server.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install @blobkit/proxy-server
9 | ```
10 |
11 | ## Quick Start
12 |
13 | ```bash
14 | # Using environment variables
15 | export RPC_URL=$MAINNET_RPC_URL
16 | export PRIVATE_KEY=$PRIVATE_KEY
17 | export ESCROW_CONTRACT=0x0000000000000000000000000000000000000000
18 |
19 | npx blobkit-proxy
20 | ```
21 |
22 | ## Configuration
23 |
24 | | Variable | Required | Default | Description |
25 | | ------------------- | -------- | ------- | -------------------------- |
26 | | `RPC_URL` | Yes | - | Ethereum RPC endpoint |
27 | | `PRIVATE_KEY` | Yes | - | Private key (64 hex chars) |
28 | | `ESCROW_CONTRACT` | Yes | - | Escrow contract address |
29 | | `PORT` | No | 3000 | Server port |
30 | | `HOST` | No | 0.0.0.0 | Server host |
31 | | `CHAIN_ID` | No | 1 | Chain ID |
32 | | `PROXY_FEE_PERCENT` | No | 0 | Fee percentage (0-10) |
33 | | `LOG_LEVEL` | No | info | Log level |
34 |
35 | ## API Endpoints
36 |
37 | ### Health Check
38 |
39 | ```bash
40 | GET /api/v1/health
41 | ```
42 |
43 | Returns server status, configuration, and blockchain connectivity.
44 |
45 | ### Blob Write
46 |
47 | ```bash
48 | POST /api/v1/blob/write
49 | ```
50 |
51 | **Request body:**
52 |
53 | ```json
54 | {
55 | "jobId": "user-0x1234-payload-hash-nonce",
56 | "paymentTxHash": "0x5678...",
57 | "payload": [72, 101, 108, 108, 111],
58 | "meta": {
59 | "appId": "my-app",
60 | "codec": "text",
61 | "filename": "hello.txt"
62 | }
63 | }
64 | ```
65 |
66 | **Response:**
67 |
68 | ```json
69 | {
70 | "success": true,
71 | "blobTxHash": "0xabcd...",
72 | "blockNumber": 18500000,
73 | "blobHash": "0xef01...",
74 | "commitment": "0x2345...",
75 | "proof": "0x6789...",
76 | "blobIndex": 0,
77 | "completionTxHash": "0xcdef..."
78 | }
79 | ```
80 |
81 | ## CLI Commands
82 |
83 | ```bash
84 | # Start server
85 | npx blobkit-proxy start
86 |
87 | # Check health
88 | npx blobkit-proxy health
89 |
90 | # Show configuration
91 | npx blobkit-proxy config
92 | ```
93 |
94 | ## Deployment
95 |
96 | ### Docker
97 |
98 | ```bash
99 | # Build image
100 | npm run docker:build
101 |
102 | # Run container
103 | docker run -p 3000:3000 \
104 | -e RPC_URL=$MAINNET_RPC_URL \
105 | -e PRIVATE_KEY=$PRIVATE_KEY \
106 | -e ESCROW_CONTRACT=0x0000000000000000000000000000000000000000 \
107 | blobkit-proxy
108 | ```
109 |
110 | ### Production Checklist
111 |
112 | - Use secure key management (AWS KMS, Hashicorp Vault)
113 | - Configure reverse proxy with TLS
114 | - Set up monitoring and alerting
115 | - Enable rate limiting
116 | - Use environment-specific configs
117 |
118 | ## Security
119 |
120 | - Never commit private keys to version control
121 | - Use secure private key storage (KMS)
122 | - Configure appropriate rate limits
123 | - Monitor for suspicious activity
124 | - Keep dependencies updated
125 |
126 | ## Testing and Development
127 |
128 | ```bash
129 | npm test # Run tests
130 | npm run build # Build for production
131 | npm run dev # Start development server
132 | npm run lint # Lint code
133 | npm run type-check # TypeScript checking
134 | ```
135 |
--------------------------------------------------------------------------------
/ops/chart/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | {{- $name := include "name" . -}}
2 | {{- $chart := include "chart" . -}}
3 |
4 | {{- range $service, $val := $.Values.services }}
5 | {{- if not .disabled }}
6 | {{- $serviceName := printf "%s-%s" $name $service -}}
7 | apiVersion: apps/v1
8 | kind: Deployment
9 | metadata:
10 | name: {{ $serviceName }}
11 | namespace: {{ $.Values.namespace }}
12 | labels:
13 | app.kubernetes.io/name: {{ $serviceName }}
14 | app.kubernetes.io/instance: {{ $.Release.Name }}
15 | app.kubernetes.io/managed-by: {{ $.Release.Service }}
16 | app.kubernetes.io/version: {{ $.Values.version | quote }}
17 | app.kubernetes.io/component: {{ $service }}
18 | helm.sh/chart: {{ $chart }}
19 | {{- if .labels}}
20 | {{ toYaml .labels | nindent 4 }}
21 | {{- end }}
22 | spec:
23 | replicas: {{ default 1 .replicas}}
24 | selector:
25 | matchLabels:
26 | app.kubernetes.io/name: {{ $serviceName }}
27 | app.kubernetes.io/instance: {{ $.Release.Name }}
28 | template:
29 | metadata:
30 | labels:
31 | app.kubernetes.io/name: {{ $serviceName }}
32 | app.kubernetes.io/instance: {{ $.Release.Name }}
33 | app.kubernetes.io/version: {{ $.Values.version | quote }}
34 | app.kubernetes.io/component: {{ $service }}
35 | app.kubernetes.io/managed-by: {{ $.Release.Service }}
36 | helm.sh/chart: {{ $chart }}
37 | {{- if .labels}}
38 | {{ toYaml .labels | nindent 8 }}
39 | {{- end }}
40 | spec:
41 | {{- if .serviceAccountName }}
42 | serviceAccountName: {{ .serviceAccountName }}
43 | {{- end }}
44 | securityContext:
45 | runAsNonRoot: true
46 | seccompProfile:
47 | type: RuntimeDefault
48 | fsGroup: 1001
49 | containers:
50 | - name: {{ $service }}
51 | securityContext:
52 | allowPrivilegeEscalation: false
53 | runAsUser: 1001
54 | runAsNonRoot: true
55 | readOnlyRootFilesystem: true
56 | capabilities:
57 | drop:
58 | - ALL
59 | image: "{{ $.Values.image }}:{{ $.Values.version }}"
60 | {{- if .command }}
61 | command: [{{ .command }}]
62 | {{- if .args }}
63 | args: {{- range .args }}
64 | - {{.}}
65 | {{- end }}
66 | {{- end }}
67 | {{- end }}
68 | env:
69 | - name: NAME
70 | value: {{ $service }}
71 | - name: VERSION
72 | value: "{{ $.Values.version }}"
73 | - name: VERBOSITY
74 | value: {{ $.Values.verbosity }}
75 | - name: PROJECT_ID
76 | value: {{ $.Values.projectId }}
77 | {{ toYaml $.Values.environment | indent 12 }}
78 | {{- if .environment }}
79 | {{ toYaml .environment | indent 12 }}
80 | {{- end }}
81 |
82 | ports:
83 | - name: http
84 | containerPort: {{ $.Values.service.targetPort }}
85 | protocol: TCP
86 | {{- if .healthPath }}
87 | readinessProbe:
88 | httpGet:
89 | path: {{ .healthPath }}
90 | port: {{ $.Values.service.targetPort }}
91 | failureThreshold: 1
92 | initialDelaySeconds: 5
93 | periodSeconds: 5
94 | {{- end }}
95 | resources:
96 | limits:
97 | {{- toYaml .resources | nindent 14 }}
98 | requests:
99 | {{- toYaml .resources | nindent 14 }}
100 | ---
101 | {{ end }}
102 | {{ end }}
103 |
--------------------------------------------------------------------------------
/packages/sdk/src/codecs/index.ts:
--------------------------------------------------------------------------------
1 | import { Codec } from '../types.js';
2 |
3 | /**
4 | * JSON codec for serializing JavaScript objects
5 | */
6 | export class JsonCodec implements Codec {
7 | public readonly contentType = 'application/json';
8 |
9 | encode(data: unknown): Uint8Array {
10 | const jsonString = JSON.stringify(data);
11 | return new TextEncoder().encode(jsonString);
12 | }
13 |
14 | decode(data: Uint8Array): unknown {
15 | const jsonString = new TextDecoder().decode(data);
16 | return JSON.parse(jsonString);
17 | }
18 | }
19 |
20 | /**
21 | * Raw binary codec for direct byte data
22 | */
23 | export class RawCodec implements Codec {
24 | public readonly contentType = 'application/octet-stream';
25 |
26 | encode(data: unknown): Uint8Array {
27 | if (data instanceof Uint8Array) {
28 | return data;
29 | }
30 | if (data instanceof ArrayBuffer) {
31 | return new Uint8Array(data);
32 | }
33 | if (typeof data === 'string') {
34 | return new TextEncoder().encode(data);
35 | }
36 | throw new Error('Raw codec only supports Uint8Array, ArrayBuffer, or string data');
37 | }
38 |
39 | decode(data: Uint8Array): Uint8Array {
40 | return data;
41 | }
42 | }
43 |
44 | /**
45 | * Text codec for UTF-8 strings
46 | */
47 | export class TextCodec implements Codec {
48 | public readonly contentType = 'text/plain';
49 |
50 | encode(data: unknown): Uint8Array {
51 | if (typeof data !== 'string') {
52 | throw new Error('Text codec only supports string data');
53 | }
54 | return new TextEncoder().encode(data);
55 | }
56 |
57 | decode(data: Uint8Array): string {
58 | return new TextDecoder().decode(data);
59 | }
60 | }
61 |
62 | /**
63 | * Codec registry for managing different encoding formats
64 | */
65 | export class CodecRegistry {
66 | private codecs = new Map();
67 |
68 | constructor() {
69 | // Register default codecs
70 | this.register('json', new JsonCodec());
71 | this.register('raw', new RawCodec());
72 | this.register('text', new TextCodec());
73 | this.register('application/json', new JsonCodec());
74 | this.register('application/octet-stream', new RawCodec());
75 | this.register('text/plain', new TextCodec());
76 | }
77 |
78 | /**
79 | * Register a new codec
80 | */
81 | register(name: string, codec: Codec): void {
82 | this.codecs.set(name.toLowerCase(), codec);
83 | }
84 |
85 | /**
86 | * Get a codec by name
87 | */
88 | get(name: string): Codec {
89 | const codec = this.codecs.get(name.toLowerCase());
90 | if (!codec) {
91 | throw new Error(`Unknown codec: ${name}`);
92 | }
93 | return codec;
94 | }
95 |
96 | /**
97 | * Check if a codec exists
98 | */
99 | has(name: string): boolean {
100 | return this.codecs.has(name.toLowerCase());
101 | }
102 |
103 | /**
104 | * Get all registered codec names
105 | */
106 | getNames(): string[] {
107 | return Array.from(this.codecs.keys());
108 | }
109 |
110 | /**
111 | * Auto-detect codec from data type
112 | */
113 | detectCodec(data: unknown): string {
114 | if (typeof data === 'string') {
115 | return 'text';
116 | }
117 | if (data instanceof Uint8Array || data instanceof ArrayBuffer) {
118 | return 'raw';
119 | }
120 | if (typeof data === 'object' && data !== null) {
121 | return 'json';
122 | }
123 | return 'json'; // Default fallback
124 | }
125 | }
126 |
127 | // Export default registry instance
128 | export const defaultCodecRegistry = new CodecRegistry();
129 |
--------------------------------------------------------------------------------
/docs/contracts/README.md:
--------------------------------------------------------------------------------
1 | # Contracts Documentation
2 |
3 | Deployment and integration guide for @blobkit/contracts.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install @blobkit/contracts ethers
9 | ```
10 |
11 | ## Integration
12 |
13 | ```typescript
14 | import { ethers } from 'ethers';
15 | import { BlobKitEscrowABI, getContractAddress } from '@blobkit/contracts';
16 |
17 | const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_PROJECT_ID');
18 | const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
19 |
20 | const escrowAddress = getContractAddress(1);
21 | const escrow = new ethers.Contract(escrowAddress, BlobKitEscrowABI, signer);
22 |
23 | // Deposit for blob job
24 | const jobId = ethers.keccak256(ethers.toUtf8Bytes('unique-job-id'));
25 | const tx = await escrow.depositForBlob(jobId, { value: ethers.parseEther('0.01') });
26 | await tx.wait();
27 |
28 | // Check job status
29 | const job = await escrow.getJob(jobId);
30 | console.log('Job completed:', job.completed);
31 | ```
32 |
33 | ## Contract Functions
34 |
35 | ### User Functions
36 |
37 | - `depositForBlob(bytes32 jobId)` - Deposit payment for blob job
38 | - `refundExpiredJob(bytes32 jobId)` - Claim refund for expired jobs
39 |
40 | ### Proxy Functions
41 |
42 | - `completeJob(bytes32 jobId, bytes32 blobTxHash, bytes calldata proof)` - Complete job and claim payment
43 | - `setProxyFee(uint256 percent)` - Set proxy fee (0-10%)
44 |
45 | ### Owner Functions
46 |
47 | - `setProxyAuthorization(address proxy, bool authorized)` - Authorize/deauthorize proxies
48 | - `setJobTimeout(uint256 timeout)` - Configure job timeout
49 | - `pause()` / `unpause()` - Emergency controls
50 |
51 | ### View Functions
52 |
53 | - `getJob(bytes32 jobId)` - Get job details
54 | - `isJobExpired(bytes32 jobId)` - Check if job expired
55 | - `getJobTimeout()` - Get current timeout
56 |
57 | ## Events
58 |
59 | ```solidity
60 | event JobCreated(bytes32 indexed jobId, address indexed user, uint256 amount);
61 | event JobCompleted(bytes32 indexed jobId, bytes32 blobTxHash, uint256 proxyFee);
62 | event JobRefunded(bytes32 indexed jobId, string reason);
63 | event ProxyAuthorizationChanged(address indexed proxy, bool authorized);
64 | ```
65 |
66 | ## Development
67 |
68 | ```bash
69 | forge build # Build contracts
70 | forge test # Run tests
71 | forge test --coverage # Run with coverage
72 | forge fmt # Format code
73 | ```
74 |
75 | ## Deployment
76 |
77 | ### Local
78 |
79 | ```bash
80 | # Start local node
81 | anvil
82 |
83 | # Deploy to local network
84 | forge script script/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast
85 | ```
86 |
87 | ### Testnet
88 |
89 | ```bash
90 | forge script script/Deploy.s.sol \
91 | --rpc-url $SEPOLIA_RPC_URL \
92 | --private-key $PRIVATE_KEY \
93 | --broadcast \
94 | --verify
95 | ```
96 |
97 | ### Mainnet
98 |
99 | ```bash
100 | forge script script/Deploy.s.sol \
101 | --rpc-url $MAINNET_RPC_URL \
102 | --ledger \
103 | --broadcast \
104 | --verify
105 | ```
106 |
107 | ## Configuration
108 |
109 | ```bash
110 | # Environment variables for deployed contracts
111 | BLOBKIT_ESCROW_1=0x... # Mainnet
112 | BLOBKIT_ESCROW_11155111=0x... # Sepolia
113 | BLOBKIT_ESCROW_17000=0x... # Holesky
114 | ```
115 |
116 | ## Security Considerations
117 |
118 | - OpenZeppelin base contracts (Ownable, ReentrancyGuard, Pausable)
119 | - Custom errors for gas efficiency
120 | - Replay protection on job completion
121 | - Input validation on all public functions
122 | - Emergency pause functionality
123 |
124 | Contracts have not been audited. Use at your own risk.
125 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/routes/health.ts:
--------------------------------------------------------------------------------
1 | import { Router, Request, Response } from 'express';
2 | import { ethers } from 'ethers';
3 | import { HealthResponse, ProxyConfig } from '../types.js';
4 | import { createLogger } from '../utils/logger.js';
5 |
6 | const logger = createLogger('HealthRoute');
7 |
8 | /**
9 | * Creates health check router
10 | */
11 | export const createHealthRouter = (config: ProxyConfig, provider: ethers.Provider, signer: ethers.Signer) => {
12 | const router = Router();
13 | const startTime = Date.now();
14 |
15 |
16 | router.get('/health/details', async (req: Request, res: Response) => {
17 | try {
18 | const uptime = Math.floor((Date.now() - startTime) / 1000);
19 | let rpcHealthy = true;
20 |
21 | // Check blockchain connectivity
22 | let blocksLag: number | undefined;
23 | try {
24 | const latestBlock = await provider.getBlockNumber();
25 | const currentTime = Math.floor(Date.now() / 1000);
26 | const block = await provider.getBlock(latestBlock);
27 | if (block) {
28 | blocksLag = Math.max(0, Math.floor((currentTime - block.timestamp) / 12)); // Assuming 12s block time
29 | }
30 | } catch (error) {
31 | logger.warn('Failed to check blockchain connectivity:', error as Error);
32 | rpcHealthy = false;
33 | }
34 |
35 | // Check circuit breakers
36 | const response: HealthResponse = {
37 | status: 'healthy',
38 | version: '0.0.1',
39 | chainId: config.chainId,
40 | signer: await signer.getAddress(),
41 | escrowContract: config.escrowContract,
42 | proxyFeePercent: config.proxyFeePercent,
43 | maxBlobSize: config.maxBlobSize,
44 | uptime,
45 | blocksLag,
46 | rpcHealthy,
47 | };
48 |
49 | res.json(response);
50 | } catch (error) {
51 | logger.error('Health check failed:', error);
52 |
53 | const response: HealthResponse = {
54 | status: 'unhealthy',
55 | version: '0.0.1',
56 | chainId: config.chainId,
57 | signer: await signer.getAddress(),
58 | escrowContract: config.escrowContract,
59 | proxyFeePercent: config.proxyFeePercent,
60 | maxBlobSize: config.maxBlobSize,
61 | rpcHealthy: false,
62 | uptime: Math.floor((Date.now() - startTime) / 1000)
63 | };
64 |
65 | res.status(503).json(response);
66 | }
67 | });
68 |
69 | router.get('/health', async (req: Request, res: Response) => {
70 | try {
71 | const uptime = Math.floor((Date.now() - startTime) / 1000);
72 |
73 | const response: HealthResponse = {
74 | status: 'healthy',
75 | version: '0.0.1',
76 | chainId: config.chainId,
77 | signer: await signer.getAddress(),
78 | escrowContract: config.escrowContract,
79 | proxyFeePercent: config.proxyFeePercent,
80 | maxBlobSize: config.maxBlobSize,
81 | uptime,
82 | };
83 |
84 | res.json(response);
85 | } catch (error) {
86 | logger.error('Health check failed:', error);
87 |
88 | const response: HealthResponse = {
89 | status: 'unhealthy',
90 | version: '0.0.1',
91 | chainId: config.chainId,
92 | signer: await signer.getAddress(),
93 | escrowContract: config.escrowContract,
94 | proxyFeePercent: config.proxyFeePercent,
95 | maxBlobSize: config.maxBlobSize,
96 | uptime: Math.floor((Date.now() - startTime) / 1000)
97 | };
98 |
99 | res.status(503).json(response);
100 | }
101 | });
102 |
103 | return router;
104 | };
105 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/middleware/validation.ts:
--------------------------------------------------------------------------------
1 | import { body, validationResult } from 'express-validator';
2 | import { Request, Response, NextFunction } from 'express';
3 | import { ProxyError, ProxyErrorCode } from '../types.js';
4 |
5 | /**
6 | * Validation rules for blob write requests
7 | */
8 | export const validateBlobWrite = [
9 | body('jobId')
10 | .isString()
11 | .matches(/^0x[a-fA-F0-9]{64}$/)
12 | .withMessage('jobId must be a valid 32-byte hex hash (0x-prefixed)'),
13 |
14 | body('paymentTxHash')
15 | .isString()
16 | .matches(/^0x[a-fA-F0-9]{64}$/)
17 | .withMessage('paymentTxHash must be a valid transaction hash'),
18 |
19 | body('payload')
20 | .isString()
21 | .custom(value => {
22 | if (typeof value !== 'string') return false;
23 |
24 | // Validate base64 format
25 | const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
26 | if (!base64Regex.test(value)) return false;
27 |
28 | // Check decoded size
29 | try {
30 | const decoded = Buffer.from(value, 'base64');
31 | if (decoded.length === 0) return false;
32 | if (decoded.length > 131072) return false; // 128KB limit
33 | return true;
34 | } catch {
35 | return false;
36 | }
37 | })
38 | .withMessage('payload must be a valid base64 string encoding max 131072 bytes'),
39 |
40 | body('meta').isObject().withMessage('meta must be an object'),
41 |
42 | body('meta.appId')
43 | .isString()
44 | .isLength({ min: 1, max: 50 })
45 | .withMessage('meta.appId must be a non-empty string'),
46 |
47 | body('meta.codec')
48 | .isString()
49 | .isLength({ min: 1, max: 50 })
50 | .withMessage('meta.codec must be a non-empty string'),
51 |
52 | body('meta.contentHash')
53 | .optional()
54 | .isString()
55 | .matches(/^(0x)?[a-fA-F0-9]{64}$/)
56 | .withMessage('meta.contentHash must be a valid SHA-256 hash'),
57 |
58 | body('meta.ttlBlocks')
59 | .optional()
60 | .isInt({ min: 1, max: 1000000 })
61 | .withMessage('meta.ttlBlocks must be a positive integer'),
62 |
63 | body('meta.timestamp')
64 | .optional()
65 | .isInt({ min: 0 })
66 | .withMessage('meta.timestamp must be a non-negative integer'),
67 |
68 | body('meta.filename')
69 | .optional()
70 | .isString()
71 | .isLength({ max: 255 })
72 | .withMessage('meta.filename must be a string with max 255 characters'),
73 |
74 | body('meta.contentType')
75 | .optional()
76 | .isString()
77 | .isLength({ max: 100 })
78 | .withMessage('meta.contentType must be a string with max 100 characters'),
79 |
80 | body('meta.tags')
81 | .optional()
82 | .isArray()
83 | .custom(value => {
84 | if (!Array.isArray(value)) return false;
85 | if (value.length > 10) return false;
86 | return value.every(tag => typeof tag === 'string' && tag.length <= 50);
87 | })
88 | .withMessage('meta.tags must be an array of strings (max 10 items, 50 chars each)')
89 | ];
90 |
91 | /**
92 | * Middleware to handle validation errors
93 | */
94 | export const handleValidationErrors = (req: Request, res: Response, next: NextFunction) => {
95 | const errors = validationResult(req);
96 |
97 | if (!errors.isEmpty()) {
98 | const firstError = errors.array()[0];
99 | throw new ProxyError(
100 | ProxyErrorCode.INVALID_REQUEST,
101 | `Validation error: ${firstError.msg}`,
102 | 400,
103 | {
104 | field: 'path' in firstError ? firstError.path : 'unknown',
105 | value: 'value' in firstError ? firstError.value : 'unknown'
106 | }
107 | );
108 | }
109 |
110 | next();
111 | };
112 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import tsEslint from '@typescript-eslint/eslint-plugin';
3 | import tsParser from '@typescript-eslint/parser';
4 |
5 | export default [
6 | js.configs.recommended,
7 | {
8 | files: ['**/*.ts', '**/*.tsx', '**/*.js'],
9 | languageOptions: {
10 | parser: tsParser,
11 | parserOptions: {
12 | ecmaVersion: 2020,
13 | sourceType: 'module'
14 | },
15 | globals: {
16 | // Node.js globals
17 | global: 'readonly',
18 | process: 'readonly',
19 | Buffer: 'readonly',
20 | console: 'readonly',
21 | __dirname: 'readonly',
22 | __filename: 'readonly',
23 | require: 'readonly',
24 | module: 'readonly',
25 | exports: 'readonly',
26 |
27 | // Browser globals
28 | window: 'readonly',
29 | document: 'readonly',
30 | fetch: 'readonly',
31 | localStorage: 'readonly',
32 | sessionStorage: 'readonly',
33 | Location: 'readonly',
34 | navigator: 'readonly',
35 | HTMLElement: 'readonly',
36 | Event: 'readonly',
37 | EventTarget: 'readonly',
38 |
39 | // Common globals
40 | TextEncoder: 'readonly',
41 | TextDecoder: 'readonly',
42 | URL: 'readonly',
43 | URLSearchParams: 'readonly',
44 | AbortController: 'readonly',
45 | AbortSignal: 'readonly',
46 | crypto: 'readonly',
47 | globalThis: 'readonly',
48 | setTimeout: 'readonly',
49 | clearTimeout: 'readonly',
50 | setInterval: 'readonly',
51 | clearInterval: 'readonly'
52 | }
53 | },
54 | plugins: {
55 | '@typescript-eslint': tsEslint
56 | },
57 | rules: {
58 | ...tsEslint.configs.recommended.rules,
59 | 'no-undef': 'off', // TypeScript handles this
60 | '@typescript-eslint/explicit-function-return-type': 'off',
61 | '@typescript-eslint/no-explicit-any': 'error',
62 | '@typescript-eslint/no-non-null-assertion': 'warn',
63 | eqeqeq: ['error', 'always'],
64 | 'no-unused-vars': 'off',
65 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
66 | 'no-console': ['warn', { allow: ['warn', 'error'] }],
67 | 'prefer-const': 'error',
68 | 'no-var': 'error',
69 | 'object-shorthand': 'error',
70 | 'prefer-template': 'error',
71 | 'prefer-destructuring': ['error', { object: true, array: false }],
72 | 'no-eval': 'error',
73 | 'no-implied-eval': 'error',
74 | 'no-new-func': 'error',
75 | 'no-return-await': 'error',
76 | 'require-await': 'error',
77 | 'no-throw-literal': 'error',
78 | // Disabled to avoid typed-linting requirement across all packages
79 | '@typescript-eslint/prefer-optional-chain': 'off',
80 | '@typescript-eslint/prefer-nullish-coalescing': 'off'
81 | }
82 | },
83 | {
84 | files: ['**/*.test.ts', '**/*.spec.ts'],
85 | rules: {
86 | '@typescript-eslint/no-explicit-any': 'warn', // Allow in tests but warn
87 | 'no-console': 'off',
88 | 'require-await': 'off' // Allow async test functions without await
89 | }
90 | },
91 | {
92 | files: ['packages/contracts/**/*.ts'],
93 | rules: {
94 | 'no-console': 'off' // Contract scripts need console output
95 | }
96 | },
97 | {
98 | files: ['packages/proxy-server/src/cli.ts'],
99 | rules: {
100 | 'no-console': 'off' // CLI needs console output
101 | }
102 | },
103 | {
104 | files: ['scripts/**/*.js', 'scripts/**/*.ts'],
105 | rules: {
106 | 'no-console': 'off' // Scripts need console output
107 | }
108 | }
109 | ];
110 |
--------------------------------------------------------------------------------
/packages/proxy-server/README.md:
--------------------------------------------------------------------------------
1 | # @blobkit/proxy-server
2 |
3 | Express.js server that executes EIP-4844 blob transactions on behalf of browser clients after verifying escrow payments.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install @blobkit/proxy-server
9 | ```
10 |
11 | ## Requirements
12 |
13 | - Node.js v18 or later
14 | - Redis 6.0 or later (for persistent job queue)
15 |
16 | ## Usage
17 |
18 | ```bash
19 | # Using CLI
20 | npx blobkit-proxy --rpc-url $RPC_URL \
21 | --private-key $PRIVATE_KEY \
22 | --escrow-contract 0x1234567890123456789012345678901234567890
23 |
24 | # Using environment variables
25 | export RPC_URL=$MAINNET_RPC_URL
26 | export PRIVATE_KEY=$PRIVATE_KEY
27 | export ESCROW_CONTRACT=0x1234567890123456789012345678901234567890
28 | npx blobkit-proxy
29 | ```
30 |
31 | ## Configuration
32 |
33 | | Environment Variable | Required | Default | Description |
34 | | ------------------------ | -------- | ---------------------- | -------------------------------- |
35 | | `RPC_URL` | Yes | - | Ethereum RPC endpoint |
36 | | `ESCROW_CONTRACT` | Yes | - | Escrow contract address |
37 | | `REQUEST_SIGNING_SECRET` | Yes | - | HMAC secret (min 32 chars) |
38 | | `PRIVATE_KEY` | No\* | - | Private key for dev (see below) |
39 | | `AWS_KMS_KEY_ID` | No\* | - | AWS KMS key ARN for production |
40 | | `PORT` | No | 3000 | Server port |
41 | | `HOST` | No | 0.0.0.0 | Server host |
42 | | `CHAIN_ID` | No | 1 | Ethereum chain ID |
43 | | `PROXY_FEE_PERCENT` | No | 0 | Fee percentage (0-10) |
44 | | `LOG_LEVEL` | No | info | Log level: debug/info/warn/error |
45 | | `REDIS_URL` | No | redis://localhost:6379 | Redis connection URL |
46 |
47 | \*One key management option required (PRIVATE_KEY for development, AWS_KMS_KEY_ID for production)
48 |
49 |
50 | The trusted setup file should be in the standard Ethereum KZG ceremony format. Download from [ceremony.ethereum.org](https://ceremony.ethereum.org/).
51 |
52 | ## Redis Persistence
53 |
54 | The proxy server uses Redis for persistent job queue management. This ensures that pending job completions survive server restarts.
55 |
56 | ### Redis Setup
57 |
58 | ```bash
59 | # Using Docker
60 | docker run -d -p 6379:6379 redis:7-alpine
61 |
62 | # Using Homebrew (macOS)
63 | brew install redis
64 | brew services start redis
65 |
66 | # Using apt (Ubuntu/Debian)
67 | sudo apt update
68 | sudo apt install redis-server
69 | sudo systemctl start redis-server
70 | ```
71 |
72 | ### Redis Configuration
73 |
74 | The server connects to Redis using the `REDIS_URL` environment variable. Examples:
75 |
76 | ```bash
77 | # Local Redis
78 | REDIS_URL=redis://localhost:6379
79 |
80 | # Redis with authentication
81 | REDIS_URL=redis://username:password@hostname:6379
82 |
83 | # Redis with TLS
84 | REDIS_URL=rediss://hostname:6380
85 | ```
86 |
87 | ## Testing and Development
88 |
89 | ```bash
90 | npm test # Run tests
91 | npm run test:coverage # Run tests with coverage
92 | npm run build # Build for production
93 | npm run dev # Start development server
94 | npm run lint # Lint code
95 | npm run type-check # TypeScript checking
96 | ```
97 |
98 | ## Documentation
99 |
100 | See [/docs/proxy/](../../docs/proxy/) for deployment guides and API documentation.
101 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # BlobKit Documentation
2 |
3 | ## Production Status
4 |
5 | **BlobKit is live on Ethereum mainnet!**
6 |
7 | - **Escrow Contract:** [`0x2e8e414bc5c6B0b8339853CEDf965B4A28FB4838`](https://etherscan.io/address/0x2e8e414bc5c6B0b8339853CEDf965B4A28FB4838)
8 | - **Status:** ✅ Fully operational
9 | - **Tested:** Complete end-to-end verification on mainnet
10 |
11 | ## Documentation Index
12 |
13 | ### Getting Started
14 |
15 | - [Quick Start](quick-start.md) - Development setup and basics
16 | - [Infrastructure Requirements](infrastructure.md) - RPC setup for blob support
17 | - [Architecture Overview](architecture.md) - System design and components
18 |
19 | ### Deployment & Operations
20 |
21 | - [Deployment Guide](deployment-guide.md) - Deploy contracts to mainnet and production hardening
22 |
23 | ### Component Documentation
24 |
25 | - [SDK Documentation](sdk/README.md) - TypeScript SDK reference
26 | - [Proxy Server](proxy/README.md) - Proxy server setup and API
27 | - [Smart Contracts](contracts/README.md) - Escrow contract documentation
28 |
29 | ### Advanced Topics
30 |
31 | - [Secure Deployment](secure-deployment.md) - Security best practices
32 | - [Distributed Tracing](distributed-tracing.md) - Monitoring and observability
33 | - [Contributing](../CONTRIBUTING.md) - How to develop and contribute
34 | - [Security Policy](../SECURITY.md) - Vulnerability reporting and policy
35 |
36 | ## Quick Start
37 |
38 | ### 1. Choose Your RPC
39 |
40 | **Important:** Standard RPCs (Alchemy, Infura) don't support blobs. Use:
41 |
42 | ```typescript
43 | // Recommended: Flashbots (FREE)
44 | const RPC = process.env.BLOBKIT_RPC_URL!;
45 | ```
46 |
47 | ### 2. Install SDK
48 |
49 | ```bash
50 | npm install @blobkit/sdk
51 | ```
52 |
53 | ### 3. Write Your First Blob
54 |
55 | ```typescript
56 | import { BlobKit } from '@blobkit/sdk';
57 | import { Wallet } from 'ethers';
58 |
59 | const signer = new Wallet(process.env.PRIVATE_KEY);
60 | const blobkit = await BlobKit.init(
61 | {
62 | rpcUrl: 'https://rpc.flashbots.net', // Must use blob-compatible RPC
63 | chainId: 1
64 | },
65 | signer
66 | );
67 |
68 | const data = Buffer.from('Hello, Ethereum blobs!');
69 | const receipt = await blobkit.writeBlob(data);
70 | console.log(`Blob stored: ${receipt.blobTxHash}`);
71 | ```
72 |
73 | ## Mainnet Verification
74 |
75 | The system has been fully tested on mainnet with real transactions:
76 |
77 | | Test | Transaction | Result |
78 | | -------------- | ------------------------------------------------------------------------------------------------------------ | ---------- |
79 | | Job Creation | [`0x2d968d9...`](https://etherscan.io/tx/0x2d968d9cd4869b53a78c77ce2daad71e1935753ad7bbcfdcac472d93bf5dbade) | ✅ Success |
80 | | Job Completion | [`0x3c05906...`](https://etherscan.io/tx/0x3c05906995b76d5625b84f7020f225b67084ae844a2ba4b06a9ca68af1514213) | ✅ Success |
81 |
82 | ## Architecture
83 |
84 | ```
85 | User → SDK → Escrow Contract → Proxy Server → Blob Transaction
86 | ↓ ↓ ↓
87 | Payment Held Verify Payment Submit to L1
88 | ↓ ↓ ↓
89 | Timeout Refund Claim Payment Store Blob
90 | ```
91 |
92 | ## Key Insights
93 |
94 | 1. **RPC Choice Matters:** Must use blob-compatible RPC (Flashbots recommended)
95 | 2. **Cost Efficient:** Blobs cost ~$0.01 vs ~$50 for equivalent calldata
96 | 3. **Production Ready:** All components tested and verified on mainnet
97 | 4. **Trust Minimized:** Escrow ensures payment security
98 |
99 | ## Support
100 |
101 | - **GitHub Issues:** [Report issues](https://github.com/blobkit/blobkit/issues)
102 | - **Email:** zcole@linux.com
103 |
104 | ## License
105 |
106 | MIT - See [LICENSE](../LICENSE) for details.
107 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Proxy Server Types
3 | * TypeScript interfaces for the BlobKit proxy server
4 | */
5 |
6 | /**
7 | * Proxy server configuration
8 | */
9 | export interface ProxyConfig {
10 | port: number;
11 | host: string;
12 | rpcUrl: string;
13 | chainId: number;
14 | escrowContract: string;
15 | privateKey: string;
16 | proxyFeePercent: number;
17 | maxBlobSize: number;
18 | rateLimitRequests: number;
19 | rateLimitWindow: number;
20 | jobTimeout: number;
21 | logLevel: 'debug' | 'info' | 'warn' | 'error';
22 | httpProxyCount: number;
23 | }
24 |
25 | /**
26 | * Blob write request body
27 | */
28 | export interface BlobWriteRequest {
29 | jobId: string;
30 | paymentTxHash: string;
31 | payload: string; // Base64 encoded binary data
32 | signature: string; // Base64 encoded signature of the payload
33 | meta: {
34 | appId: string;
35 | codec: string;
36 | contentHash?: string;
37 | ttlBlocks?: number;
38 | timestamp?: number;
39 | filename?: string;
40 | contentType?: string;
41 | tags?: string[];
42 | callbackUrl?: string;
43 | };
44 | }
45 |
46 | /**
47 | * Blob write response
48 | */
49 | export interface BlobWriteResponse {
50 | success: true;
51 | blobTxHash: string;
52 | blockNumber: number;
53 | blobHash: string;
54 | commitment: string;
55 | proofs: string[];
56 | blobIndex: number;
57 | completionTxHash: string;
58 | jobId: string;
59 | }
60 |
61 | /**
62 | * Health check response
63 | */
64 | export interface HealthResponse {
65 | status: 'healthy' | 'unhealthy' | 'degraded';
66 | version: string;
67 | chainId: number;
68 | escrowContract: string;
69 | proxyFeePercent: number;
70 | maxBlobSize: number;
71 | uptime: number;
72 | signer: string;
73 | blocksLag?: number;
74 | rpcHealthy?: boolean;
75 | }
76 |
77 | /**
78 | * Standardized error response
79 | */
80 | export interface ErrorResponse {
81 | error: string;
82 | message: string;
83 | details?: Record;
84 | }
85 |
86 | /**
87 | * Job verification result
88 | */
89 | export interface JobVerification {
90 | valid: boolean;
91 | exists: boolean;
92 | user: string;
93 | amount: string;
94 | completed: boolean;
95 | timestamp: number;
96 | paymentTxHash?: string;
97 | }
98 |
99 | /**
100 | * Rate limit info
101 | */
102 | export interface RateLimitInfo {
103 | limit: number;
104 | remaining: number;
105 | resetTime: number;
106 | }
107 |
108 | /**
109 | * Proxy error codes
110 | */
111 | export enum ProxyErrorCode {
112 | INVALID_REQUEST = 'INVALID_REQUEST',
113 | PAYMENT_INVALID = 'PAYMENT_INVALID',
114 | PAYMENT_NOT_FOUND = 'PAYMENT_NOT_FOUND',
115 | JOB_ALREADY_COMPLETED = 'JOB_ALREADY_COMPLETED',
116 | JOB_EXPIRED = 'JOB_EXPIRED',
117 | BLOB_TOO_LARGE = 'BLOB_TOO_LARGE',
118 | BLOB_EXECUTION_FAILED = 'BLOB_EXECUTION_FAILED',
119 | RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
120 | NETWORK_ERROR = 'NETWORK_ERROR',
121 | INTERNAL_ERROR = 'INTERNAL_ERROR',
122 | CONTRACT_ERROR = 'CONTRACT_ERROR',
123 | TRANSACTION_FAILED = 'TRANSACTION_FAILED',
124 | JOB_LOCKED = 'JOB_LOCKED',
125 | SIGNATURE_INVALID = 'SIGNATURE_INVALID'
126 | }
127 |
128 | /**
129 | * Custom proxy error class
130 | */
131 | export class ProxyError extends Error {
132 | public readonly code: ProxyErrorCode;
133 | public readonly statusCode: number;
134 | public readonly details?: Record;
135 |
136 | constructor(
137 | code: ProxyErrorCode,
138 | message: string,
139 | statusCode: number = 400,
140 | details?: Record
141 | ) {
142 | super(message);
143 | this.name = 'ProxyError';
144 | this.code = code;
145 | this.statusCode = statusCode;
146 | this.details = details;
147 | }
148 | }
149 |
150 | /**
151 | * Blob execution job
152 | */
153 | export interface BlobJob {
154 | jobId: string;
155 | user: string;
156 | paymentTxHash: string;
157 | payload: Uint8Array;
158 | meta: Record;
159 | timestamp: number;
160 | retryCount: number;
161 | }
162 |
--------------------------------------------------------------------------------
/docs/quick-start.md:
--------------------------------------------------------------------------------
1 | # BlobKit Quick Start
2 |
3 | ## ⚡ 5-Minute Setup
4 |
5 | ### 1. Install
6 |
7 | ```bash
8 | npm install @blobkit/sdk
9 | ```
10 |
11 | ### 2. Configure (IMPORTANT: Use Flashbots RPC)
12 |
13 | ```typescript
14 | import { BlobKit } from '@blobkit/sdk';
15 | import { Wallet } from 'ethers';
16 |
17 | const signer = new Wallet(process.env.PRIVATE_KEY);
18 | const blobkit = await BlobKit.init(
19 | {
20 | rpcUrl: 'https://rpc.flashbots.net', // ← MUST use blob-compatible RPC!
21 | chainId: 1 // Mainnet
22 | },
23 | signer
24 | );
25 | ```
26 |
27 | ### 3. Write Blob
28 |
29 | ```typescript
30 | const data = Buffer.from('Hello, blobs!');
31 | const receipt = await blobkit.writeBlob(data);
32 | console.log(`Stored at: ${receipt.blobTxHash}`);
33 | ```
34 |
35 | ### 4. Read Blob
36 |
37 | ```typescript
38 | const blob = await blobkit.readBlob(receipt.blobTxHash);
39 | console.log('Data:', blob.data);
40 | ```
41 |
42 | ## Critical: RPC Requirements
43 |
44 | **Standard RPCs (Alchemy, Infura) DO NOT support blob transactions!**
45 |
46 | ### ✅ Working RPCs:
47 |
48 | - **Flashbots (FREE):** `https://rpc.flashbots.net`
49 | - **Your own node:** Geth + Lighthouse/Prysm
50 | - **Some MEV relays:** Check documentation
51 |
52 | ### ❌ NOT Working:
53 |
54 | - ❌ `https://eth-mainnet.g.alchemy.com/...`
55 | - ❌ `https://mainnet.infura.io/...`
56 | - ❌ Most standard RPC providers
57 |
58 | ## What's Deployed
59 |
60 | | Component | Mainnet Address | Status |
61 | | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------- |
62 | | **Escrow Contract** | [`0x2e8e414bc5c6B0b8339853CEDf965B4A28FB4838`](https://etherscan.io/address/0x2e8e414bc5c6B0b8339853CEDf965B4A28FB4838) | ✅ Live |
63 |
64 | ## Costs
65 |
66 | - **Blob storage:** ~$0.01-0.10 per blob (131KB)
67 | - **Calldata equivalent:** ~$50-100 (100x more expensive!)
68 | - **Gas for operations:** ~$0.50-2.00
69 |
70 | ## Environment Variables
71 |
72 | ```bash
73 | # .env
74 | PRIVATE_KEY=0x...
75 | BLOBKIT_RPC_URL=https://rpc.flashbots.net # Use blob-compatible RPC!
76 | BLOBKIT_ESCROW_1=0x2e8e414bc5c6B0b8339853CEDf965B4A28FB4838
77 | ```
78 |
79 | ## Complete Example
80 |
81 | ```typescript
82 | import { BlobKit } from '@blobkit/sdk';
83 | import { Wallet } from 'ethers';
84 | import dotenv from 'dotenv';
85 |
86 | dotenv.config();
87 |
88 | async function main() {
89 | // 1. Initialize
90 | const signer = new Wallet(process.env.PRIVATE_KEY!);
91 | const blobkit = await BlobKit.init(
92 | {
93 | rpcUrl: 'https://rpc.flashbots.net', // MUST use this or similar!
94 | chainId: 1
95 | },
96 | signer
97 | );
98 |
99 | // 2. Estimate cost
100 | const data = Buffer.from('Hello, Ethereum blobs!');
101 | const estimate = await blobkit.estimateCost(data);
102 | console.log(`Cost: ${estimate.totalETH} ETH`);
103 |
104 | // 3. Write blob
105 | const receipt = await blobkit.writeBlob(data, {
106 | appId: 'my-app',
107 | codec: 'text/plain'
108 | });
109 | console.log(`Blob TX: ${receipt.blobTxHash}`);
110 |
111 | // 4. Read blob
112 | const blob = await blobkit.readBlob(receipt.blobTxHash);
113 | const text = Buffer.from(blob.data).toString();
114 | console.log(`Retrieved: ${text}`);
115 | }
116 |
117 | main().catch(console.error);
118 | ```
119 |
120 | ## ❓ FAQ
121 |
122 | **Q: Why does my transaction fail with Alchemy/Infura?**
123 | A: They don't support blob transactions. Use Flashbots RPC.
124 |
125 | **Q: How much does it cost?**
126 | A: ~$0.01-0.10 per 131KB blob (vs ~$50+ for calldata).
127 |
128 | **Q: Is it production ready?**
129 | A: Yes! Fully deployed and tested on mainnet.
130 |
131 | **Q: Can I use it in the browser?**
132 | A: Yes, with a proxy server (see docs).
133 |
134 | ## 🆘 Help
135 |
136 | - **Docs:** [Full Documentation](docs/README.md)
137 | - **Issues:** [GitHub Issues](https://github.com/blobkit/blobkit/issues)
138 | - **RPC Setup:** [Infrastructure Guide](docs/infrastructure.md)
139 |
140 | ## ⚠️ Remember
141 |
142 | **Always use a blob-compatible RPC like Flashbots (`https://rpc.flashbots.net`)!**
143 |
144 | Standard RPCs will NOT work for blob transactions.
145 |
--------------------------------------------------------------------------------
/docs/secure-deployment.md:
--------------------------------------------------------------------------------
1 | # Secure Deployment Guide
2 |
3 | This guide covers secure deployment practices for the BlobKit proxy server in production environments.
4 |
5 | ## Key Management
6 |
7 | The proxy server requires a private key to sign blob transactions. **Never store private keys in plain text in production.**
8 |
9 | ### Supported Key Management Options
10 |
11 | #### 1. AWS Key Management Service (KMS)
12 |
13 | **Recommended for AWS deployments**
14 |
15 | ```bash
16 | # Set up AWS KMS key
17 | export AWS_KMS_KEY_ID="arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
18 | export AWS_REGION="us-east-1"
19 |
20 | # The proxy will automatically use KMS
21 | npm start
22 | ```
23 |
24 | **Prerequisites:**
25 |
26 | - Install AWS SDK: `npm install @aws-sdk/client-kms`
27 | - Configure AWS credentials (IAM role recommended)
28 | - Create an asymmetric ECC_SECG_P256K1 key in KMS
29 | - Grant permissions to use the key
30 |
31 | **IAM Policy:**
32 |
33 | ```json
34 | {
35 | "Version": "2012-10-17",
36 | "Statement": [
37 | {
38 | "Effect": "Allow",
39 | "Action": ["kms:Sign", "kms:GetPublicKey"],
40 | "Resource": "arn:aws:kms:region:account:key/key-id"
41 | }
42 | ]
43 | }
44 | ```
45 |
46 | #### 2. Environment Variable (Development Only)
47 |
48 | **For development and testing only**
49 |
50 | ```bash
51 | export PRIVATE_KEY="0x..."
52 | ```
53 |
54 | ## Network Security
55 |
56 | ### 1. TLS/HTTPS
57 |
58 | Always use HTTPS in production:
59 |
60 | ```nginx
61 | server {
62 | listen 443 ssl http2;
63 | server_name proxy.example.com;
64 |
65 | ssl_certificate /etc/nginx/ssl/cert.pem;
66 | ssl_certificate_key /etc/nginx/ssl/key.pem;
67 |
68 | location / {
69 | proxy_pass http://localhost:3000;
70 | proxy_set_header X-Real-IP $remote_addr;
71 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
72 | }
73 | }
74 | ```
75 |
76 | ### 2. Rate Limiting
77 |
78 | Configure rate limits via environment variables:
79 |
80 | ```bash
81 | RATE_LIMIT_REQUESTS=100
82 | RATE_LIMIT_WINDOW_MS=60000 # 1 minute
83 | ```
84 |
85 |
86 | ## Monitoring
87 |
88 | ### Prometheus Metrics
89 |
90 | The proxy exposes metrics at `/metrics`:
91 |
92 | ```yaml
93 | # prometheus.yml
94 | scrape_configs:
95 | - job_name: 'blobkit-proxy'
96 | static_configs:
97 | - targets: ['proxy.example.com:3000']
98 | ```
99 |
100 | Key metrics to monitor:
101 |
102 | - `blobkit_blob_submissions_total` - Submission success/failure rates
103 | - `blobkit_errors_total` - Error counts by type
104 | - `blobkit_job_processing_duration_seconds` - Performance metrics
105 |
106 | ### Logging
107 |
108 | Configure structured JSON logging:
109 |
110 | ```bash
111 | LOG_LEVEL=info # debug, info, warn, error
112 | NODE_ENV=production
113 | ```
114 |
115 | ## Docker Deployment
116 |
117 | Use the provided Dockerfile:
118 |
119 | ```bash
120 | # Build
121 | docker build -t blobkit-proxy .
122 |
123 | # Run with environment file
124 | docker run -d \
125 | --name blobkit-proxy \
126 | --env-file .env.production \
127 | -p 3000:3000 \
128 | blobkit-proxy
129 | ```
130 |
131 | ## Health Checks
132 |
133 | Configure health check endpoints:
134 |
135 | - `/api/v1/health` - Basic health status
136 | - `/api/v1/health/detailed` - Detailed system status
137 |
138 | ```bash
139 | # Kubernetes example
140 | livenessProbe:
141 | httpGet:
142 | path: /api/v1/health
143 | port: 3000
144 | initialDelaySeconds: 30
145 | periodSeconds: 10
146 | ```
147 |
148 | ## Environment Variables
149 |
150 | Required for production:
151 |
152 | ```bash
153 | # Network
154 | RPC_URL=$MAINNET_RPC_URL
155 | CHAIN_ID=1
156 |
157 | # Security
158 | AWS_KMS_KEY_ID=arn:aws:kms:... # OR use PRIVATE_KEY for dev
159 |
160 | # Redis
161 | REDIS_URL=redis://redis.example.com:6379
162 |
163 | # Contracts
164 | ESCROW_CONTRACT=0x...
165 |
166 | # Monitoring
167 | LOG_LEVEL=info
168 | METRICS_ENABLED=true
169 | ```
170 |
171 | ## Security Checklist
172 |
173 | - [ ] Private key secured in KMS (not environment variable)
174 | - [ ] HTTPS/TLS enabled
175 | - [ ] Request signing configured
176 | - [ ] Rate limiting enabled
177 | - [ ] Monitoring and alerting set up
178 | - [ ] Regular security updates applied
179 | - [ ] Access logs enabled
180 | - [ ] Firewall rules configured
181 | - [ ] Redis password protected
182 | - [ ] No default or test values in production
183 |
--------------------------------------------------------------------------------
/packages/sdk/src/blob-reader.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { BlobMeta, BlobReadResult, BlobKitError, BlobKitConfig, BlobKitErrorCode } from './types';
3 | import { decodeBlob } from './kzg';
4 | import { defaultCodecRegistry } from './codecs';
5 | import { hexToBytes } from './utils';
6 |
7 | export class BlobReader {
8 | private provider: ethers.JsonRpcProvider;
9 | private archiveUrl?: string;
10 |
11 | constructor(config: BlobKitConfig) {
12 | this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
13 | this.archiveUrl = config.archiveUrl;
14 | }
15 |
16 | async readBlob(blobHashOrTxHash: string, index: number = 0): Promise {
17 | return this.getBlobData(blobHashOrTxHash, index);
18 | }
19 |
20 | async readBlobWithMeta(blobHashOrTxHash: string, index: number = 0): Promise {
21 | return this.getBlobData(blobHashOrTxHash, index);
22 | }
23 |
24 | private async getBlobData(input: string, index: number): Promise {
25 | if (input.startsWith('0x01')) {
26 | return this.getBlobByHash(input);
27 | } else {
28 | return this.getBlobByTxHash(input, index);
29 | }
30 | }
31 |
32 | private async getBlobByTxHash(txHash: string, index: number): Promise {
33 | const tx = await this.provider.getTransaction(txHash);
34 | if (!tx) {
35 | throw new BlobKitError(BlobKitErrorCode.BLOB_NOT_FOUND, `Transaction not found: ${txHash}`);
36 | }
37 |
38 | const blobHashes = (tx as any).blobVersionedHashes;
39 | if (!blobHashes?.length) {
40 | throw new BlobKitError(BlobKitErrorCode.BLOB_NOT_FOUND, 'No blobs found in transaction');
41 | }
42 |
43 | if (index < 0 || index >= blobHashes.length) {
44 | throw new BlobKitError(BlobKitErrorCode.BLOB_NOT_FOUND, `Invalid blob index: ${index}. Transaction has ${blobHashes.length} blobs.`);
45 | }
46 |
47 | const result = await this.getBlobByHash(blobHashes[index]);
48 | result.blobIndex = index;
49 | return result;
50 | }
51 |
52 |
53 | private async getBlobByHash(blobHash: string): Promise {
54 | let blob: Uint8Array | null = null;
55 | let source: 'rpc' | 'archive' | 'fallback' = 'rpc';
56 |
57 | if (this.archiveUrl) {
58 | blob = await this.fetchBlobFromArchive(blobHash);
59 | if (blob) {
60 | source = 'archive';
61 | }
62 | }
63 |
64 | if (!blob) {
65 | blob = await this.fetchBlobFromNode(blobHash);
66 | if (blob) {
67 | source = 'rpc';
68 | }
69 | }
70 |
71 | if (!blob) {
72 | throw new BlobKitError(BlobKitErrorCode.BLOB_NOT_FOUND, `Blob not found: ${blobHash}`);
73 | }
74 |
75 | const blobData = decodeBlob(blob);
76 | return {
77 | data: blobData,
78 | blobIndex: 0,
79 | source: source
80 | };
81 | }
82 |
83 | private async fetchBlobFromNode(blobHash: string): Promise {
84 | try {
85 | const result = await this.provider.send('eth_getBlobSidecars', [blobHash]);
86 | if (result?.[0]?.blob) {
87 | return ethers.getBytes(result[0].blob);
88 | }
89 | } catch (error) {
90 | // Silently handle error - blob not available from node
91 | }
92 | return null;
93 | }
94 |
95 | private async fetchBlobFromArchive(blobHash: string): Promise {
96 | try {
97 | const endpointUrl = `${this.archiveUrl}/blobs/${blobHash}/data`;
98 | const response = await fetch(endpointUrl);
99 | if (response.ok) {
100 | let dataHex = await response.text();
101 | if (dataHex[0] === '"') {
102 | dataHex = dataHex.slice(1, -1);
103 | }
104 | return hexToBytes(dataHex);
105 | }
106 | } catch (error) {
107 | // Archive fetch failed - silently handle
108 | }
109 | return null;
110 | }
111 | static decodeToString(data: Uint8Array): string {
112 | // Remove trailing zeros (blob padding)
113 | let end = data.length;
114 | while (end > 0 && data[end - 1] === 0) {
115 | end--;
116 | }
117 | const trimmed = data.slice(0, end);
118 | return new TextDecoder().decode(trimmed);
119 | }
120 |
121 | /**
122 | * Decode blob data to JSON object
123 | */
124 | static decodeToJSON(data: Uint8Array): unknown {
125 | const str = BlobReader.decodeToString(data);
126 | return JSON.parse(str);
127 | }
128 | }
--------------------------------------------------------------------------------
/docs/deployment-guide.md:
--------------------------------------------------------------------------------
1 | # BlobKit Deployment Guide
2 |
3 | ## Prerequisites
4 |
5 | Before deploying to mainnet, ensure you have:
6 |
7 | 1. **Wallet with ETH** - At least 0.1 ETH for deployment and gas
8 | 2. **Private key** - Securely stored, never commit to git
9 | 3. **Contract compiled** - Run `npm run build:contracts`
10 | 4. **Tests passing** - All contract tests must pass
11 |
12 | ## Step 1: Prepare Environment
13 |
14 | ```bash
15 | # Add to .env file (NEVER commit this!)
16 | DEPLOYER_PRIVATE_KEY=0x_your_private_key_here
17 | ```
18 |
19 | ## Step 2: Compile Contracts
20 |
21 | ```bash
22 | cd packages/contracts
23 | npm run build
24 | npm test
25 | ```
26 |
27 | ## Step 3: Review Deployment Script
28 |
29 | Check `/scripts/deploy-mainnet.ts` and update:
30 |
31 | - Proxy addresses to authorize
32 | - Job timeout (default 5 minutes)
33 | - Proxy fee percentage (default 2%)
34 |
35 | ## Step 4: Deploy Contract
36 |
37 | ```bash
38 | # From project root
39 | npx ts-node scripts/deploy-mainnet.ts
40 | ```
41 |
42 | The script will:
43 |
44 | 1. Connect to mainnet via your configured RPC (Flashbots recommended)
45 | 2. Check wallet balance
46 | 3. Verify gas prices are reasonable
47 | 4. Estimate deployment cost
48 | 5. Request explicit confirmation
49 | 6. Deploy the contract
50 | 7. Save deployment info to `/deployments/mainnet.json`
51 |
52 | ## Step 5: Verify on Etherscan
53 |
54 | ```bash
55 | # Get your Etherscan API key from https://etherscan.io/apis
56 | export ETHERSCAN_API_KEY=your_api_key
57 |
58 | # Verify contract
59 | npx hardhat verify --network mainnet CONTRACT_ADDRESS
60 | ```
61 |
62 | ## Step 6: Update Configuration
63 |
64 | Add to `.env`:
65 |
66 | ```bash
67 | BLOBKIT_ESCROW_1=0x_deployed_contract_address
68 | ```
69 |
70 | The SDK reads escrow addresses from environment variables; no code changes required.
71 |
72 | ## Step 7: Deploy Proxy Servers
73 |
74 | For each proxy server:
75 |
76 | 1. Deploy proxy infrastructure
77 | 2. Configure with escrow contract address
78 | 3. Authorize proxy in escrow contract (if not done during deployment)
79 |
80 | ## Step 8: Test Deployment
81 |
82 | ```bash
83 | # Run mainnet integration test
84 | node test-mainnet-production.mjs
85 | ```
86 |
87 | ## Security Checklist (Production)
88 |
89 | - [ ] Private key secured (use hardware wallet for production)
90 | - [ ] Contract code audited
91 | - [ ] Deployment script reviewed
92 | - [ ] Gas price reasonable (<100 gwei)
93 | - [ ] Contract verified on Etherscan
94 | - [ ] Access controls configured
95 | - [ ] Emergency pause tested
96 | - [ ] Proxy authorizations correct
97 | - [ ] Small test transactions successful
98 |
99 | ## Estimated Costs
100 |
101 | | Operation | Gas | Cost @ 30 gwei |
102 | | --------------- | ---------- | -------------- |
103 | | Contract Deploy | ~2,000,000 | ~0.06 ETH |
104 | | Authorize Proxy | ~50,000 | ~0.0015 ETH |
105 | | Set Timeout | ~30,000 | ~0.0009 ETH |
106 | | **Total** | ~2,100,000 | ~0.063 ETH |
107 |
108 | ## Emergency Procedures
109 |
110 | ### If Deployment Fails
111 |
112 | 1. Check transaction on Etherscan
113 | 2. Verify wallet has sufficient ETH
114 | 3. Increase gas price if needed
115 | 4. Retry with higher gas limit
116 |
117 | ### After Deployment
118 |
119 | 1. Save deployment info immediately
120 | 2. Transfer ownership to multisig if required
121 | 3. Monitor contract for first transactions
122 | 4. Set up monitoring alerts
123 |
124 | ## Contract Addresses
125 |
126 | | Network | Address | Block |
127 | | ------- | ------------------------------------------ | ----- |
128 | | Mainnet | (To be deployed) | - |
129 | | Sepolia | 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD77 | - |
130 | | Holesky | (To be deployed) | - |
131 |
132 | ## Support
133 |
134 | For deployment issues:
135 |
136 | - Check logs in `/deployments/mainnet.json`
137 | - Review transaction on Etherscan
138 | - Contact team for assistance
139 |
140 | ## Important Notes
141 |
142 | 1. **NEVER** commit private keys to git
143 | 2. **ALWAYS** test on testnet first
144 | 3. **VERIFY** contract on Etherscan
145 | 4. **MONITOR** first transactions carefully
146 | 5. **BACKUP** deployment information
147 |
148 | ## Post-Deployment
149 |
150 | After successful deployment:
151 |
152 | 1. Update documentation with contract address
153 | 2. Configure monitoring and alerts
154 | 3. Test with small amounts first
155 | 4. Gradually increase usage
156 | 5. Monitor gas costs and optimize if needed
157 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/services/job-cache.ts:
--------------------------------------------------------------------------------
1 | import { createClient, RedisClientType } from 'redis';
2 | import { BlobWriteResponse } from '../types.js';
3 | import { createLogger } from '../utils/logger.js';
4 |
5 | const logger = createLogger('JobResultCache');
6 |
7 | /**
8 | * Redis-based cache for BlobWriteResponse objects using jobId as key with locking support
9 | */
10 | export class JobCache {
11 | private redis: RedisClientType;
12 | private isConnected = false;
13 | private readonly keyPrefix = 'blobkit:job_result:';
14 | private readonly lockPrefix = 'blobkit:job_lock:';
15 | private readonly defaultTtl: number = 86400; // 24 hours in seconds
16 |
17 | constructor(
18 | private redisUrl: string = process.env.REDIS_URL || 'redis://localhost:6379',
19 | private chainId: string = process.env.CHAIN_ID || '1',
20 | private ttl: number = 86400
21 | ) {
22 | this.redis = createClient({ url: this.redisUrl });
23 | this.defaultTtl = this.ttl;
24 |
25 | this.redis.on('error', (err: Error) => {
26 | logger.error('Redis connection error:', err);
27 | this.isConnected = false;
28 | });
29 |
30 | this.redis.on('connect', () => {
31 | logger.info('JobCache Redis connected');
32 | this.isConnected = true;
33 | });
34 | }
35 |
36 | async connect(): Promise {
37 | if (!this.isConnected) {
38 | await this.redis.connect();
39 | }
40 | }
41 |
42 | async disconnect(): Promise {
43 | if (this.isConnected) {
44 | await this.redis.quit();
45 | this.isConnected = false;
46 | }
47 | }
48 |
49 | /**
50 | * Store a BlobWriteResponse in cache
51 | */
52 | async set(jobId: string, response: BlobWriteResponse): Promise {
53 | try {
54 | const key = this.getKey(jobId);
55 | const value = JSON.stringify(response);
56 | await this.redis.setEx(key, this.defaultTtl, value);
57 | logger.debug(`Cached result for job ${jobId}`);
58 | } catch (error) {
59 | logger.error(`Failed to cache result for job ${jobId}:`, error);
60 | throw error;
61 | }
62 | }
63 |
64 | /**
65 | * Retrieve a BlobWriteResponse from cache
66 | */
67 | async get(jobId: string): Promise {
68 | try {
69 | const key = this.getKey(jobId);
70 | const value = await this.redis.get(key);
71 |
72 | if (!value) {
73 | return null;
74 | }
75 |
76 | return JSON.parse(value) as BlobWriteResponse;
77 | } catch (error) {
78 | logger.error(`Failed to retrieve cached result for job ${jobId}:`, error);
79 | return null;
80 | }
81 | }
82 |
83 |
84 | /**
85 | * Check if a job result exists in cache
86 | */
87 | async exists(jobId: string): Promise {
88 | try {
89 | const key = this.getKey(jobId);
90 | const exists = await this.redis.exists(key);
91 | return exists > 0;
92 | } catch (error) {
93 | logger.error(`Failed to check existence for job ${jobId}:`, error);
94 | return false;
95 | }
96 | }
97 |
98 | /**
99 | * Acquire a lock for a job
100 | */
101 | async acquireLock(jobId: string, lockTtl: number = 300): Promise {
102 | try {
103 | const key = this.getLockKey(jobId);
104 | const result = await this.redis.set(key, 'locked', {
105 | EX: lockTtl,
106 | NX: true
107 | });
108 | return result === 'OK';
109 | } catch (error) {
110 | logger.error(`Failed to acquire lock for job ${jobId}:`, error);
111 | return false;
112 | }
113 | }
114 |
115 | /**
116 | * Release a lock for a job
117 | */
118 | async releaseLock(jobId: string): Promise {
119 | try {
120 | const key = this.getLockKey(jobId);
121 | const deleted = await this.redis.del(key);
122 | return deleted > 0;
123 | } catch (error) {
124 | logger.warn(`Failed to release lock for job ${jobId}:`, error);
125 | return false;
126 | }
127 | }
128 |
129 | /**
130 | * Check if a job is locked
131 | */
132 | async isLocked(jobId: string): Promise {
133 | try {
134 | const key = this.getLockKey(jobId);
135 | const exists = await this.redis.exists(key);
136 | return exists > 0;
137 | } catch (error) {
138 | logger.error(`Failed to check lock for job ${jobId}:`, error);
139 | return false;
140 | }
141 | }
142 |
143 | private getKey(jobId: string): string {
144 | return `${this.chainId}:${this.keyPrefix}${jobId}`;
145 | }
146 |
147 | private getLockKey(jobId: string): string {
148 | return `${this.chainId}:${this.lockPrefix}${jobId}`;
149 | }
150 | }
--------------------------------------------------------------------------------
/packages/sdk/src/metrics.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Metrics collection for BlobKit SDK
3 | * Provides hooks for monitoring and observability
4 | */
5 |
6 | export interface BlobKitMetrics {
7 | blobWriteCount: number;
8 | blobWriteErrors: number;
9 | totalBytesWritten: number;
10 | averageWriteDuration: number;
11 | blobReadCount: number;
12 | blobReadErrors: number;
13 | totalBytesRead: number;
14 | averageReadDuration: number;
15 | proxyRequestCount: number;
16 | proxyRequestErrors: number;
17 | kzgOperationCount: number;
18 | kzgOperationErrors: number;
19 | }
20 |
21 | export interface MetricsHooks {
22 | onBlobWrite?: (size: number, duration: number, success: boolean) => void;
23 | onBlobRead?: (size: number, duration: number, success: boolean, source: string) => void;
24 | onProxyRequest?: (url: string, duration: number, success: boolean) => void;
25 | onKzgOperation?: (operation: string, duration: number, success: boolean) => void;
26 | onError?: (error: Error, context: string) => void;
27 | }
28 |
29 | /**
30 | * Default metrics collector
31 | */
32 | export class MetricsCollector {
33 | private metrics: BlobKitMetrics = {
34 | blobWriteCount: 0,
35 | blobWriteErrors: 0,
36 | totalBytesWritten: 0,
37 | averageWriteDuration: 0,
38 | blobReadCount: 0,
39 | blobReadErrors: 0,
40 | totalBytesRead: 0,
41 | averageReadDuration: 0,
42 | proxyRequestCount: 0,
43 | proxyRequestErrors: 0,
44 | kzgOperationCount: 0,
45 | kzgOperationErrors: 0
46 | };
47 |
48 | private writeDurations: number[] = [];
49 | private readDurations: number[] = [];
50 | private hooks: MetricsHooks;
51 |
52 | constructor(hooks: MetricsHooks = {}) {
53 | this.hooks = hooks;
54 | }
55 |
56 | recordBlobWrite(size: number, duration: number, success: boolean): void {
57 | if (success) {
58 | this.metrics.blobWriteCount++;
59 | this.metrics.totalBytesWritten += size;
60 | this.writeDurations.push(duration);
61 |
62 | // Update average
63 | const sum = this.writeDurations.reduce((a, b) => a + b, 0);
64 | this.metrics.averageWriteDuration = sum / this.writeDurations.length;
65 | } else {
66 | this.metrics.blobWriteErrors++;
67 | }
68 |
69 | this.hooks.onBlobWrite?.(size, duration, success);
70 | }
71 |
72 | recordBlobRead(size: number, duration: number, success: boolean, source: string): void {
73 | if (success) {
74 | this.metrics.blobReadCount++;
75 | this.metrics.totalBytesRead += size;
76 | this.readDurations.push(duration);
77 |
78 | // Update average
79 | const sum = this.readDurations.reduce((a, b) => a + b, 0);
80 | this.metrics.averageReadDuration = sum / this.readDurations.length;
81 | } else {
82 | this.metrics.blobReadErrors++;
83 | }
84 |
85 | this.hooks.onBlobRead?.(size, duration, success, source);
86 | }
87 |
88 | recordProxyRequest(url: string, duration: number, success: boolean): void {
89 | if (success) {
90 | this.metrics.proxyRequestCount++;
91 | } else {
92 | this.metrics.proxyRequestErrors++;
93 | }
94 |
95 | this.hooks.onProxyRequest?.(url, duration, success);
96 | }
97 |
98 | recordKzgOperation(operation: string, duration: number, success: boolean): void {
99 | if (success) {
100 | this.metrics.kzgOperationCount++;
101 | } else {
102 | this.metrics.kzgOperationErrors++;
103 | }
104 |
105 | this.hooks.onKzgOperation?.(operation, duration, success);
106 | }
107 |
108 | recordError(error: Error, context: string): void {
109 | this.hooks.onError?.(error, context);
110 | }
111 |
112 | trackOperation(
113 | operation: string,
114 | phase: 'start' | 'complete' | 'error',
115 | duration?: number
116 | ): void {
117 | // Simple operation tracking
118 | if (phase === 'complete' && duration !== undefined) {
119 | this.recordKzgOperation(operation, duration, true);
120 | } else if (phase === 'error') {
121 | this.recordKzgOperation(operation, 0, false);
122 | }
123 | }
124 |
125 | getMetrics(): BlobKitMetrics {
126 | return { ...this.metrics };
127 | }
128 |
129 | reset(): void {
130 | this.metrics = {
131 | blobWriteCount: 0,
132 | blobWriteErrors: 0,
133 | totalBytesWritten: 0,
134 | averageWriteDuration: 0,
135 | blobReadCount: 0,
136 | blobReadErrors: 0,
137 | totalBytesRead: 0,
138 | averageReadDuration: 0,
139 | proxyRequestCount: 0,
140 | proxyRequestErrors: 0,
141 | kzgOperationCount: 0,
142 | kzgOperationErrors: 0
143 | };
144 | this.writeDurations = [];
145 | this.readDurations = [];
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/packages/sdk/README.md:
--------------------------------------------------------------------------------
1 | # @blobkit/sdk
2 |
3 | TypeScript SDK for EIP-4844 blob storage on Ethereum. Supports browser and Node.js environments.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm install @blobkit/sdk ethers
9 | ```
10 |
11 | ## Usage
12 |
13 | ### Browser
14 |
15 | ```typescript
16 | import { BlobKit } from '@blobkit/sdk';
17 | import { ethers } from 'ethers';
18 |
19 | const provider = new ethers.BrowserProvider(window.ethereum);
20 | const signer = await provider.getSigner();
21 |
22 | const blobkit = new BlobKit(
23 | {
24 | rpcUrl: process.env.BLOBKIT_RPC_URL!,
25 | chainId: 1,
26 | proxyUrl: 'https://proxy.example.com',
27 | requestSigningSecret: 'shared-secret-with-proxy'
28 | },
29 | signer
30 | );
31 |
32 | const result = await blobkit.writeBlob({ message: 'Hello world' }, { appId: 'my-app' });
33 |
34 | console.log('Blob hash:', result.blobHash);
35 | console.log('Transaction:', result.blobTxHash);
36 | ```
37 |
38 | ### Node.js
39 |
40 | ```typescript
41 | import { BlobKit, createFromEnv } from '@blobkit/sdk';
42 | import { ethers } from 'ethers';
43 |
44 | const provider = new ethers.JsonRpcProvider(process.env.BLOBKIT_RPC_URL);
45 | const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
46 |
47 | // Option 1: From environment variables
48 | const blobkit = createFromEnv(signer);
49 |
50 | // Option 2: Manual configuration
51 | const blobkit = new BlobKit(
52 | {
53 | rpcUrl: process.env.BLOBKIT_RPC_URL!,
54 | chainId: 1
55 | },
56 | signer
57 | );
58 |
59 | const result = await blobkit.writeBlob(data);
60 | console.log('Blob hash:', result.blobHash);
61 | ```
62 |
63 | ## Reading Blobs
64 |
65 | BlobKit supports reading blob data from transactions:
66 |
67 | ```typescript
68 | // Read blob data by transaction hash
69 | const blobData = await blobkit.readBlob(txHash);
70 | console.log('Blob data:', blobData.data);
71 | console.log('Source:', blobData.source); // 'rpc', 'archive', or 'fallback'
72 |
73 | // Read specific blob by index (for transactions with multiple blobs)
74 | const secondBlob = await blobkit.readBlob(txHash, 1);
75 |
76 | // Read and decode as string
77 | const text = await blobkit.readBlobAsString(txHash);
78 | console.log('Text content:', text);
79 |
80 | // Read and decode as JSON
81 | const json = await blobkit.readBlobAsJSON(txHash);
82 | console.log('JSON data:', json);
83 | ```
84 |
85 | ### Archive Support
86 |
87 | If you configure an `archiveUrl`, BlobKit will attempt to read blobs from your archive first:
88 |
89 | ```typescript
90 | const blobkit = new BlobKit(
91 | {
92 | rpcUrl: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID',
93 | archiveUrl: 'https://your-blob-archive.com',
94 | chainId: 1
95 | },
96 | signer
97 | );
98 | ```
99 |
100 | The archive URL should serve blob data at `${archiveUrl}/${blobTxHash}` as binary data.
101 |
102 | ## Configuration
103 |
104 | ### Environment Variables
105 |
106 | ```bash
107 | BLOBKIT_RPC_URL=https://rpc.flashbots.net
108 | BLOBKIT_CHAIN_ID=1
109 | BLOBKIT_PROXY_URL=https://custom-proxy.example.com
110 | BLOBKIT_ESCROW_1=0x1234567890123456789012345678901234567890
111 | BLOBKIT_LOG_LEVEL=info
112 | ```
113 |
114 | ## Testing and Development
115 |
116 | ```bash
117 | npm test # Run tests
118 | npm run test:coverage # Run tests with coverage
119 | npm run build # Build distribution
120 | npm run lint # Lint code
121 | npm run type-check # TypeScript checking
122 | ```
123 |
124 | ## KZG Trusted Setup
125 |
126 | BlobKit requires a KZG trusted setup for creating blob commitments and proofs. This is handled automatically in most cases:
127 |
128 | ### Browser Environment
129 |
130 | In browsers, the SDK uses a lightweight WASM implementation that includes the necessary trusted setup data.
131 |
132 | ### Node.js Environment
133 |
134 | In Node.js, the SDK can use either:
135 |
136 | 1. **Built-in Setup** (default): The SDK includes the official Ethereum mainnet trusted setup
137 | 2. **Custom Setup**: Provide your own trusted setup file path
138 |
139 | ```typescript
140 | // Using built-in setup (recommended)
141 | const blobkit = new BlobKit(
142 | {
143 | rpcUrl: process.env.BLOBKIT_RPC_URL!,
144 | chainId: 1
145 | },
146 | signer
147 | );
148 |
149 | // Using custom trusted setup
150 | const blobkit = new BlobKit(
151 | {
152 | rpcUrl: process.env.BLOBKIT_RPC_URL!,
153 | chainId: 1,
154 | kzgSetup: {
155 | trustedSetupPath: '/path/to/trusted_setup.txt'
156 | }
157 | },
158 | signer
159 | );
160 | ```
161 |
162 | ### Trusted Setup File Format
163 |
164 | The trusted setup file should contain the powers of tau ceremony results in the standard format used by Ethereum. You can download the official setup from the [Ethereum KZG Ceremony](https://ceremony.ethereum.org/).
165 |
166 | ## Documentation
167 |
168 | See [/docs/sdk/](../../docs/sdk/) for complete API reference.
169 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/index.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | import { ethers } from 'ethers';
3 | import { createApp } from './app.js';
4 | import { loadConfig, validateConfig } from './config.js';
5 | import { createLogger } from './utils/logger.js';
6 | import { ProxyConfig } from './types.js';
7 | import { createSecureSigner, loadSignerConfig } from './services/secure-signer.js';
8 |
9 | // Load environment variables
10 | dotenv.config();
11 |
12 | const logger = createLogger('Server');
13 |
14 | /**
15 | * Verify proxy is authorized in escrow contract
16 | */
17 | async function verifyProxyAuthorization(config: ProxyConfig): Promise {
18 | const provider = new ethers.JsonRpcProvider(config.rpcUrl);
19 |
20 | // Use secure signer
21 | const signerConfig = loadSignerConfig();
22 | const secureSigner = await createSecureSigner(signerConfig, provider);
23 | const proxyAddress = await secureSigner.getAddress();
24 |
25 | // Check authorization in escrow contract with circuit breaker
26 | const escrowAbi = [
27 | 'function authorizedProxies(address) view returns (bool)',
28 | 'function isProxyAuthorized(address) view returns (bool)'
29 | ];
30 | const escrowContract = new ethers.Contract(config.escrowContract, escrowAbi, provider);
31 |
32 |
33 | try {
34 | // Try both method names for compatibility
35 | let isAuthorized = false;
36 | try {
37 | isAuthorized = await escrowContract.isProxyAuthorized(proxyAddress);
38 | } catch {
39 | // Fallback to old method name
40 | isAuthorized = await escrowContract.authorizedProxies(proxyAddress);
41 | }
42 |
43 | if (!isAuthorized) {
44 | throw new Error(
45 | `Proxy ${proxyAddress} is not authorized in escrow contract ${config.escrowContract}`
46 | );
47 | }
48 | logger.info(`Proxy ${proxyAddress} is authorized in escrow contract`);
49 |
50 | } catch (error) {
51 | if (error instanceof Error && error.message.includes('not authorized')) {
52 | logger.error('CRITICAL: Proxy is not authorized in escrow contract!');
53 | throw error;
54 | }
55 | if (error instanceof Error && error.message.includes('Circuit breaker')) {
56 | logger.error('CRITICAL: Cannot verify escrow contract - circuit breaker is OPEN');
57 | throw new Error(
58 | 'Escrow contract is unreachable. Check RPC connection and contract deployment.'
59 | );
60 | }
61 | // Contract might not be deployed or have different interface
62 | logger.warn('Could not verify proxy authorization, proceeding anyway');
63 | }
64 | }
65 |
66 | /**
67 | * Starts the proxy server
68 | */
69 | const startServer = async () => {
70 | try {
71 | // Load and validate configuration
72 | const config = loadConfig();
73 | validateConfig(config);
74 |
75 | logger.info('Starting BlobKit proxy server...', {
76 | port: config.port,
77 | host: config.host,
78 | chainId: config.chainId,
79 | escrowContract: config.escrowContract,
80 | proxyFeePercent: config.proxyFeePercent
81 | });
82 |
83 | // Create Express app and context with secure signer
84 | const { app, jobCompletionQueue } = await createApp(config);
85 |
86 | // Verify proxy authorization before starting
87 | await verifyProxyAuthorization(config);
88 |
89 | // Connect Redis and start server
90 | await jobCompletionQueue.connect();
91 | logger.info('Connected to Redis for persistent job queue');
92 |
93 | const server = app.listen(config.port, config.host, () => {
94 | logger.info(`Proxy server running on http://${config.host}:${config.port}`, {
95 | health: `/api/v1/health`,
96 | docs: `/docs`,
97 | blob: `/api/v1/blob/write`
98 | });
99 |
100 | // Start job completion queue after server is ready
101 | jobCompletionQueue.start();
102 | logger.info('Job completion retry queue started');
103 | });
104 |
105 | // Graceful shutdown
106 | const shutdown = (signal: string) => {
107 | logger.info(`Received ${signal}, shutting down gracefully...`);
108 |
109 | // Stop job completion queue first
110 | jobCompletionQueue.stop();
111 |
112 | server.close(async () => {
113 | // Disconnect Redis
114 | await jobCompletionQueue.disconnect();
115 | logger.info('Server closed and Redis disconnected');
116 | process.exit(0);
117 | });
118 |
119 | // Force close after 10 seconds
120 | setTimeout(() => {
121 | logger.error('Forced shutdown');
122 | process.exit(1);
123 | }, 10000);
124 | };
125 |
126 | process.on('SIGTERM', () => shutdown('SIGTERM'));
127 | process.on('SIGINT', () => shutdown('SIGINT'));
128 | } catch (error) {
129 | logger.error('Failed to start server:', error);
130 | process.exit(1);
131 | }
132 | };
133 |
134 | // Start server if this file is run directly
135 | if (import.meta.url === `file://${process.argv[1]}`) {
136 | startServer();
137 | }
138 |
139 | export { startServer };
140 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/monitoring/metrics.ts:
--------------------------------------------------------------------------------
1 | import { createLogger } from '../utils/logger.js';
2 |
3 | const logger = createLogger('Metrics');
4 |
5 | export interface ProxyMetrics {
6 | jobsProcessed: number;
7 | jobsFailed: number;
8 | jobsRetried: number;
9 | blobsSubmitted: number;
10 | totalFeesCollected: bigint;
11 | averageJobDuration: number;
12 | lastError?: { timestamp: Date; message: string; jobId?: string };
13 | }
14 |
15 | export class MetricsCollector {
16 | private metrics: ProxyMetrics = {
17 | jobsProcessed: 0,
18 | jobsFailed: 0,
19 | jobsRetried: 0,
20 | blobsSubmitted: 0,
21 | totalFeesCollected: 0n,
22 | averageJobDuration: 0
23 | };
24 |
25 | private jobStartTimes = new Map();
26 |
27 | /**
28 | * Record job start
29 | */
30 | jobStarted(jobId: string): void {
31 | this.jobStartTimes.set(jobId, Date.now());
32 | logger.debug(`Job ${jobId} started`);
33 | }
34 |
35 | /**
36 | * Record successful job completion
37 | */
38 | jobCompleted(jobId: string, feeCollected: bigint): void {
39 | const startTime = this.jobStartTimes.get(jobId);
40 | if (startTime) {
41 | const duration = Date.now() - startTime;
42 | this.updateAverageDuration(duration);
43 | this.jobStartTimes.delete(jobId);
44 | }
45 |
46 | this.metrics.jobsProcessed++;
47 | this.metrics.totalFeesCollected += feeCollected;
48 |
49 | logger.info(`Job ${jobId} completed successfully`, {
50 | totalJobs: this.metrics.jobsProcessed,
51 | totalFees: this.metrics.totalFeesCollected.toString()
52 | });
53 | }
54 |
55 | /**
56 | * Record job failure
57 | */
58 | jobFailed(jobId: string, error: string): void {
59 | this.jobStartTimes.delete(jobId);
60 | this.metrics.jobsFailed++;
61 | this.metrics.lastError = {
62 | timestamp: new Date(),
63 | message: error,
64 | jobId
65 | };
66 |
67 | logger.error(`Job ${jobId} failed`, {
68 | error,
69 | totalFailed: this.metrics.jobsFailed
70 | });
71 |
72 | // Alert if failure rate is high
73 | const failureRate =
74 | this.metrics.jobsFailed / (this.metrics.jobsProcessed + this.metrics.jobsFailed);
75 | if (failureRate > 0.1 && this.metrics.jobsProcessed > 10) {
76 | logger.error(`⚠️ HIGH FAILURE RATE: ${(failureRate * 100).toFixed(2)}%`);
77 | }
78 | }
79 |
80 | /**
81 | * Record job retry
82 | */
83 | jobRetried(jobId: string): void {
84 | this.metrics.jobsRetried++;
85 | logger.info(`Job ${jobId} added to retry queue`, {
86 | totalRetries: this.metrics.jobsRetried
87 | });
88 | }
89 |
90 | /**
91 | * Record blob submission
92 | */
93 | blobSubmitted(size: number): void {
94 | this.metrics.blobsSubmitted++;
95 | logger.debug(`Blob submitted`, {
96 | size,
97 | totalBlobs: this.metrics.blobsSubmitted
98 | });
99 | }
100 |
101 | /**
102 | * Get current metrics
103 | */
104 | getMetrics(): ProxyMetrics {
105 | return { ...this.metrics };
106 | }
107 |
108 | /**
109 | * Get health status based on metrics
110 | */
111 | getHealthStatus(): {
112 | healthy: boolean;
113 | warnings: string[];
114 | } {
115 | const warnings: string[] = [];
116 | let healthy = true;
117 |
118 | // Check failure rate
119 | const totalJobs = this.metrics.jobsProcessed + this.metrics.jobsFailed;
120 | if (totalJobs > 0) {
121 | const failureRate = this.metrics.jobsFailed / totalJobs;
122 | if (failureRate > 0.1) {
123 | warnings.push(`High failure rate: ${(failureRate * 100).toFixed(2)}%`);
124 | healthy = false;
125 | }
126 | }
127 |
128 | // Check recent errors
129 | if (this.metrics.lastError) {
130 | const errorAge = Date.now() - this.metrics.lastError.timestamp.getTime();
131 | if (errorAge < 300000) {
132 | // 5 minutes
133 | warnings.push(`Recent error: ${this.metrics.lastError.message}`);
134 | }
135 | }
136 |
137 | // Check job duration
138 | if (this.metrics.averageJobDuration > 30000) {
139 | // 30 seconds
140 | warnings.push(
141 | `Slow job processing: ${(this.metrics.averageJobDuration / 1000).toFixed(2)}s average`
142 | );
143 | }
144 |
145 | return { healthy, warnings };
146 | }
147 |
148 | /**
149 | * Reset metrics (for testing)
150 | */
151 | reset(): void {
152 | this.metrics = {
153 | jobsProcessed: 0,
154 | jobsFailed: 0,
155 | jobsRetried: 0,
156 | blobsSubmitted: 0,
157 | totalFeesCollected: 0n,
158 | averageJobDuration: 0
159 | };
160 | this.jobStartTimes.clear();
161 | }
162 |
163 | private updateAverageDuration(newDuration: number): void {
164 | const total = this.metrics.jobsProcessed;
165 | if (total === 0) {
166 | this.metrics.averageJobDuration = newDuration;
167 | } else {
168 | this.metrics.averageJobDuration =
169 | (this.metrics.averageJobDuration * total + newDuration) / (total + 1);
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/app.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import helmet from 'helmet';
3 | import cors from 'cors';
4 | import { ethers } from 'ethers';
5 | import swaggerUi from 'swagger-ui-express';
6 | import YAML from 'yamljs';
7 | import path from 'path';
8 |
9 | import { ProxyConfig } from './types.js';
10 | import { PaymentVerifier } from './services/payment-verifier.js';
11 | import { BlobExecutor } from './services/blob-executor.js';
12 | import { PersistentJobQueue } from './services/persistent-job-queue.js';
13 | import { JobCache } from './services/job-cache.js';
14 | import { createRateLimit } from './middleware/rate-limit.js';
15 | import { errorHandler, notFoundHandler } from './middleware/error-handler.js';
16 | import { tracingMiddleware } from './middleware/tracing.js';
17 | import { createHealthRouter } from './routes/health.js';
18 | import { createBlobRouter } from './routes/blob.js';
19 | import { createMetricsRouter } from './routes/metrics.js';
20 | import { createLogger } from './utils/logger.js';
21 | import { createSecureSigner, loadSignerConfig } from './services/secure-signer.js';
22 | import { metricsMiddleware } from './monitoring/prometheus-metrics.js';
23 |
24 | const logger = createLogger('App');
25 |
26 | // Get dirname for ES modules/CommonJS compatibility
27 | const getDirname = (): string => {
28 | // In Node.js/CommonJS environments
29 | if (typeof __dirname !== 'undefined') {
30 | return __dirname;
31 | }
32 |
33 | // Fallback for environments without __dirname
34 | return process.cwd();
35 | };
36 |
37 | /**
38 | * Creates and configures the Express application
39 | */
40 | export interface AppContext {
41 | app: express.Application;
42 | jobCompletionQueue: PersistentJobQueue;
43 | }
44 |
45 | export const createApp = async (config: ProxyConfig): Promise => {
46 | const app = express();
47 |
48 | // Security middleware
49 | app.use(helmet());
50 | app.use(
51 | cors({
52 | origin: process.env.CORS_ORIGIN || true,
53 | credentials: true
54 | })
55 | );
56 |
57 | // Request parsing
58 | app.use(express.json({ limit: '1mb' }));
59 | app.use(express.urlencoded({ extended: true, limit: '1mb' }));
60 |
61 | // Distributed tracing middleware - must come early
62 | app.use(tracingMiddleware('blobkit-proxy'));
63 |
64 | // Metrics middleware
65 | app.use(metricsMiddleware());
66 |
67 | // Rate limiting
68 | if(config.httpProxyCount > 0 ){
69 | app.set('trust proxy', 1);
70 | }
71 | const rateLimiter = createRateLimit(config.rateLimitRequests, config.rateLimitWindow);
72 | app.use('/api/v1/blob', rateLimiter);
73 |
74 | // Request logging (now includes trace context from tracing middleware)
75 | app.use('/api/v1/blob', (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 | logger.info(`${req.method} ${req.path}`, {
77 | traceId: req.traceId,
78 | spanId: req.spanId,
79 | ip: req.ip,
80 | userAgent: req.get('User-Agent')
81 | });
82 | next();
83 | });
84 |
85 | // Initialize services with secure signer
86 | const provider = new ethers.JsonRpcProvider(config.rpcUrl);
87 |
88 | // Create secure signer
89 | const signerConfig = loadSignerConfig();
90 | const signer = await createSecureSigner(signerConfig, provider);
91 |
92 | const paymentVerifier = new PaymentVerifier(config.rpcUrl, config.escrowContract);
93 | const blobExecutor = new BlobExecutor(
94 | config.rpcUrl,
95 | config.chainId,
96 | signer,
97 | );
98 | await blobExecutor.ensureBlobKit()
99 | const jobCache = new JobCache()
100 | await jobCache.connect()
101 | const jobCompletionQueue = new PersistentJobQueue(paymentVerifier, signer, jobCache);
102 |
103 |
104 |
105 | // Note: jobCompletionQueue.start() is called after server starts listening
106 |
107 | // API routes
108 | app.use('/api/v1', createHealthRouter(config, provider, signer));
109 | app.use(
110 | '/api/v1/blob',
111 | createBlobRouter(config, paymentVerifier, blobExecutor, jobCompletionQueue, jobCache, signer)
112 | );
113 |
114 | // Metrics endpoint (not under /api to avoid rate limiting)
115 | app.use('/metrics', createMetricsRouter());
116 |
117 | // Swagger documentation (skip in test environment)
118 | if (process.env.NODE_ENV !== 'test') {
119 | try {
120 | const swaggerPath = process.env.SWAGGER_PATH || path.join(getDirname(), 'openapi.yaml');
121 | const swaggerDocument = YAML.load(swaggerPath);
122 | app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
123 | } catch (error) {
124 | logger.warn('Failed to load OpenAPI documentation:', error as Error);
125 | }
126 | }
127 |
128 | // Root endpoint
129 | app.get('/', (req: express.Request, res: express.Response) => {
130 | res.json({
131 | name: '@blobkit/proxy-server',
132 | version: '0.0.1',
133 | description: 'BlobKit proxy server for blob transaction execution',
134 | endpoints: {
135 | health: '/api/v1/health',
136 | blobWrite: '/api/v1/blob/write',
137 | docs: '/docs'
138 | }
139 | });
140 | });
141 |
142 | // Error handling
143 | app.use(notFoundHandler);
144 | app.use(errorHandler);
145 |
146 | return { app, jobCompletionQueue };
147 | };
148 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { Command } from 'commander';
4 | import { startServer } from './index.js';
5 | import { loadConfig } from './config.js';
6 | import { proxyLogger } from './utils/logger.js';
7 |
8 | const logger = proxyLogger.child('cli');
9 | const program = new Command();
10 |
11 | program
12 | .name('blobkit-proxy')
13 | .description('BlobKit proxy server for blob transaction execution')
14 | .version('1.0.0');
15 |
16 | program
17 | .command('start')
18 | .description('Start the proxy server')
19 | .option('-p, --port ', 'Port to listen on')
20 | .option('-h, --host ', 'Host to bind to')
21 | .option('--rpc-url ', 'Ethereum RPC URL')
22 | .option('--chain-id ', 'Chain ID')
23 | .option('--escrow-contract ', 'Escrow contract address')
24 | .option('--private-key ', 'Private key for proxy operations')
25 | .option('--proxy-fee ', 'Proxy fee percentage (0-10)')
26 | .action(async (options: Record) => {
27 | try {
28 | // Override config with CLI options
29 | if (options.port) process.env.PORT = options.port;
30 | if (options.host) process.env.HOST = options.host;
31 | if (options.rpcUrl) process.env.RPC_URL = options.rpcUrl;
32 | if (options.chainId) process.env.CHAIN_ID = options.chainId;
33 | if (options.escrowContract) process.env.ESCROW_CONTRACT = options.escrowContract;
34 | if (options.privateKey) process.env.PRIVATE_KEY = options.privateKey;
35 | if (options.proxyFee) process.env.PROXY_FEE_PERCENT = options.proxyFee;
36 |
37 | await startServer();
38 | } catch (error) {
39 | logger.error('Failed to start proxy server:', error);
40 | process.exit(1);
41 | }
42 | });
43 |
44 | program
45 | .command('health')
46 | .description('Check if a proxy server is healthy')
47 | .option('--url ', 'Proxy server URL', 'http://localhost:3000')
48 | .action(async (options: { url: string }) => {
49 | try {
50 | const response = await fetch(`${options.url}/api/v1/health`);
51 | const health = await response.json();
52 |
53 | logger.info('Proxy server health check', health);
54 |
55 | if (health.status === 'healthy') {
56 | logger.info('Proxy server is healthy');
57 | process.exit(0);
58 | } else {
59 | logger.warn('Proxy server is unhealthy');
60 | process.exit(1);
61 | }
62 | } catch (error) {
63 | logger.error('Failed to check proxy health', error as Error);
64 | process.exit(1);
65 | }
66 | });
67 |
68 | program
69 | .command('config')
70 | .description('Show current configuration')
71 | .action(() => {
72 | try {
73 | const config = loadConfig();
74 | logger.info('Current configuration', {
75 | ...config,
76 | privateKey: `${config.privateKey.substring(0, 6)}...`
77 | });
78 | } catch (error) {
79 | logger.error('Failed to load configuration:', error);
80 | process.exit(1);
81 | }
82 | });
83 |
84 | program
85 | .command('dev-proxy')
86 | .description('Start a local development proxy server')
87 | .action(async () => {
88 | try {
89 | // Set development defaults
90 | process.env.NODE_ENV = 'development';
91 | process.env.PORT = process.env.PORT || '3000';
92 | process.env.HOST = process.env.HOST || 'localhost';
93 | process.env.RPC_URL = process.env.RPC_URL || 'http://localhost:8545';
94 | process.env.CHAIN_ID = process.env.CHAIN_ID || '31337';
95 | process.env.LOG_LEVEL = 'debug';
96 | process.env.PROXY_FEE_PERCENT = '0';
97 |
98 | logger.info('Starting development proxy server...');
99 | await startServer();
100 | } catch (error) {
101 | logger.error('Failed to start dev proxy:', error);
102 | process.exit(1);
103 | }
104 | });
105 |
106 | program
107 | .command('simulate-payment')
108 | .description('Simulate a payment flow for testing')
109 | .requiredOption('--job-id ', 'Job ID to simulate payment for')
110 | .option('--amount ', 'Payment amount in ETH', '0.001')
111 | .action((options: { jobId: string; amount: string }) => {
112 | try {
113 | const config = loadConfig();
114 | logger.info(`Simulating payment for job ${options.jobId}`);
115 | logger.info(`Amount: ${options.amount} ETH`);
116 | logger.info(`Escrow contract: ${config.escrowContract}`);
117 |
118 | logger.info('Payment simulation completed.');
119 | } catch (error) {
120 | logger.error('Failed to simulate payment:', error);
121 | process.exit(1);
122 | }
123 | });
124 |
125 | program
126 | .command('check-health')
127 | .description('Verify proxy connectivity and configuration')
128 | .option('--url ', 'Proxy server URL', 'http://localhost:3000')
129 | .action(async (options: { url: string }) => {
130 | try {
131 | const response = await fetch(`${options.url}/api/v1/health`);
132 | const health = await response.json();
133 |
134 | logger.info('Proxy Health Check', {
135 | status: health.status,
136 | version: health.version,
137 | chainId: health.chainId,
138 | escrowContract: health.escrowContract,
139 | proxyFeePercent: health.proxyFeePercent,
140 | maxBlobSize: health.maxBlobSize
141 | });
142 |
143 | if (health.status === 'healthy') {
144 | logger.info('Proxy is healthy and ready to accept requests');
145 | process.exit(0);
146 | } else {
147 | logger.error('Proxy is unhealthy');
148 | process.exit(1);
149 | }
150 | } catch (error) {
151 | logger.error('Failed to connect to proxy', error as Error);
152 | process.exit(1);
153 | }
154 | });
155 |
156 | // Parse command line arguments
157 | program.parse();
158 |
--------------------------------------------------------------------------------
/packages/contracts/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * BlobKit Contracts - TypeScript Interface
3 | * Provides typed interfaces for interacting with BlobKit smart contracts
4 | */
5 |
6 | // Contract ABI for BlobKitEscrow
7 | export const BlobKitEscrowABI = [
8 | // Events
9 | 'event JobCreated(bytes32 indexed jobId, address indexed user, uint256 amount)',
10 | 'event JobCompleted(bytes32 indexed jobId, bytes32 blobTxHash, uint256 proxyFee)',
11 | 'event JobRefunded(bytes32 indexed jobId, string reason)',
12 | 'event JobTimeoutUpdated(uint256 oldTimeout, uint256 newTimeout)',
13 | 'event ProxyAuthorizationChanged(address indexed proxy, bool authorized)',
14 | 'event ProxyFeeUpdated(address indexed proxy, uint256 oldFee, uint256 newFee)',
15 |
16 | // Read Functions
17 | 'function getJobTimeout() external view returns (uint256)',
18 | 'function getJob(bytes32 jobId) external view returns (tuple(address user, uint256 amount, bool completed, uint256 timestamp, bytes32 blobTxHash))',
19 | 'function isJobExpired(bytes32 jobId) external view returns (bool)',
20 | 'function getProxyFee(address proxy) external view returns (uint256)',
21 | 'function jobs(bytes32) external view returns (address user, uint256 amount, bool completed, uint256 timestamp, bytes32 blobTxHash)',
22 | 'function authorizedProxies(address) external view returns (bool)',
23 | 'function proxyFees(address) external view returns (uint256)',
24 | 'function owner() external view returns (address)',
25 | 'function paused() external view returns (bool)',
26 |
27 | // Constants
28 | 'function MAX_PROXY_FEE_PERCENT() external view returns (uint256)',
29 | 'function DEFAULT_JOB_TIMEOUT() external view returns (uint256)',
30 |
31 | // Write Functions
32 | 'function depositForBlob(bytes32 jobId) external payable',
33 | 'function completeJob(bytes32 jobId, bytes32 blobTxHash, bytes calldata proof) external',
34 | 'function refundExpiredJob(bytes32 jobId) external',
35 | 'function setProxyFee(uint256 percent) external',
36 |
37 | // Owner Functions
38 | 'function setJobTimeout(uint256 _timeout) external',
39 | 'function setProxyAuthorization(address proxy, bool authorized) external',
40 | 'function pause() external',
41 | 'function unpause() external',
42 | 'function emergencyWithdraw() external',
43 | 'function transferOwnership(address newOwner) external',
44 | 'function renounceOwnership() external'
45 | ] as const;
46 |
47 | // TypeScript interfaces
48 | export interface Job {
49 | user: string;
50 | amount: bigint;
51 | completed: boolean;
52 | timestamp: bigint;
53 | blobTxHash: string;
54 | }
55 |
56 | export interface CostEstimate {
57 | blobFee: bigint;
58 | gasFee: bigint;
59 | proxyFee: bigint;
60 | total: bigint;
61 | }
62 |
63 | /**
64 | * Contract addresses for a specific network
65 | */
66 | export interface ContractAddresses {
67 | escrow: string;
68 | }
69 |
70 | /**
71 | * Contract addresses by network
72 | * These will be populated after contract deployment
73 | */
74 | export const CONTRACT_ADDRESSES: Record = {
75 | // Mainnet
76 | 1: {
77 | escrow: process.env.BLOBKIT_ESCROW_MAINNET || '' // Set after mainnet deployment
78 | },
79 | // Sepolia testnet
80 | 11155111: {
81 | escrow: process.env.BLOBKIT_ESCROW_SEPOLIA || '' // Set after testnet deployment
82 | },
83 | // Holesky testnet
84 | 17000: {
85 | escrow: process.env.BLOBKIT_ESCROW_HOLESKY || '' // Set after testnet deployment
86 | }
87 | };
88 |
89 | /**
90 | * Get contract address for a specific network
91 | * @param chainId - Network chain ID
92 | * @param contract - Contract type
93 | * @returns Contract address
94 | * @throws Error if contract not deployed on network
95 | */
96 | export function getContractAddress(chainId: number, contract: keyof ContractAddresses): string {
97 | const addresses = CONTRACT_ADDRESSES[chainId];
98 | if (!addresses) {
99 | throw new Error(`BlobKit contracts not deployed on chain ${chainId}`);
100 | }
101 |
102 | const address = addresses[contract];
103 | if (!address) {
104 | throw new Error(
105 | `${contract} contract not deployed on chain ${chainId}. Please set the appropriate environment variable.`
106 | );
107 | }
108 |
109 | return address;
110 | }
111 |
112 | // Error types
113 | export enum EscrowErrorCode {
114 | JOB_ALREADY_EXISTS = 'JobAlreadyExists',
115 | JOB_NOT_FOUND = 'JobNotFound',
116 | JOB_ALREADY_COMPLETED = 'JobAlreadyCompleted',
117 | JOB_NOT_EXPIRED = 'JobNotExpired',
118 | UNAUTHORIZED_PROXY = 'UnauthorizedProxy',
119 | INVALID_PROXY_FEE = 'InvalidProxyFee',
120 | INVALID_JOB_TIMEOUT = 'InvalidJobTimeout',
121 | INVALID_PROOF = 'InvalidProof',
122 | TRANSFER_FAILED = 'TransferFailed',
123 | ZERO_AMOUNT = 'ZeroAmount'
124 | }
125 |
126 | // Event filter helpers
127 | export const createJobCreatedFilter = (jobId?: string, user?: string) => {
128 | // JobCreated(bytes32 indexed jobId, address indexed user, uint256 amount)
129 | const topics = ['0xc9f761cef4b498085beaa83472253ad1dbcaa175c7e97bd6893d9da4b6ab0868'];
130 | if (jobId) topics.push(jobId);
131 | if (user) topics.push(user.toLowerCase().padEnd(66, '0'));
132 | return { topics };
133 | };
134 |
135 | export const createJobCompletedFilter = (jobId?: string) => {
136 | // JobCompleted(bytes32 indexed jobId, bytes32 blobTxHash, uint256 proxyFee)
137 | const topics = ['0x9bb5b9fff77191c79356e2cc9fbdb082cd52c3d60643ca121716890337f818e7'];
138 | if (jobId) topics.push(jobId);
139 | return { topics };
140 | };
141 |
142 | // Utility functions
143 | export const calculateJobId = (user: string, payloadHash: string, nonce: number): string => {
144 | // This should match the job ID generation in the SDK
145 | return `${user.toLowerCase()}-${payloadHash}-${nonce}`;
146 | };
147 |
148 | export const formatEther = (wei: bigint): string => {
149 | return (Number(wei) / 1e18).toFixed(8);
150 | };
151 |
152 | export const parseEther = (eth: string): bigint => {
153 | return BigInt(Math.floor(parseFloat(eth) * 1e18));
154 | };
155 |
156 | // Export contract factory type for TypeScript usage
157 | export type BlobKitEscrowContract = {
158 | address: string;
159 | abi: typeof BlobKitEscrowABI;
160 | };
161 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | We actively support and provide security updates for the following versions:
6 |
7 | | Version | Supported |
8 | | ------- | --------- |
9 | | 0.3.x | Yes |
10 | | 0.2.x | Yes |
11 | | 0.1.x | No |
12 |
13 | ## Reporting a Vulnerability
14 |
15 | **Please do not report security vulnerabilities through public GitHub issues.**
16 |
17 | Instead, please report security vulnerabilities by emailing the maintainers directly. Include as much information as possible:
18 |
19 | - Description of the vulnerability
20 | - Steps to reproduce the issue
21 | - Potential impact assessment
22 | - Suggested fix (if you have one)
23 |
24 | You should receive a response within 48 hours. If the issue is confirmed, we will:
25 |
26 | 1. Work on a fix immediately
27 | 2. Coordinate disclosure timing with you
28 | 3. Credit you in the security advisory (if desired)
29 | 4. Release a security update as soon as possible
30 |
31 | ## Security Measures
32 |
33 | ### Input Validation
34 |
35 | BlobKit implements comprehensive input validation:
36 |
37 | - **Environment Variables**: All environment variables are validated for format and security
38 | - **Private Keys**: Validated for correct 64-character hex format
39 | - **RPC URLs**: Validated for proper HTTP/HTTPS format
40 | - **Blob Hashes**: Validated for correct versioned hash format
41 | - **Transaction Hashes**: Validated for correct 64-character hex format
42 | - **Numeric Inputs**: Range validation for chain IDs, compression levels, and block numbers
43 |
44 | ### Cryptographic Security
45 |
46 | - **Audited Libraries**: Uses `@noble/curves` and `@noble/hashes` for cryptographic operations
47 | - **KZG Implementation**: Follows EIP-4844 specification exactly
48 | - **Trusted Setup**: Supports official Ethereum KZG ceremony parameters
49 | - **Development Warning**: Clear warnings about mock setup usage
50 |
51 | ### Data Handling
52 |
53 | - **No Sensitive Data Logging**: Private keys and sensitive information are never logged
54 | - **Secure Error Handling**: Error messages don't expose sensitive internal details
55 | - **Memory Safety**: Uses TypeScript strict mode and validates all data types
56 | - **Buffer Operations**: Uses safe buffer operations to prevent overflows
57 |
58 | ### Network Security
59 |
60 | - **HTTPS Validation**: Ensures RPC and archive URLs use secure protocols
61 | - **No Hardcoded Credentials**: All credentials must be provided via environment variables
62 | - **Secure Defaults**: Conservative default settings for all configurable options
63 |
64 | ## Security Best Practices for Users
65 |
66 | ### Environment Variables
67 |
68 | ```bash
69 | # DO: Use environment variables for sensitive data
70 | RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
71 | PRIVATE_KEY=0x1234567890abcdef...
72 |
73 | # DON'T: Hardcode sensitive information in code
74 | const privateKey = "0x1234567890abcdef..."; // Never do this
75 | ```
76 |
77 | ### Private Key Management
78 |
79 | - **Never commit private keys to version control**
80 | - **Use hardware wallets for high-value operations**
81 | - **Rotate keys regularly for production systems**
82 | - **Use different keys for different environments**
83 | - **Consider key management services for production**
84 |
85 | ### RPC Security
86 |
87 | - **Use reputable RPC providers**
88 | - **Implement rate limiting in your applications**
89 | - **Monitor RPC usage for anomalies**
90 | - **Use API keys with appropriate scoping**
91 |
92 | ### Production Deployment
93 |
94 | - **Always use production KZG trusted setup**
95 | - **Validate all inputs at application boundaries**
96 | - **Implement proper logging without sensitive data**
97 | - **Use HTTPS for all network communications**
98 | - **Regularly update dependencies**
99 |
100 | ### Archive Services
101 |
102 | - **Verify archive service integrity**
103 | - **Implement fallback mechanisms**
104 | - **Monitor archive service availability**
105 | - **Consider running your own archive service for critical applications**
106 |
107 | ## Vulnerability Disclosure Timeline
108 |
109 | We follow responsible disclosure practices:
110 |
111 | 1. **Day 0**: Vulnerability reported
112 | 2. **Day 1-2**: Initial response and acknowledgment
113 | 3. **Day 3-7**: Vulnerability assessment and reproduction
114 | 4. **Day 8-14**: Fix development and testing
115 | 5. **Day 15-21**: Security release preparation
116 | 6. **Day 22**: Public disclosure and release
117 |
118 | This timeline may be adjusted based on the severity and complexity of the vulnerability.
119 |
120 | ## Security Considerations for Developers
121 |
122 | ### Blob Data
123 |
124 | - Blob data is ephemeral (auto-deleted after ~18 days)
125 | - Blob data is publicly readable by anyone
126 | - Never store sensitive information in blobs
127 | - Consider encryption for private data
128 |
129 | ### Transaction Costs
130 |
131 | - Blob transactions have gas costs
132 | - Implement proper fee estimation
133 | - Monitor for unusual cost spikes
134 | - Consider fee limits in production
135 |
136 | ### Network Considerations
137 |
138 | - Blob availability depends on network conditions
139 | - Implement retry logic for failed reads
140 | - Consider fallback mechanisms
141 | - Monitor network health
142 |
143 | ## Dependency Security
144 |
145 | We regularly audit and update dependencies:
146 |
147 | - **Automated Security Scanning**: Dependencies are scanned for known vulnerabilities
148 | - **Regular Updates**: Security updates are applied promptly
149 | - **Minimal Dependencies**: We keep dependencies minimal to reduce attack surface
150 | - **Vetted Libraries**: Only use well-maintained, audited libraries for cryptographic operations
151 |
152 | ## Security Testing
153 |
154 | Our security testing includes:
155 |
156 | - **Static Code Analysis**: Automated scanning for security patterns
157 | - **Dependency Auditing**: Regular npm audit runs
158 | - **Input Fuzzing**: Testing with malformed and edge-case inputs
159 | - **Cryptographic Testing**: Verification against known test vectors
160 | - **Integration Testing**: End-to-end security validation
161 |
162 | ## Contact
163 |
164 | For security-related questions or concerns, please contact Zak Cole directly at zcole@linux.com rather than opening public issues.
165 |
166 | Thank you for helping keep BlobKit secure.
167 |
--------------------------------------------------------------------------------
/packages/proxy-server/src/services/blob-executor.ts:
--------------------------------------------------------------------------------
1 | import { BlobKit, Signer } from '@blobkit/sdk';
2 | import { ethers } from 'ethers';
3 | import { BlobJob, ProxyError, ProxyErrorCode } from '../types.js';
4 | import { createLogger } from '../utils/logger.js';
5 | import { TracingService, ExtendedTraceContext } from '../middleware/tracing.js';
6 | // import type { TraceContext } from '../utils/logger.js';
7 |
8 | const logger = createLogger('BlobExecutor');
9 | const tracingService = new TracingService('blobkit-blob-executor');
10 |
11 | /**
12 | * Service for executing blob transactions
13 | */
14 | export class BlobExecutor {
15 | private blobkit: BlobKit | null = null;
16 | private config: { rpcUrl: string; chainId: number };
17 | private signer: Signer;
18 | private logger = createLogger('BlobExecutor');
19 |
20 | constructor(rpcUrl: string, chainId: number, signer: Signer) {
21 | this.config = { rpcUrl, chainId };
22 | this.signer = signer;
23 | }
24 |
25 | /**
26 | * Initialize BlobKit asynchronously
27 | */
28 | async ensureBlobKit(): Promise {
29 | if (!this.blobkit) {
30 | // Initialize BlobKit with KZG setup for Node.js environment
31 | this.blobkit = await BlobKit.init(
32 | {
33 | rpcUrl: this.config.rpcUrl,
34 | chainId: this.config.chainId,
35 | logLevel: 'info',
36 | },
37 | this.signer
38 | );
39 | }
40 | return this.blobkit;
41 | }
42 |
43 |
44 | /**
45 | * Executes a blob transaction
46 | */
47 | async executeBlob(
48 | job: BlobJob,
49 | traceContext?: ExtendedTraceContext
50 | ): Promise<{
51 | blobTxHash: string;
52 | blockNumber: number;
53 | blobHash: string;
54 | commitment: string;
55 | proofs: string[];
56 | blobIndex: number;
57 | }> {
58 | const span = tracingService.startSpan('blob.execute', traceContext);
59 | span.setAttribute('job.id', job.jobId);
60 | span.setAttribute('job.user', job.user);
61 | span.setAttribute('payload.size', job.payload.length);
62 |
63 | const tracedLogger = traceContext
64 | ? tracingService.getLoggerWithTrace(logger, traceContext)
65 | : logger;
66 |
67 | try {
68 | tracedLogger.info(`Executing blob transaction for job ${job.jobId}`);
69 |
70 | // Validate blob size
71 | if (job.payload.length > 131072) {
72 | // 128KB
73 | throw new ProxyError(
74 | ProxyErrorCode.BLOB_TOO_LARGE,
75 | `Blob size ${job.payload.length} exceeds maximum 131072 bytes`,
76 | 400
77 | );
78 | }
79 |
80 | // Convert payload back to the appropriate format based on codec
81 | let decodedPayload: unknown;
82 |
83 | const { codec } = job.meta as { codec?: string };
84 | switch (codec?.toLowerCase()) {
85 | case 'json':
86 | case 'application/json':
87 | decodedPayload = JSON.parse(new TextDecoder().decode(job.payload));
88 | break;
89 | case 'text':
90 | case 'text/plain':
91 | decodedPayload = new TextDecoder().decode(job.payload);
92 | break;
93 | case 'raw':
94 | case 'application/octet-stream':
95 | decodedPayload = job.payload;
96 | break;
97 | default:
98 | // Try to parse as JSON by default
99 | try {
100 | decodedPayload = JSON.parse(new TextDecoder().decode(job.payload));
101 | } catch {
102 | decodedPayload = job.payload;
103 | }
104 | }
105 |
106 | // Execute blob write using the SDK
107 | const result = await this.writeBlob(decodedPayload, job.meta, job.jobId);
108 |
109 | tracedLogger.info(
110 | `Blob transaction executed successfully for job ${job.jobId}: ${result.blobTxHash}`
111 | );
112 |
113 | span.setAttribute('blob.tx_hash', result.blobTxHash);
114 | span.setAttribute('blob.block_number', result.blockNumber);
115 | span.setAttribute('blob.index', result.blobIndex);
116 | span.setStatus({ code: 0 }); // Success
117 |
118 | return {
119 | blobTxHash: result.blobTxHash,
120 | blockNumber: result.blockNumber,
121 | blobHash: result.blobHash,
122 | commitment: result.commitment,
123 | proofs: result.proofs,
124 | blobIndex: result.blobIndex
125 | };
126 | } catch (error) {
127 | tracedLogger.error(`Blob execution failed for job ${job.jobId}:`, {
128 | error: error instanceof Error ? error.message : String(error)
129 | });
130 |
131 | span.recordException(error as Error);
132 | span.setStatus({
133 | code: 2,
134 | message: error instanceof Error ? error.message : 'Unknown error'
135 | }); // Error
136 |
137 | if (error instanceof ProxyError) {
138 | throw error;
139 | }
140 |
141 | throw new ProxyError(
142 | ProxyErrorCode.BLOB_EXECUTION_FAILED,
143 | `Failed to execute blob transaction: ${error instanceof Error ? error.message : 'Unknown error'}`,
144 | 500,
145 | { jobId: job.jobId, error: error instanceof Error ? error.message : 'Unknown error' }
146 | );
147 | } finally {
148 | span.end();
149 | }
150 | }
151 |
152 | /**
153 | * Writes blob data using the SDK
154 | */
155 | private async writeBlob(
156 | payload: unknown,
157 | meta: Record,
158 | jobId: string
159 | ): Promise<{
160 | blobTxHash: string;
161 | blockNumber: number;
162 | blobHash: string;
163 | commitment: string;
164 | proofs: string[];
165 | blobIndex: number;
166 | }> {
167 | // Use the initialized BlobKit instance
168 | try {
169 | const blobkit = await this.ensureBlobKit();
170 |
171 | // Write blob directly (without proxy)
172 | const result = await blobkit.writeBlob(payload as Uint8Array | string | object, meta, jobId);
173 |
174 | return {
175 | blobTxHash: result.blobTxHash,
176 | blockNumber: result.blockNumber,
177 | blobHash: result.blobHash,
178 | commitment: result.commitment,
179 | proofs: result.proofs,
180 | blobIndex: result.blobIndex
181 | };
182 | } catch (error) {
183 | this.logger.error('Blob writing failed:', error);
184 | throw new ProxyError(
185 | ProxyErrorCode.BLOB_EXECUTION_FAILED,
186 | `Blob execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
187 | 500
188 | );
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/packages/sdk/src/proxy-client.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Proxy client for BlobKit
3 | *
4 | * Handles communication with the BlobKit proxy server
5 | */
6 |
7 | import { BlobMeta, ProxyHealthResponse, BlobKitError, BlobKitErrorCode } from './types.js';
8 | import { sleep } from './utils.js';
9 | import { Logger } from './logger.js';
10 |
11 | export interface ProxyClientConfig {
12 | proxyUrl: string;
13 | requestSigningSecret?: string;
14 | logLevel?: 'debug' | 'info' | 'silent';
15 | }
16 |
17 | export interface BlobSubmitResult {
18 | blobTxHash: string;
19 | blockNumber: number;
20 | blobHash: string;
21 | commitment: string;
22 | proofs: string[];
23 | blobIndex: number;
24 | completionTxHash: string;
25 | }
26 |
27 | export class ProxyClient {
28 | private logger: Logger;
29 |
30 | constructor(private config: ProxyClientConfig) {
31 | this.logger = new Logger({ context: 'ProxyClient', level: config.logLevel as any });
32 | }
33 |
34 | /**
35 | * Submit blob data to proxy server
36 | */
37 | async submitBlob(data: {
38 | jobId: string;
39 | paymentTxHash: string;
40 | payload: Uint8Array;
41 | signature: Uint8Array;
42 | meta: BlobMeta;
43 | blobVersion: string;
44 | }): Promise {
45 | const maxRetries = 3;
46 | const baseDelay = 1000; // 1 second
47 |
48 | for (let attempt = 0; attempt <= maxRetries; attempt++) {
49 | try {
50 | const requestBody = {
51 | jobId: data.jobId,
52 | paymentTxHash: data.paymentTxHash,
53 | payload: Buffer.from(data.payload).toString('base64'),
54 | signature: Buffer.from(data.signature).toString('base64'),
55 | meta: data.meta,
56 | blobVersion: data.blobVersion
57 | };
58 |
59 | const headers: Record = {
60 | 'Content-Type': 'application/json'
61 | };
62 |
63 |
64 | const response = await fetch(`${this.config.proxyUrl}/api/v1/blob/write`, {
65 | method: 'POST',
66 | headers,
67 | body: JSON.stringify(requestBody),
68 | signal: AbortSignal.timeout(60000) // 60 second timeout
69 | });
70 |
71 | if (!response || !response.ok) {
72 | const errorText = response ? await response.text() : 'No response received';
73 |
74 | // Don't retry on client errors (4xx)
75 | if (response && response.status >= 400 && response.status < 500) {
76 | throw new BlobKitError(
77 | BlobKitErrorCode.PROXY_ERROR,
78 | `Proxy request failed: ${response.status} ${errorText}`
79 | );
80 | }
81 |
82 | // Retry on server errors (5xx)
83 | if (attempt < maxRetries && response && response.status >= 500) {
84 | const delay = baseDelay * Math.pow(2, attempt);
85 | this.logger.info(
86 | `Proxy request failed with ${response.status}, retrying in ${delay}ms...`
87 | );
88 | await sleep(delay);
89 | continue;
90 | }
91 |
92 | throw new BlobKitError(
93 | BlobKitErrorCode.PROXY_ERROR,
94 | `Proxy request failed: ${errorText}`
95 | );
96 | }
97 |
98 | const result = await response.json();
99 |
100 | if (!result.success) {
101 | throw new BlobKitError(
102 | BlobKitErrorCode.PROXY_ERROR,
103 | result.error || 'Proxy request failed'
104 | );
105 | }
106 |
107 | return {
108 | blobTxHash: result.blobTxHash,
109 | blockNumber: result.blockNumber,
110 | blobHash: result.blobHash,
111 | commitment: result.commitment,
112 | proofs: result.proofs,
113 | blobIndex: result.blobIndex,
114 | completionTxHash: result.completionTxHash
115 | };
116 | } catch (error) {
117 | // If it's already a BlobKitError, re-throw
118 | if (error instanceof BlobKitError) {
119 | throw error;
120 | }
121 |
122 | // Network errors - retry if we have attempts left
123 | if (attempt < maxRetries) {
124 | const delay = baseDelay * Math.pow(2, attempt);
125 | this.logger.info(`Network error, retrying in ${delay}ms...`, { error: String(error) });
126 | await sleep(delay);
127 | continue;
128 | }
129 |
130 | throw new BlobKitError(
131 | BlobKitErrorCode.NETWORK_ERROR,
132 | `Failed to submit blob: ${error instanceof Error ? error.message : String(error)}`
133 | );
134 | }
135 | }
136 |
137 | throw new BlobKitError(BlobKitErrorCode.PROXY_ERROR, 'Max retries exceeded');
138 | }
139 |
140 | /**
141 | * Get proxy health status
142 | */
143 | async getHealth(): Promise {
144 | try {
145 | const response = await fetch(`${this.config.proxyUrl}/api/v1/health`, {
146 | signal: AbortSignal.timeout(5000)
147 | });
148 |
149 | if (!response.ok) {
150 | throw new Error(`Health check failed: ${response.status}`);
151 | }
152 |
153 | return await response.json();
154 | } catch (error) {
155 | throw new BlobKitError(
156 | BlobKitErrorCode.NETWORK_ERROR,
157 | `Failed to check proxy health: ${error instanceof Error ? error.message : String(error)}`
158 | );
159 | }
160 | }
161 |
162 | /**
163 | * Discover proxy URL for a given chain
164 | */
165 | static async discover(chainId: number, currentUrl?: string): Promise {
166 | // Check current URL first
167 | if (currentUrl) {
168 | try {
169 | const response = await fetch(`${currentUrl}/api/v1/health`, {
170 | signal: AbortSignal.timeout(5000)
171 | });
172 |
173 | if (response.ok) {
174 | const health = await response.json();
175 | if (health.chainId === chainId) {
176 | return currentUrl;
177 | }
178 | }
179 | } catch {
180 | // Continue to discovery
181 | }
182 | }
183 |
184 | // Check environment variable
185 | const envUrl = process.env?.BLOBKIT_PROXY_URL;
186 | if (envUrl) {
187 | try {
188 | const response = await fetch(`${envUrl}/api/v1/health`, {
189 | signal: AbortSignal.timeout(5000)
190 | });
191 |
192 | if (response.ok) {
193 | const health = await response.json();
194 | if (health.chainId === chainId) {
195 | return envUrl;
196 | }
197 | }
198 | } catch {
199 | // Continue to error
200 | }
201 | }
202 |
203 | throw new BlobKitError(
204 | BlobKitErrorCode.PROXY_NOT_FOUND,
205 | `No proxy found for chain ${chainId}. Please configure proxyUrl.`
206 | );
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to BlobKit
2 |
3 | Thank you for your interest in contributing to BlobKit! This document provides guidelines and information for contributors.
4 |
5 | ## Code of Conduct
6 |
7 | By participating in this project, you agree to abide by our Code of Conduct. Please report any unacceptable behavior to Zak Cole at zcole@linux.com.
8 |
9 | ## Getting Started
10 |
11 | ### Prerequisites
12 |
13 | - Node.js 16.0.0 or higher
14 | - npm or yarn package manager
15 | - Git
16 |
17 | ### Setting Up Development Environment
18 |
19 | 1. Fork the repository on GitHub
20 | 2. Clone your fork locally:
21 |
22 | ```bash
23 | git clone https://github.com/YOUR_USERNAME/blobkit.git
24 | cd blobkit
25 | ```
26 |
27 | 3. Install dependencies:
28 |
29 | ```bash
30 | npm install
31 | ```
32 |
33 | 4. Create a `.env` file for testing:
34 |
35 | ```bash
36 | cp .env.example .env
37 | # Edit .env with your test configuration
38 | ```
39 |
40 | 5. Run tests to ensure everything works:
41 | ```bash
42 | npm test
43 | ```
44 |
45 | ## Development Workflow
46 |
47 | ### Before Making Changes
48 |
49 | 1. Create a new branch for your feature or fix:
50 |
51 | ```bash
52 | git checkout -b feature/your-feature-name
53 | ```
54 |
55 | 2. Ensure all tests pass:
56 |
57 | ```bash
58 | npm test
59 | ```
60 |
61 | 3. Check code quality:
62 | ```bash
63 | npm run lint
64 | npm run typecheck
65 | ```
66 |
67 | ### Making Changes
68 |
69 | 1. **Follow TypeScript best practices**
70 | - Use strict typing
71 | - Add JSDoc comments for public APIs
72 | - Follow existing code patterns
73 |
74 | 2. **Write tests for new functionality**
75 | - Unit tests for all new functions
76 | - Integration tests for API changes
77 | - Maintain or improve test coverage
78 |
79 | 3. **Follow security practices**
80 | - Validate all inputs
81 | - Use structured error handling
82 | - Never hardcode sensitive information
83 |
84 | ### Code Style
85 |
86 | - Use TypeScript strict mode
87 | - Follow existing naming conventions
88 | - Use meaningful variable and function names
89 | - Add comprehensive error handling
90 | - Document complex algorithms or cryptographic operations
91 |
92 | ### Commit Guidelines
93 |
94 | Use conventional commit messages:
95 |
96 | ```
97 | type(scope): description
98 |
99 | feat(kzg): add new KZG proof verification
100 | fix(blob): resolve encoding issue with large data
101 | docs(readme): update installation instructions
102 | test(utils): add edge cases for blob validation
103 | ```
104 |
105 | Types:
106 |
107 | - `feat`: New features
108 | - `fix`: Bug fixes
109 | - `docs`: Documentation changes
110 | - `test`: Test additions or fixes
111 | - `refactor`: Code refactoring
112 | - `perf`: Performance improvements
113 | - `chore`: Maintenance tasks
114 |
115 | ## Testing
116 |
117 | ### Running Tests
118 |
119 | ```bash
120 | # Run all tests
121 | npm test
122 |
123 | # Run tests in watch mode
124 | npm run test:watch
125 |
126 | # Run tests with coverage
127 | npm run test:coverage
128 |
129 | # Run specific test file
130 | npm test -- blob.test.ts
131 | ```
132 |
133 | ### Writing Tests
134 |
135 | - Place tests in the `test/` directory
136 | - Mirror the source structure in test organization
137 | - Test both success and failure cases
138 | - Include edge cases and boundary conditions
139 | - Mock external dependencies appropriately
140 |
141 | ### Test Categories
142 |
143 | 1. **Unit Tests**: Test individual functions and classes
144 | 2. **Integration Tests**: Test component interactions
145 | 3. **Edge Case Tests**: Test boundary conditions and error cases
146 | 4. **Performance Tests**: Verify performance characteristics (disabled by default)
147 |
148 | ## Documentation
149 |
150 | ### Code Documentation
151 |
152 | - Add JSDoc comments to all public APIs
153 | - Include parameter descriptions and examples
154 | - Document error conditions and return types
155 | - Explain complex algorithms or cryptographic operations
156 |
157 | ### README Updates
158 |
159 | - Update feature lists for new functionality
160 | - Add new configuration options
161 | - Include new usage examples
162 | - Update API reference sections
163 |
164 | ## Security
165 |
166 | ### Reporting Security Issues
167 |
168 | Do not open public issues for security vulnerabilities. Instead:
169 |
170 | 1. Email security concerns to the maintainers
171 | 2. Include detailed reproduction steps
172 | 3. Provide potential fix suggestions if available
173 |
174 | ### Security Guidelines
175 |
176 | - Validate all user inputs
177 | - Use parameterized queries and safe APIs
178 | - Never log sensitive information
179 | - Follow cryptographic best practices
180 | - Use audited libraries for cryptographic operations
181 |
182 | ## Performance
183 |
184 | ### Performance Considerations
185 |
186 | - Optimize for memory efficiency
187 | - Use zero-copy operations where possible
188 | - Pre-allocate buffers for known sizes
189 | - Avoid unnecessary object creation in hot paths
190 | - Profile performance-critical code paths
191 |
192 | ### Benchmarking
193 |
194 | - Add benchmarks for new performance-critical features
195 | - Ensure changes don't regress existing performance
196 | - Document performance characteristics
197 |
198 | ## Pull Request Process
199 |
200 | ### Before Submitting
201 |
202 | 1. Ensure all tests pass
203 | 2. Run the full lint and type check suite
204 | 3. Update documentation if needed
205 | 4. Add changelog entries for significant changes
206 |
207 | ### Pull Request Requirements
208 |
209 | 1. **Clear Description**
210 | - Explain what the PR does
211 | - Reference related issues
212 | - Include screenshots for UI changes
213 |
214 | 2. **Code Quality**
215 | - All tests pass
216 | - No linting errors
217 | - Type checking passes
218 | - Good test coverage
219 |
220 | 3. **Documentation**
221 | - Update relevant documentation
222 | - Add JSDoc for new public APIs
223 | - Update changelog if appropriate
224 |
225 | ### Review Process
226 |
227 | 1. Maintainers will review your PR
228 | 2. Address feedback and requested changes
229 | 3. Ensure CI checks pass
230 | 4. PR will be merged after approval
231 |
232 | ## Release Process
233 |
234 | 1. Update version in package.json
235 | 2. Update CHANGELOG.md
236 | 3. Create release notes
237 | 4. Tag the release
238 | 5. Publish to npm
239 |
240 | ## Getting Help
241 |
242 | - Open an issue for bugs or feature requests
243 | - Join discussions for questions about implementation
244 | - Contact Zak Cole at zcole@linux.com for security concerns or direct support
245 |
246 | ## Recognition
247 |
248 | Contributors will be recognized in:
249 |
250 | - Release notes for significant contributions
251 | - Project documentation
252 | - Special thanks for major features or fixes
253 |
254 | Thank you for contributing to BlobKit!
255 |
--------------------------------------------------------------------------------