- swr.ts: remove useBandwidth(), drop bw from useDashboard() - events/route.ts: skip bandwidth SSE (Kubo 0.28.0 unsupported) - usage/page.tsx: remove getBWStats call + bw references - helpers.ts: gatewayLink -> ipfs.maos.dedyn.io (subdomain only) - upload/page.tsx: fix local gatewayLink (subdomain only) - storage.ts: update default gatewayUrl, add defensive getHistory()
13 KiB
IPFS Portal — Architecture
Overview
IPFS Portal is a Next.js SPA (Single Page Application) that provides a browser-based interface for managing an IPFS node, uploading files, and handling crypto payments via a smart contract. All pages are 'use client' — no server-side rendering.
Stack
| Layer | Technology |
|---|---|
| Frontend | Next.js 14+ (React, 'use client' SPA) |
| Styling | Tailwind CSS + custom glass/surface design tokens |
| Icons | Lucide React |
| Wallet | EIP-6963 + viem |
| Blockchain | zkSync Local (chain 270) |
| IPFS | Kubo RPC API (port 5001) |
| User Mgmt | Python backend (port 8444) |
| IPFS Gateway | https://maos.dedyn.io/ipfs/<CID> |
System Architecture
BROWSER (React SPA)
│
┌─────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
11 Pages (SPA) WalletProvider PaymentService
┌──────────────┐ (EIP-6963) (viem, chain 270)
│ apiFetch() │ │ │
│ wrapper │ │ │
└──────┬───────┘ │ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ Next.js API Route: [...path]/route.ts │
│ │
│ matchRoute(path) → │
│ 'health' → inline JSON │
│ 'users' → proxyUserAPI() → :8444 (Python) │
│ default → proxyIPFS() → :5001 (Kubo) │
│ │
│ mapToKuboAPI(path) → /api/v0/* translation │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Kubo IPFS │ │ Python User API │
│ Daemon │ │ (port 8444) │
│ (port 5001) │ │ │
│ │ │ htpasswd mgmt │
│ /api/v0/id │ │ Auth: X-Admin- │
│ /api/v0/swarm │ │ Key header │
│ /api/v0/pin/* │ │ │
│ /api/v0/add │ └──────────────────┘
│ /api/v0/ls │
│ /api/v0/name/ │
│ /api/v0/repo │ CHAIN 270
│ /api/v0/bw │ ┌──────────────────┐
└─────────────────┘ │ Smart Contract │
│ 0xCBc6... │
│ zkSync Local │
│ RPC :3050 │
│ │
│ payWithETH() │
│ payWithToken() │
│ getPrice() │
│ configureToken()│
└──────────────────┘
Pages
| Page | Route | Lines | Backend Dependencies | Description |
|---|---|---|---|---|
| Landing | / |
~130 | Kubo (health) | Hero + connect wallet + quick stats |
| Dashboard | /dashboard |
253 | Kubo (peers, pins, health, repo, bw) | Node overview, stats, bandwidth |
| Upload | /upload |
590 | Kubo + Smart Contract | Quick + crypto file upload |
| Explorer | /explorer |
244 | Kubo (ls, cat) | IPFS content browser |
| IPNS | /ipns |
285 | Kubo (keys, publish, resolve) | IPNS name management |
| Users | /users |
275 | Python API + Smart Contract | htpasswd users + wallet lookup |
| Peers | /peers |
63 | Kubo (swarm/peers) | Connected peers list |
| Pins | /pins |
167 | Kubo (pin/ls, add, rm) | Pin management |
| Admin/Payment | /admin/payment |
536 | Smart Contract | Owner payment dashboard |
| Settings | /settings |
319 | localStorage only | App configuration |
| History | /history |
393 | localStorage only | Upload history viewer |
Note: IPNS page does NOT wrap in PortalLayout — appears standalone. All other pages use the shared layout.
Data Flow
1. API Flow (all pages except Settings, History)
Page Component
→ apiFetch('/api/endpoint')
→ fetch('/api/endpoint', { credentials: 'include' })
→ Next.js [...path]/route.ts
→ matchRoute(path)
→ proxyIPFS() → Kubo RPC :5001
→ proxyUserAPI() → Python :8444
2. Upload Flow (Quick)
User drags file
→ UploadPage state: FileEntry[]
→ startUpload() — sequential per file
→ uploadFile(file) → apiFetch POST /api/files/upload (FormData)
→ proxyIPFS → Kubo /api/v0/add
→ addToHistory({ cid, name, size }) → localStorage
→ Show results (CID, gateway link, copy)
3. Upload Flow (Crypto)
User selects file
→ PaymentPanel shown
→ WalletProvider.connectWallet() → get address
→ PaymentService.getPrice(bytes, addr) → { finalWei, isFree }
→ PaymentService.getSupportedTokens() → [ETH, USDC, MAOS]
→ PaymentService.getMAOSDiscountTiers() → discount %
→ User pays
→ Step 1: uploadFile(file) → Kubo → get CID
→ Step 2a (free): onPaid({ method: 'free' })
→ Step 2b (ETH): payWithETH(provider, cid, size, wei)
→ sendTransaction → contract.payWithETH(cid, bytesSize)
→ Step 2c (token): approveAndPayWithToken(...)
→ check allowance → approve if needed → contract.payWithToken(...)
→ addToHistory({ method: 'eth' }) → localStorage
4. Payment Admin Flow
Admin connects wallet
→ Check isOwner (compare connected address to contract.owner)
→ Load: getOwner, getBasePricePerMB, getFreeTierBytes,
getSupportedTokens, getMAOSDiscountTiers, getTotalUploads
→ Per token: getTotalRevenue, getBalance (ETH) / balanceOf (ERC20)
→ Owner can: setPrice, setFreeTier, configureToken,
add/remove discount tiers, withdraw funds
Key Modules
src/lib/api.ts (217 lines)
Central API layer. Exports:
| Function | HTTP | Endpoint | Category |
|---|---|---|---|
getNodeInfo() |
GET | /api/node/info |
Node |
getPeers() |
GET | /api/peers |
Peers |
listPins() / addPin() / removePin() |
GET/POST/DELETE | /api/pins |
Pins |
uploadFile(file) |
POST (FormData) | /api/files/upload |
Files |
listUsers() / createUser() / deleteUser() |
GET/POST/DELETE | /api/users |
Users |
explorerLs() / explorerCat() / explorerStat() |
POST | /api/explorer/* |
Explorer |
listIPNSKeys() / ipnsPublish() / ipnsResolve() / ipnsGenKey() |
GET/POST | /api/ipns/* |
IPNS |
getRepoStats() / getBWStats() |
GET | /api/repo / /api/bw |
Stats |
checkHealth() |
GET | /api/health |
Health |
src/lib/wallet.ts (196 lines)
Wallet connection layer.
- EIP-6963 event-based provider discovery (
discoverWallets) - Legacy
window.ethereumfallback - MAOS Wallet preferred (
window.maosv6) - Chain 270 (zkSync Local) switch + add
- Helpers:
formatWeiToETH,formatBytes,signMessage,getBalance
src/lib/payment.ts (539 lines)
Smart contract interaction via viem.
- 28 methods wrapping ABI functions
- Supports ETH and ERC20 token payments
- Read:
getPrice,getUserStats,getSupportedTokens,getMAOSDiscountTiers - Write:
payWithETH,approveAndPayWithToken - Admin:
adminSetPricePerMB,adminConfigureToken,adminWithdrawETH, etc. - Contract:
0xCBc6b8aeea129c206F4836799621C833Bf8B9BDeon chain 270
src/lib/storage.ts (98 lines)
localStorage persistence.
PortalSettings: gateway URL, API endpoint, storage max, refresh, themeUploadRecord: history with dedup by CID+name, capped at 500 entries
src/app/api/[...path]/route.ts (~320 lines)
Catch-all API proxy. Routes:
| Path | Handler | Backend |
|---|---|---|
/api/health |
handleHealth() |
Inline JSON |
/api/users* |
proxyUserAPI() |
Python :8444 |
/api/explorer/* / /api/ipns/* / default |
proxyIPFS() |
Kubo :5001 |
mapToKuboAPI(path) translates Portal API paths to Kubo RPC endpoints:
/node/info→/api/v0/id/peers→/api/v0/swarm/peers/pins→/api/v0/pin/ls|add|rm/files/upload→/api/v0/add/explorer/ls→/api/v0/ls/ipns/publish→/api/v0/name/publish/repo/stats→/api/v0/repo/stat/bw/stats→/api/v0/bw
Smart Contract (Chain 270 — zkSync Local)
Address: 0xCBc6b8aeea129c206F4836799621C833Bf8B9BDe
RPC: http://tom1687.no-ip.org:3050
Functions
| Category | Functions |
|---|---|
| Payment | payWithETH(cid, bytesSize) (payable), payWithToken(cid, bytes, symbol) |
| Pricing | getPrice(bytes, user) → (totalWei, discountBps, finalWei, isFree) |
| Free Tier | freeTierBytes(), userTotalBytes(user) |
| Token Mgmt | configureToken(symbol, addr, decimals, weiPerToken, enabled) |
| Discounts | getMAOSDiscountTiers(), setMAOSDiscountTier(minBal, bps), removeMAOSDiscountTier(index) |
| Admin | setPricePerMB(wei), setFreeTierBytes(bytes), withdrawETH(to), withdrawToken(symbol, to), transferOwnership(to) |
| Stats | totalRevenue(symbol), totalUploads(), getUserStats(user) |
| Events | UploadPaidETH, UploadPaidToken, FreeUploadUsed |
Known Bug
Contract returns duplicate token entries — admin page deduplicates on symbol.
Graph Analysis
Static (AST) View
- 251 nodes, 401 edges, 17 communities
- Graphify knowledge graph at
graphify-out/graph.json - 79 isolated nodes (type definitions, interfaces, scripts)
Bridge Nodes
| Node | Edges | Role |
|---|---|---|
PaymentService |
31 | Bridges Smart Contract ↔ UI (Payments) |
WalletProvider |
15 | Bridges Wallet ↔ PaymentService |
Graph Gap
The AST parser cannot trace URL-routed connections. The runtime flow:
apiFetch('/api/node/info') → fetch('/api/node/info')
→ [...path]/route.ts → proxyIPFS() → Kubo :5001
Appears as 3 disconnected communities in the graph but is one continuous flow at runtime.
Code Quality Summary
Issues Found (24 total)
| Severity | Count | Key Items |
|---|---|---|
| 🔴 Critical | 4 | XSS, unhandled promises, silent catch blocks, oversized files |
| 🟡 Medium | 5 | 15× @ts-expect-error, 8× as any, duplicate helpers, precision loss |
| 🟢 Low | 3 | Unused imports, console.* logging, IPNS layout inconsistency |
Priority Fixes
- Window type augmentation — Declare
Windowinterface inwallet.tsto eliminate 15@ts-expect-error - Extract duplicate patterns — Consolidate
formatBytes,gatewayLink,truncateCidintolib/helpers.ts - Add error logging — Every empty catch block gets
console.erroror structured logging - Split oversized files —
upload/page.tsx(590→2 components),admin/payment/page.tsx(536→4 views),payment.ts(539→abi/types/service) - Fix XSS — Add DOMPurify or replace
dangerouslySetInnerHTMLinFilePreview.tsx - Fix unhandled promises — Add
.catch()toisDeployed().then() - Replace
as anycasts — Use proper viemparseAbitypes instead ofas unknown as X
Configuration
- API_BASE:
''(empty — same origin; nginx routes/api/*to backend) - Kubo RPC:
http://127.0.0.1:5001 - Python API:
http://192.168.1.176:8444(htpasswd + admin key) - Smart Contract:
0xCBc6...on chain 270 (zkSync Local) - IPFS Gateway:
https://maos.dedyn.io/ipfs/ - Refresh Interval: 15s (Dashboard)
- Max Uploads: 20 files, 100MB each
- Storage Max: 30 GB (localStorage setting)
- Admin Key:
maos-admin-2024(X-Admin-Key header)