A blockchain implementation with a layered Express backend and a React frontend.
For Applicants: See INSTRUCTIONS.md for task requirements (2 tasks, 4–6 hours). See SETUP.md for a quick-start guide.
Backend
POST /api/wallets— generates a secp256k1 key pair (publicKeyas SPKI DER hex,privateKeyas PKCS#8 PEM) viacrypto.generateKeyPairSync.Transaction.signTransaction(signingKey)— signs the transaction hash with Nodecrypto.sign/ ECDSA; accepts PEM orKeyObject.Transaction.isValid()— rejects unsigned user transactions; mining rewards (fromAddress === null) remain valid.Blockchain.addTransaction()— requires a non-empty signature before accepting a transaction.POST /api/transactions— body now includestimestampandsignature(client signs before submit).- Demo seed uses real wallets and signed transactions instead of plain-string addresses.
Frontend
Walletcomponent — generates a wallet, shows address and balance, keeps the private key in React state only.TransactionForm— signs with the stored private key (src/utils/walletSigning.js+@noble/curves) before posting.- Mine block pays the active wallet’s public key when a wallet is loaded.
- Balance display includes pending transfers immediately; shows on-chain confirmed balance when they differ.
New / updated files: utils/walletCrypto.js, controllers/wallet.controller.js, routes/wallet.routes.js, src/components/Wallet.js, src/utils/walletSigning.js, and related API/UI wiring.
services/persistence.service.js—save(blockchain),load(), andclear()read/writeblockchain.jsonin the project root (gitignored).models/index.js— restores from disk on startup when valid; otherwise seeds demo data. Auto-saves after every successfuladdTransactionandminePendingTransactions.- Corrupt JSON, invalid shape, empty chain, or failed
isChainValid()→ log a warning, remove the bad file, start fresh (no crash).
No new env vars were introduced. Existing variables still apply:
| Variable | Default | Purpose |
|---|---|---|
PORT |
3002 |
API server port |
NODE_ENV |
development |
Logging level |
CORS_ORIGIN |
http://localhost:3000 |
Allowed frontend origin |
BLOCKCHAIN_DIFFICULTY |
2 |
Proof-of-work difficulty |
BLOCKCHAIN_MINING_REWARD |
100 |
Coins per mined block |
INITIAL_MINER_ADDRESS |
genesis-miner |
Demo seed mining reward recipient |
SEED_DEMO_DATA |
enabled (!== 'false') |
Seed demo chain when no valid snapshot exists |
Persistence path is fixed at ./blockchain.json (not configurable via env).
@noble/curves,@noble/hashes— browser-side ECDSA signing compatible with Nodecrypto.verify.
- Single process, synchronous I/O —
writeFileSync/readFileSyncblock the event loop briefly; fine for a demo, not for high throughput. - No spend validation — the chain accepts signed transfers even when the sender’s balance is insufficient; balances are informational only.
- Private keys in the browser — stored in React state only, but not encrypted at rest; refreshing the page loses the key unless you export it yourself.
- Browser signing dependency — frontend uses
@noble/curvesto match Node signatures; PEM parsing on the client is minimal (secp256k1 PKCS#8 only). - Multi-tab / multi-wallet — a second browser tab does not auto-sync balances; use Refresh Balance after the other tab mines.
- Demo seed vs snapshot — if
blockchain.jsonloads successfully, demo seed is skipped even whenSEED_DEMO_DATAis enabled. - Config drift — changing
BLOCKCHAIN_DIFFICULTYorBLOCKCHAIN_MINING_REWARDin env does not migrate an existing file; difficulty/reward come from the saved snapshot when loaded.
hometask-blockchain/
│
├── config/
│ └── index.js # Environment config (port, CORS, blockchain settings)
│
├── models/
│ ├── blockchain.js # Block, Transaction, Blockchain domain classes
│ └── index.js # Singleton instance + demo data seeding
│
├── services/
│ └── persistence.service.js # save / load / clear blockchain.json
│
├── utils/
│ ├── logger.js # Levelled logger (error / warn / info / debug)
│ ├── response.js # Unified sendSuccess / sendCreated / sendError helpers
│ ├── validator.js # isValidAddress, isValidAmount, sanitizers
│ └── walletCrypto.js # secp256k1 key generation helpers
│
├── middleware/
│ ├── cors.middleware.js # CORS policy
│ ├── logger.middleware.js # Morgan HTTP request logger
│ ├── errorHandler.middleware.js# Centralised error handler (must be last)
│ ├── notFound.middleware.js # 404 handler
│ ├── validateRequest.middleware.js # validateBody / validateParams factories
│ └── rateLimit.middleware.js # apiLimiter (100 req/min) + writeLimiter (20 req/min)
│
├── routes/
│ ├── index.js # Aggregates all /api sub-routes
│ ├── blockchain.routes.js # /api/chain
│ ├── transaction.routes.js # /api/transactions
│ ├── wallet.routes.js # /api/wallets
│ ├── mining.routes.js # /api/mine
│ ├── balance.routes.js # /api/balance
│ ├── stats.routes.js # /api/stats
│ └── health.routes.js # /health (no rate limit)
│
├── controllers/
│ ├── blockchain.controller.js
│ ├── transaction.controller.js
│ ├── wallet.controller.js
│ ├── mining.controller.js
│ ├── balance.controller.js
│ └── stats.controller.js
│
├── src/ # React frontend
│ ├── api/
│ │ ├── client.js # Axios instance with request/response interceptors
│ │ ├── endpoints.js # All API URL constants
│ │ └── blockchain.api.js # Typed fetch functions (fetchChain, addTransaction…)
│ ├── hooks/
│ │ ├── useBlockchain.js # Polls /api/chain + /api/stats, returns state
│ │ └── usePolling.js # Reusable interval-based polling hook
│ ├── utils/
│ │ ├── formatters.js # truncateHash, formatTimestamp, formatAmount
│ │ ├── helpers.js # isPositiveNumber, groupTransactionsByBlock, etc.
│ │ └── walletSigning.js # Client-side transaction hash + ECDSA sign
│ ├── constants/
│ │ └── index.js # POLL_INTERVAL_MS, DEFAULT_MINER_ADDRESS, enums
│ ├── components/
│ │ ├── BlockchainViewer.js
│ │ ├── Wallet.js
│ │ ├── TransactionForm.js
│ │ ├── StatsPanel.js
│ │ ├── Header.js
│ │ └── ErrorBoundary.js # React class error boundary
│ ├── App.js
│ └── index.js
│
├── blockchain.js # Backward-compat re-export → models/blockchain.js
├── server.js # Entry point — wires middleware, routes, starts server
└── package.json
- Node.js v16 or higher
- npm
npm install# Terminal 1 — React dev server on http://localhost:3000
npm start
# Terminal 2 — API server on http://localhost:3002, with auto-reload
npm run devThe React app proxies all /api/* requests to the API server automatically via src/setupProxy.js.
npm run serve # builds the React app, then serves everything from port 3002All API responses share a common envelope:
{ "success": true, ...payload }
{ "success": false, "error": "message" }| Method | Path | Description |
|---|---|---|
| GET | /api/chain |
Full chain + length |
| GET | /api/chain/valid |
{ isValid: bool } |
| Method | Path | Description |
|---|---|---|
| POST | /api/transactions |
Add a pending transaction |
| GET | /api/transactions/pending |
All pending transactions |
| GET | /api/transactions/all |
All confirmed transactions |
POST /api/transactions body:
{
"fromAddress": "<spki-der-hex-public-key>",
"toAddress": "<spki-der-hex-public-key>",
"amount": 100,
"timestamp": 1710000000000,
"signature": "<ecdsa-der-hex>"
}| Method | Path | Description |
|---|---|---|
| POST | /api/wallets |
Generate { publicKey, privateKey } (PEM private key) |
| Method | Path | Description |
|---|---|---|
| POST | /api/mine |
Mine pending transactions into a new block |
POST /api/mine body:
{ "miningRewardAddress": "miner1" }| Method | Path | Description |
|---|---|---|
| GET | /api/balance/:address |
Confirmed balance of an address |
| Method | Path | Description |
|---|---|---|
| GET | /api/stats |
Chain length, difficulty, validity, pending count |
| Method | Path | Description |
|---|---|---|
| GET | /health |
Server uptime, env, timestamp — no rate limit |
The React app is organised into distinct concerns:
src/api/— all network calls live here. Components never callfetch/axiosdirectly.src/hooks/useBlockchain— single source of truth for chain + stats state; polls every 5 s.src/utils/formatters— pure formatting functions (hash truncation, timestamps, amounts).src/constants/— magic strings and numbers in one place.ErrorBoundary— catches any unhandled React render errors gracefully.
- Node.js + Express
morgan— HTTP request loggingdotenv— environment variable loadingexpress-rate-limit— API rate limitingcors— CORS policy middleware- Node.js built-in
crypto— SHA-256 hashing, secp256k1 wallets, ECDSA verify
- React 18
- Axios (with interceptors)
@noble/curves/@noble/hashes— browser ECDSA signing- CSS3 (glassmorphism, gradients, animations)
Port already in use
# Use a different port
PORT=3003 npm run devFrontend can't reach the API
- Confirm
npm run devis running on port 3002 - Confirm
src/setupProxy.jstarget matchesPORT
Chain resets on every restart
- State is persisted to
blockchain.json. Delete that file (or callpersistence.clear()from a script) to start fresh. - Corrupt
blockchain.jsonis removed automatically; the server falls back to demo seed whenSEED_DEMO_DATAis enabled.
MIT — for learning and assessment purposes.