Building a crypto wallet from scratch empowers developers to create secure, customizable tools for managing digital assets. This comprehensive guide walks you through the process, focusing on a Node.js-based Ethereum wallet prototype using libraries like ethereum-cryptography and ethers.js, while covering key concepts, security best practices, and advanced features.[1][2]

Whether you’re a beginner aiming for a simple prototype or an experienced developer targeting production-ready apps, you’ll find actionable steps, code examples, and curated resources here.

Understanding Crypto Wallets: Types and Fundamentals

Crypto wallets are software or hardware tools that generate, store, and manage public-private key pairs for interacting with blockchains. They don’t store cryptocurrencies themselves but control access via private keys.[2][6]

Key Wallet Types

  • Custodial Wallets: Managed by third parties (e.g., exchanges); easier but less secure as you don’t control keys.[3]
  • Non-Custodial Wallets: You control keys; ideal for self-sovereignty (e.g., MetaMask clones).[5]
  • Hot Wallets: Online/software-based for quick access.[1]
  • Cold Wallets: Offline/hardware for maximum security (e.g., Ledger, Trezor).[3]
  • HD Wallets: Hierarchical Deterministic, using seed phrases for multiple accounts (BIP-39 standard).[6]

For this tutorial, we’ll build a non-custodial hot wallet prototype supporting Ethereum and ERC-20 tokens, similar to a MetaMask clone.[1][5]

Core Components:

  • Key generation and storage.
  • Blockchain connection (e.g., via RPC providers).
  • Transaction signing and broadcasting.
  • Balance querying and UI integration.[2][6]

Prerequisites and Tech Stack

Before coding, ensure you have:

  • Node.js (v18+).
  • Yarn or npm.
  • Basic JavaScript knowledge.
  • A testnet RPC (e.g., Alchemy or Infura for Ethereum Sepolia).[1][5]

Essential Libraries:

  • ethereum-cryptography: For low-level key operations.
  • ethers.js: For wallet interactions, providers, and transactions.
  • Optional: React/Next.js for frontend UI.[1][2]

Security Note: Never expose private keys in production. Use hardware security modules (HSMs) or secure enclaves for real apps.[2][6]

Step 1: Set Up Your Development Environment

Create a new project folder and initialize it.

mkdir my-crypto-wallet
cd my-crypto-wallet
yarn init -y
yarn add ethereum-cryptography ethers
yarn add -D typescript @types/node  # Optional for TypeScript

For a full example, clone the Chainlink repo:

git clone https://github.com/smartcontractkit/smart-contract-examples.git
cd smart-contract-examples/my-crypto-wallet
yarn install

[1]

This sets up a backend prototype. For frontend, initialize a React app:

npx create-react-app wallet-frontend --template typescript
cd wallet-frontend
yarn add ethers

[2]

Step 2: Generate Keys and Addresses (HD Wallet Basics)

Wallets derive addresses from a seed phrase using BIP-39/44 standards. Start with a root key and generate child accounts.

Create 01_generate.js:

const { hdkey } = require('ethereum-cryptography/hdkey');
const { toHex } = require('ethereum-cryptography/utils');
const bip39 = require('ethereum-cryptography/bip39');  // Add: yarn add ethereum-cryptography/bip39

async function main() {
  const mnemonic = bip39.generateMnemonic();  // 12-24 word seed
  console.log('Seed Phrase:', mnemonic);

  const seed = await bip39.mnemonicToSeed(mnemonic);
  const hdRootKey = hdkey.fromMasterSeed(seed);
  
  const accountOneIndex = "m/44'/60'/0'/0/0";  // BIP-44 path for ETH
  const accountOnePrivateKey = hdRootKey.derive(accountOneIndex).privateKey;
  const accountOnePublicKey = /* Derive public key */;
  const accountOneAddress = /* Get ETH address */;

  console.log('Account Address:', `0x${toHex(accountOneAddress)}`);
}

main().catch(console.error);

[1][6]

