Building a Crypto Wallet: Architecture and Security
In this tutorial, you'll learn crypto wallet development including key management, HD wallets, transaction signing, hardware wallet integration, and security best practices. Why it matters: Wallets are the primary user interface for Web3, managing over $1 trillion in assets, and wallet security directly determines whether users retain control of their funds against theft and loss. By the end, you'll build a functional non-custodial wallet.
A crypto wallet is a software or hardware system that manages private keys, signs transactions, and interacts with blockchain networks, allowing users to send, receive, and manage digital assets without a trusted intermediary.
HD Wallet Architecture
Hierarchical Deterministic (HD) wallets generate all keys from a single seed phrase using BIP-32/BIP-39/BIP-44 standards.
flowchart TD A[Mnemonic Phrase
12 or 24 words] --> B[Seed
PBKDF2] B --> C[Master Key
BIP-32 Root] C --> D[Purpose: 44'] D --> E[Coin: 60' (ETH)] E --> F[Account: 0'] F --> G[External Chain: 0] F --> H[Internal Chain: 1] G --> I[Address 0] G --> J[Address 1] H --> K[Change Address 0] style A fill:#ff9999 style B fill:#ffcc99 style C fill:#ffff99
const { ethers } = require("ethers");
// Generate a wallet from mnemonic
function createHDWallet(mnemonic, derivationPath = "m/44'/60'/0'/0/0") {
// Validate mnemonic
if (!mnemonic) {
mnemonic = ethers.Mnemonic.fromEntropy(ethers.randomBytes(32));
}
// Create HD wallet
const hdNode = ethers.HDNodeWallet.fromPhrase(
mnemonic.phrase || mnemonic,
undefined,
derivationPath
);
return {
mnemonic: hdNode.mnemonic.phrase,
privateKey: hdNode.privateKey,
address: hdNode.address,
derivationPath: hdNode.path,
publicKey: hdNode.publicKey,
fingerprint: hdNode.fingerprint,
};
}
// Generate first 5 Ethereum addresses from same seed
function deriveAddresses(mnemonic, count = 5) {
const addresses = [];
for (let i = 0; i < count; i++) {
const path = `m/44'/60'/0'/0/${i}`;
const wallet = ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, path);
addresses.push({
index: i,
address: wallet.address,
path: path,
privateKey: wallet.privateKey,
});
}
return addresses;
}
// Create wallet
const wallet = createHDWallet();
console.log("Mnemonic:", wallet.mnemonic);
console.log("Address:", wallet.address);
// Derive multiple addresses
const addresses = deriveAddresses(wallet.mnemonic, 3);
addresses.forEach(addr => {
console.log(`${addr.path}: ${addr.address}`);
});
// Expected output:
// Mnemonic: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
// Address: 0x...
// m/44'/60'/0'/0/0: 0x...
// m/44'/60'/0'/0/1: 0x...
// m/44'/60'/0'/0/2: 0x...
Key Management and Storage
Private keys must be stored securely. Different storage methods offer different security levels.
| Storage Method | Security Level | Convenience | Use Case |
|---|---|---|---|
| Memory (variable) | Low | High | Browser wallets (MetaMask) |
| Local storage | Low | High | Hot wallets |
| Encrypted keystore | Medium | Medium | Software wallets |
| Hardware wallet | High | Low | Cold storage |
| Multi-party computation (MPC) | Very high | Medium | Institutional wallets |
const { ethers } = require("ethers");
const fs = require("fs");
// Encrypt a private key with a password (JSON keystore)
async function encryptKeystore(privateKey, password) {
const wallet = new ethers.Wallet(privateKey);
const encryptedJson = await wallet.encrypt(password, {
scrypt: {
N: 131072, // 2^17 - higher = more secure but slower
r: 8,
p: 1,
},
});
// Save to file
fs.writeFileSync("wallet.json", encryptedJson);
console.log("Wallet encrypted and saved to wallet.json");
return encryptedJson;
}
// Decrypt keystore
async function decryptKeystore(password) {
const encryptedJson = fs.readFileSync("wallet.json", "utf8");
const wallet = await ethers.Wallet.fromEncryptedJson(encryptedJson, password);
return {
address: wallet.address,
privateKey: wallet.privateKey,
provider: wallet.provider,
};
}
// Usage
encryptKeystore("0x...privatekey...", "strong_password123").then(() => {
console.log("Encryption complete");
});
// Expected output:
// Wallet encrypted and saved to wallet.json
// Encryption complete
Transaction Signing
Sign transactions securely without exposing the private key to the network.
const { ethers } = require("ethers");
class WalletSigner {
constructor(privateKey, provider) {
this.wallet = new ethers.Wallet(privateKey, provider);
}
async signAndSendTransaction(to, valueInEth, data = "0x") {
// Build transaction
const tx = {
to: to,
value: ethers.parseEther(valueInEth),
data: data,
};
// Estimate gas (optional but recommended)
let gasEstimate;
try {
gasEstimate = await this.wallet.estimateGas(tx);
console.log("Estimated gas:", gasEstimate.toString());
} catch (error) {
console.log("Gas estimation failed, using default");
gasEstimate = 21000n;
}
// Get fee data
const feeData = await this.wallet.provider.getFeeData();
// Sign the transaction locally
const signedTx = await this.wallet.signTransaction({
...tx,
gasLimit: gasEstimate,
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
nonce: await this.wallet.getNonce(),
chainId: (await this.wallet.provider.getNetwork()).chainId,
});
console.log("Transaction signed locally");
console.log("Signed hex:", signedTx.slice(0, 20) + "...");
// Broadcast
const response = await this.wallet.provider.broadcastTransaction(signedTx);
console.log("Transaction sent:", response.hash);
// Wait for confirmation
const receipt = await response.wait();
console.log("Confirmed in block:", receipt.blockNumber);
console.log("Status:", receipt.status === 1 ? "Success" : "Failed");
return receipt;
}
async signMessage(message) {
const signature = await this.wallet.signMessage(message);
console.log("Message signed");
// Verify signature
const recovered = ethers.verifyMessage(message, signature);
const isValid = recovered.toLowerCase() === this.wallet.address.toLowerCase();
console.log("Signature valid:", isValid);
return signature;
}
}
// Usage
const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const signer = new WalletSigner("0x...privatekey...", provider);
signer.signAndSendTransaction("0x...", "0.1").then(receipt => {
console.log("Transaction complete:", receipt.hash);
});
// Expected output:
// Estimated gas: 21000
// Transaction signed locally
// Transaction sent: 0x7a3f...
// Confirmed in block: 5
// Status: Success
Hardware Wallet Integration
Connect to hardware wallets like Ledger or Trezor for secure key management.
const { ethers } = require("ethers");
const { LedgerSigner } = require("@ethers-ext/signer-ledger");
async function connectLedger() {
// Connect to Ledger via USB
const signer = new LedgerSigner(undefined, "live", "hid");
console.log("Connected to Ledger");
// Get first address
const address = await signer.getAddress();
console.log("Ledger address:", address);
// Get multiple addresses
const paths = [
"m/44'/60'/0'/0/0",
"m/44'/60'/0'/0/1",
"m/44'/60'/0'/0/2",
];
for (const path of paths) {
const addr = await signer.getAddress(path);
console.log(`${path}: ${addr}`);
}
// Sign transaction (requires physical approval on Ledger)
const tx = await signer.sendTransaction({
to: "0x...",
value: ethers.parseEther("0.01"),
});
console.log("Transaction sent. Check Ledger for approval.");
console.log("Hash:", tx.hash);
}
// Expected output:
// Connected to Ledger
// Ledger address: 0x...
// m/44'/60'/0'/0/0: 0x...
// m/44'/60'/0'/0/1: 0x...
// Transaction sent. Check Ledger for approval.
// Hash: 0x...
Building a Simple Wallet UI
Create a React-based wallet interface.
import { useState, useEffect } from "react";
import { ethers } from "ethers";
function WalletApp() {
const [wallet, setWallet] = useState(null);
const [balance, setBalance] = useState("0");
const [address, setAddress] = useState("");
const [network, setNetwork] = useState("");
const [txHash, setTxHash] = useState("");
async function createWalletFromMnemonic(mnemonic) {
const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic);
const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const connectedWallet = hdNode.connect(provider);
setWallet(connectedWallet);
setAddress(connectedWallet.address);
const network = await provider.getNetwork();
setNetwork(network.name);
const bal = await provider.getBalance(connectedWallet.address);
setBalance(ethers.formatEther(bal));
}
async function sendETH(to, amount) {
if (!wallet) return;
try {
const tx = await wallet.sendTransaction({
to: to,
value: ethers.parseEther(amount),
});
setTxHash(tx.hash);
const receipt = await tx.wait();
console.log("Confirmed:", receipt.blockNumber);
// Update balance
const bal = await wallet.provider.getBalance(wallet.address);
setBalance(ethers.formatEther(bal));
} catch (error) {
console.error("Transaction failed:", error.message);
}
}
return (
<div>
<h1>Simple Wallet</h1>
{!wallet ? (
<div>
<p>Enter mnemonic phrase to restore wallet, or generate a new one.</p>
<button onClick={() => createWalletFromMnemonic("")}>
Generate New Wallet
</button>
</div>
) : (
<div>
<p>Address: {address}</p>
<p>Balance: {balance} ETH</p>
<p>Network: {network}</p>
<h2>Send ETH</h2>
<input type="text" placeholder="Recipient address" id="to" />
<input type="text" placeholder="Amount in ETH" id="amount" />
<button onClick={() => {
const to = document.getElementById("to").value;
const amount = document.getElementById("amount").value;
sendETH(to, amount);
}}>
Send
</button>
{txHash && <p>Transaction: {txHash}</p>}
</div>
)}
</div>
);
}
Common Errors and Misunderstandings
1. Sharing Private Keys
Never share private keys or seed phrases. No legitimate service will ask for them. Anyone with the seed phrase controls all derived accounts.
2. Confusing Address with Public Key
The address is a hash of the public key. The public key is derived from the private key. Only the private key can sign transactions.
3. Forgetting Mnemonic Backups
Lost mnemonic = lost funds. Store seed phrases offline (paper, metal). Never store digitally (screenshots, cloud storage, email).
4. Using Weak Passwords for Keystore
Encrypted keystores are only as secure as their password. Use strong passwords (16+ chars) and consider hardware wallets for significant value.
5. Not Testing Recovery
Test wallet recovery on testnets before using on mainnet. Verify that your mnemonic correctly restores all addresses and balances.
Practice Questions
What is a BIP-39 mnemonic phrase? A human-readable encoding of entropy used to generate a wallet seed. Typically 12 or 24 words from a standardized word list. The seed is used in BIP-32 hierarchical key derivation.
How does HD wallet key derivation work? From a master seed (derived from mnemonic via PBKDF2), HD wallets use BIP-32 to derive child keys hierarchically. BIP-44 defines the path structure:
m/purpose'/coin'/account'/chain/address.What is the difference between hot and cold wallets? Hot wallets are connected to the internet (browser, mobile) for convenience. Cold wallets are offline (hardware, paper) for security. Hot wallets should hold small amounts, cold wallets for long-term storage.
How does a hardware wallet protect private keys? Private keys never leave the device. Transactions are signed internally and the signed transaction is exported. The private key is protected by the device's secure element and PIN.
What is a keystore file? A JSON file (UTC/Web3 standard) containing an encrypted private key. Encrypted with a user password using scrypt key derivation. Standard format:
UTC--<date>--<address>.json.
Challenge
Build a complete non-custodial wallet application using Node.js and ethers.js with: BIP-39 mnemonic generation and recovery, HD wallet address derivation (first 10 addresses), encrypted keystore storage, transaction signing and broadcast, ERC-20 token balance display, and transaction history. Include a CLI interface.
Real-World Task
Create a multi-chain wallet using React and ethers.js that manages accounts on Ethereum, Polygon, Arbitrum, and zkSync Era from a single mnemonic. Implement: HD key derivation per EIP-2304 (multicoin), balance aggregation with USD value, cross-chain transfer via bridges, hardware wallet (Ledger) support, and encrypted keystore export compatible with MetaMask.
Frequently Asked Questions
Next Steps
After building a wallet, explore DeFi Protocols for wallet-integrated lending and swapping, then study Web3 Security for protecting wallet users.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro