Skip to content

Building a Crypto Wallet: Architecture and Security

DodaTech Updated 2026-06-22 8 min read

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

Is it safe to store my seed phrase in a password manager?

This is debated. Password managers are encrypted but online. For significant value, use metal or paper backups stored in multiple secure locations. For smaller amounts, a password manager is acceptable if it uses strong encryption and two-factor authentication.

What happens if I lose my hardware wallet?

HD wallets can be recovered from the seed phrase on any compatible wallet. Buy a new hardware wallet, restore from the mnemonic, and all addresses and funds are accessible. Always keep your seed phrase separate from the hardware device.

Can I use the same mnemonic for multiple blockchains?

Yes. BIP-44 path differentiates chains: Ethereum uses m/44'/60'/0'/0/0, Bitcoin uses m/44'/0'/0'/0/0, Polygon shares Ethereum's path. A single mnemonic can manage keys across all blockchains.

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