Run: node 01_generate.js. Securely back up the seed phrase—it’s your wallet recovery key.[3]

Step 3: Connect to Blockchain and Query Balances

Use ethers.js to connect to a provider (e.g., public RPC).

Create 02_balance.js:

const { ethers } = require('ethers');

async function getBalance(address, providerUrl = 'https://sepolia.infura.io/v3/YOUR_KEY') {
  const provider = new ethers.JsonRpcProvider(providerUrl);
  const balance = await provider.getBalance(address);
  console.log('Balance:', ethers.formatEther(balance), 'ETH');
}

getBalance('YOUR_ADDRESS');

[1][2]

For ERC-20 tokens, add contract ABI queries.[5]

Step 4: Implement Send/Receive Transactions

Sign and broadcast transactions with a Wallet instance.

Create 03_send.js:

const { ethers } = require('ethers');

async function main(receiverAddress, ethAmount) {
  const provider = new ethers.JsonRpcProvider('YOUR_RPC_URL');
  const accountData = JSON.parse(fs.readFileSync('account.json'));  // Load securely
  const privateKey = Object.values(accountData.privateKey);
  const signer = new ethers.Wallet(privateKey, provider);

  const tx = await signer.sendTransaction({
    to: receiverAddress,
    value: ethers.parseEther(ethAmount),
  });
  
  console.log('Transaction:', tx.hash);
  await tx.wait();  // Confirm
}

main(process.argv[2], process.argv[3]);

[1]

Run: node 03_send.js 0xReceiverAddress 0.01

Pro Tip: Always simulate transactions (provider.call) before signing to prevent errors.[2]

Step 5: Build a Frontend UI (React Example)

Integrate into React for a MetaMask-like interface.

In App.js:

import { ethers } from 'ethers';
import { useState } from 'react';

function App() {
  const [provider, setProvider] = useState(null);
  const [signer, setSigner] = useState(null);
  const [balance, setBalance] = useState('');

  const connectWallet = async () => {
    if (window.ethereum) {
      const prov = new ethers.BrowserProvider(window.ethereum);
      await prov.send('eth_requestAccounts', []);
      const sign = await prov.getSigner();
      setProvider(prov);
      setSigner(sign);
      const bal = await prov.getBalance(await sign.getAddress());
      setBalance(ethers.formatEther(bal));
    }
  };

  return (
    <div>
      <button onClick={connectWallet}>Connect Wallet</button>
      <p>Balance: {balance} ETH</p>
    </div>
  );
}

[2][5]

Add transaction forms for send/receive with QR codes.[4]

Step 6: Advanced Features and Security Hardening

  • Multi-Chain Support: Use libraries like wagmi for EVM chains.[2]
  • Seed Phrase Recovery: Implement BIP-39 validation.
  • Transaction History: Query via provider.getLogs or TheGraph.
  • Security:
    • Encrypt private keys with user passwords (use crypto.subtle).
    • Multi-sig for high-value wallets.
    • Audit for reentrancy, phishing vectors.[6]
  • Testing: Use Anvil (Foundry) for local blockchain: anvil.[5]
FeatureLibrary/ToolPurpose
Key Genethereum-cryptographySecure primitives[1]
Providerethers.jsRPC interactions[1][2]
Local ChainFoundry AnvilTesting[5]
UIReact + wagmiUser-friendly dApp[2]

Common Pitfalls and Best Practices

  • Never hardcode keys; use environment variables or vaults.
  • Test on testnets first (Sepolia, Goerli).
  • Comply with regulations (KYC for custodial).[3][7]
  • Open-source and audit code for trust.

Conclusion

Building a crypto wallet from scratch demystifies blockchain interaction and equips you with production-grade skills. Start with the Node.js prototype above, expand to a full React dApp, and iterate based on user needs. Prioritize security—lost keys mean lost funds.

This guide provides a functional starting point; scale it with multi-currency support and hardware integration for real-world use. Happy coding!

Resources and Further Reading

Deploy your prototype today and join the decentralized future!