├── .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 | --------------------------------------------------------------------------------