A secure client-server web application demonstrating the use of Elliptic Curve Digital Signatures (ECDSA) for authorizing cryptocurrency-like transfers between accounts. This project shows how to implement public key cryptography in a practical web application to ensure that only the rightful owner of an account can move funds.
This project was completed as part of the Alchemy University Blockchain Developer Bootcamp. It is a centralized simulation and does not implement a blockchain, but it demonstrates cryptography, signing, and secure transaction validation in a web app.
- Secure transactions: Users can only transfer funds if they control the private key corresponding to their account.
- Digital signatures: Transactions are signed with ECDSA and verified by the server.
- Ethereum-style addresses: Account addresses are derived from public keys, similar to Ethereum.
- Real-time balance updates: Transfers update balances on the server in real time.
- Client-server architecture: React frontend and Node.js backend using Express.
/client -> React + Vite frontend
/server -> Node.js + Express backend
/scripts -> CLI utilities (wallet generation & message signing)
git clone https://github.com/0-x-joseph/ecdsa-web-wallet.git
cd ecdsa-web-walletcd client
npm install
npm run devVisit http://localhost:5173 in your browser.
cd ../server
npm install
node index
# or, for auto-reloading:
# npm i -g nodemon
# nodemon indexServer runs on port 3042 by default.
You can run the full stack (frontend + backend) using Docker Compose without manually starting npm on each service.
From the root of the project (where docker-compose.yml lives):
docker-compose up --build- This will build the frontend and backend images and start the containers
- The frontend container listens on 5173
- The backend container listens on 4000 (or the port you configured)
- Compose automatically sets up a network so the frontend can reach the backend at
http://backend:4000
Open your browser:
http://localhost:5173
- The frontend will load and communicate with the backend through the internal Docker network
This application follows a real cryptographic transaction flow, similar to how wallets work in blockchain systems.
Navigate to the scripts folder and run the wallet generation script:
cd scripts
node wallet-gen.jsThis will output:
- A private key (keep this secret!)
- A public key
- An Ethereum-style address
👉 Use the generated address in the web app as your wallet address. 👉 The private key is never sent to the server.
- Open the web app at
http://localhost:5173 - Paste your generated address into the wallet address input
- Your balance (stored on the server) will be displayed
When you want to send funds:
-
Enter:
- Recipient address
- Amount
-
The app will construct a message representing the transaction
-
Use the signing script to sign that message with your private key
From the scripts folder:
node sign-msg.jsYou will be prompted for:
- Your private key (hidden while typing)
- The exact message shown in the app
The script outputs:
- Signature
- Recovery bit
Back in the web app:
- Paste the signature
- Paste the recovery bit
- Submit the transaction
✨ Voilà! The server:
- Recovers the public key from the signature
- Derives the sender’s address
- Verifies ownership
- Transfers the funds if the signature is valid
-
The client never sends private keys.
-
Transactions are signed client-side using ECDSA.
-
The server:
- Verifies the signature
- Recovers the public key from
(signature + recovery bit) - Matches the derived address against stored balances
-
Funds move only if cryptographic ownership is proven.
sequenceDiagram
participant User
participant WalletScript as Wallet / Signing Script
participant Client as Web Client (React)
participant Server as Backend (Node + Express)
User->>WalletScript: Generate private & public key
WalletScript-->>User: Address (derived from public key)
User->>Client: Enter address
Client->>Server: Request balance
Server-->>Client: Return balance
User->>Client: Create transaction (to, amount)
Client-->>User: Message to sign
User->>WalletScript: Sign message with private key
WalletScript-->>User: Signature + Recovery Bit
User->>Client: Submit signature + recovery bit
Client->>Client: Recover public key
Client->>Client: Derive sender address
Client->>Server: {sender, recipient, amount, signature}
Server->>Server: Verify signature & balance
Server-->>Client: Transaction result
import * as secp from "ethereum-cryptography/secp256k1.js";
import { toHex } from "ethereum-cryptography/utils.js";
import { keccak256 } from "ethereum-cryptography/keccak.js";
const privateKey = secp.utils.randomPrivateKey();
console.log("Private Key:", toHex(privateKey));
const publicKey = secp.getPublicKey(privateKey);
console.log("Public Key:", toHex(publicKey));
const address = toHex(keccak256(publicKey)).slice(-20);
console.log("Address: " + address);import * as secp from "ethereum-cryptography/secp256k1.js";
import { keccak256 } from "ethereum-cryptography/keccak.js";
import { toHex, utf8ToBytes } from "ethereum-cryptography/utils.js";
import promptSync from "prompt-sync";
const prompt = promptSync({ sigint: true }); // allow Ctrl+C
function hashMessage(message) {
return keccak256(utf8ToBytes(message));
}
async function main() {
const privateKey = prompt("Enter your private key: ", { echo: "*" });
const message = prompt("Enter your message: ");
const [signature, recoveryBit] = await secp.sign(
hashMessage(message),
privateKey,
{ recovered: true },
);
console.log("\nSignature:", toHex(signature));
console.log("Recovery Bit:", recoveryBit);
}
main();- Private keys never leave the user’s machine
- The server relies entirely on signature verification
- Replay attacks are possible in this demo (no nonce system)
- This project is educational, not production-ready
In this project, public key recovery and sender address derivation are performed on the frontend.
Because the backend trusts the sender address provided by the client, anyone could impersonate another address by submitting a forged request. In production systems, the backend must independently recover the public key from the signature and derive the sender address to establish cryptographic ownership.
This design choice was made intentionally to:
- Keep the project simple and beginner-friendly
- Focus on understanding ECDSA signing and verification
- Avoid overloading the backend logic early on
🔒 Do not use this architecture in production.
- Implementing ECDSA signing and verification
- Recovering public keys from signatures
- Designing a secure client–server crypto flow
- Ethereum-style address derivation
- Hands-on cryptography via Alchemy University
- Add nonces to prevent replay attacks
- Build an in-app wallet generator
- Add transaction history
- Explore multi-signature wallets
- Connect to a real blockchain backend
- Alchemy University Blockchain Developer Bootcamp
- Noble secp256k1
- Ethereum Cryptography
- ECDSA Overview
