Web Crypto Wallet Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Web Crypto Wallet Development
Medium
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Web Crypto Wallet Development

A crypto wallet is not a place to store coins. Coins are stored on-chain, the wallet stores the private key to sign them. This fundamental distinction differs from a bank account and determines the entire architecture: where and how to store the key, how to sign transactions, how to protect against compromise.

A web wallet is the most accessible option (no need to install an extension or app), but also the most attacked: browser environment, XSS threats, supply chain attacks on JavaScript dependencies. MetaMask, Coinbase Wallet, Rainbow — browser extensions, not pure web. Pure web wallet requires additional security measures.

Wallet Types and Architectural Choice

EOA vs Smart Account

EOA (Externally Owned Account) — classical wallet. One private key → one address. Simple, compatible with everything. Problem: loss of key = permanent loss of access. No social recovery, multi-factor auth, spending limits.

Smart Account (ERC-4337 Account Abstraction) — smart contract wallet. Transaction validation logic is programmable. Capabilities: social recovery (restoration via trusted contacts), session keys (limited keys for dApp without full access), batched transactions (multiple operations in one call), gas sponsorship (paymaster pays gas for user), signature verification with any algorithm (not just ECDSA secp256k1).

For consumer web wallet in 2024-2025 — recommend ERC-4337. Teach users recovery methods understandable to ordinary people instead of seed phrases.

Key Storage in Browser

Critical security question. Options from least to most secure:

localStorage / sessionStorage — never use for private keys. Accessible to any JavaScript on page, any XSS.

IndexedDB with encryption — private key encrypted via Web Crypto API (AES-GCM 256-bit) with key derived from user password (PBKDF2 or Argon2 with high cost factor). Encrypted blob stored in IndexedDB. This is the standard for browser wallets.

async function encryptPrivateKey(
  privateKey: Uint8Array,
  password: string
): Promise<{ encrypted: ArrayBuffer; salt: Uint8Array; iv: Uint8Array }> {
  const salt = crypto.getRandomValues(new Uint8Array(32));
  const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit for GCM
  
  // Derive encryption key from password
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(password),
    "PBKDF2",
    false,
    ["deriveKey"]
  );
  
  const encryptionKey = await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt,
      iterations: 600000, // OWASP recommends 600k for SHA-256
      hash: "SHA-256",
    },
    keyMaterial,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt"]
  );
  
  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    encryptionKey,
    privateKey
  );
  
  return { encrypted, salt, iv };
}

WebAuthn + hardware key — most secure option. Wallet's private key never leaves WebAuthn authenticator (platform key in TPM/Secure Enclave, or hardware key like YubiKey). Signing operation happens inside the device.

Implementation via Passkey: user creates passkey (WebAuthn credential), private key generated in Secure Enclave iPhone/Android/Windows TPM. For ERC-4337 wallet — smart contract can verify P-256 (secp256r1) WebAuthn signatures. Daimo and Coinbase Smart Wallet use this approach.

MPC Wallets

Multi-Party Computation: private key never exists completely in one place. Split into shares among several parties (user + server, or user + device 1 + device 2). Signature requires threshold participants, but none know the full key.

MPC-as-a-service providers: Privy, Dynamic, Web3Auth, Fireblocks Web3. For non-custodial MPC — user holds one share, service holds another. Service compromise doesn't mean key compromise.

Minus MPC: signing is slower (multi-round protocol) and requires both parties available. If service is down — can't sign.

Key Derivation and HD Wallets

BIP-39 and BIP-44

Standard Web3 wallet uses seed phrase (mnemonic) — 12 or 24 words from BIP-39 dictionary. From seed via PBKDF2 (2048 iterations), master private key is derived. From master key via BIP-32 derivation, child keys are derived by path.

BIP-44 defines standard derivation path: m/purpose'/coin_type'/account'/change/address_index.

For Ethereum: m/44'/60'/0'/0/0 — first account. m/44'/60'/0'/0/1 — second. This allows one seed phrase to manage infinite addresses deterministically. Import seed into MetaMask, Rainbow, Ledger — get same addresses.

import { mnemonicToSeed } from "@scure/bip39";
import { HDKey } from "@scure/bip32";

async function deriveAccount(mnemonic: string, index: number) {
  const seed = await mnemonicToSeed(mnemonic);
  const masterKey = HDKey.fromMasterSeed(seed);
  
  const derivationPath = `m/44'/60'/0'/0/${index}`;
  const childKey = masterKey.derive(derivationPath);
  
  return {
    privateKey: childKey.privateKey!,
    address: computeAddress(childKey.publicKey!),
  };
}

Libraries: @scure/bip39 and @scure/bip32 — modern, audited, tree-shakeable replacements for noble-secp256k1 and bip39.

Web Wallet Architecture

Separation of Concerns: UI vs Signing

Critical rule: code that has access to private key must be isolated from code that interacts with dApps and internet. XSS in UI layer should not compromise key.

Implementation via Web Worker or Service Worker: signing operations happen in isolated worker, UI layer doesn't have direct key access. Worker receives signature request, shows user what's being signed, gets confirmation, returns signature (not key).

// Main thread — UI
async function signTransaction(txRequest: TransactionRequest): Promise<string> {
  return new Promise((resolve, reject) => {
    const worker = new Worker("/signing-worker.js");
    
    worker.postMessage({ type: "SIGN_TX", payload: txRequest });
    
    worker.onmessage = (e) => {
      if (e.data.type === "SIGNED") resolve(e.data.signature);
      if (e.data.type === "REJECTED") reject(new Error("User rejected"));
      if (e.data.type === "ERROR") reject(new Error(e.data.error));
    };
  });
}

// signing-worker.js — isolated worker with key access
self.onmessage = async (e) => {
  if (e.data.type === "SIGN_TX") {
    const approved = await requestUserApproval(e.data.payload);
    if (!approved) {
      self.postMessage({ type: "REJECTED" });
      return;
    }
    const signature = await signWithStoredKey(e.data.payload);
    self.postMessage({ type: "SIGNED", signature });
  }
};

WalletConnect and dApp Integration

WalletConnect v2 — standard for wallet-to-dApp connection. Works via relay server: dApp creates pairing QR-code or deep link, wallet scans, encrypted session established. All requests (eth_signTypedData, eth_sendTransaction) go through this channel.

For web wallet: @walletconnect/web3wallet SDK. Wallet acts as "wallet" (not "dapp") in WalletConnect terminology.

import { Web3Wallet } from "@walletconnect/web3wallet";
import { Core } from "@walletconnect/core";

const core = new Core({ projectId: WALLETCONNECT_PROJECT_ID });
const web3wallet = await Web3Wallet.init({
  core,
  metadata: {
    name: "My Wallet",
    description: "Custom Web3 Wallet",
    url: "https://mywallet.app",
    icons: ["https://mywallet.app/icon.png"],
  },
});

// Handle incoming requests from dApps
web3wallet.on("session_request", async (event) => {
  const { id, topic, params } = event;
  const { request } = params;
  
  if (request.method === "eth_sendTransaction") {
    const approved = await showTransactionConfirmation(request.params[0]);
    if (approved) {
      const txHash = await sendTransaction(request.params[0]);
      await web3wallet.respondSessionRequest({
        topic,
        response: { id, result: txHash, jsonrpc: "2.0" },
      });
    } else {
      await web3wallet.respondSessionRequest({
        topic,
        response: { id, error: { code: 4001, message: "User rejected" }, jsonrpc: "2.0" },
      });
    }
  }
});

EIP-1193 Provider

For direct dApp integration via window.ethereum: wallet injects EIP-1193 compatible provider into DOM. This is what MetaMask does. For web wallet in tab (not extension) — limited to same origin, not ideal.

Alternative: Service Worker as background process (Progressive Web App) that can inject provider and handle requests from other tabs of same origin.

ERC-4337 Account Abstraction Implementation

UserOperation Flow

Instead of regular Ethereum transaction, ERC-4337 introduces UserOperation — data structure signed by wallet and sent to Bundler (not directly to mempool):

interface UserOperation {
  sender: string;          // smart account address
  nonce: bigint;
  initCode: Hex;           // for new account creation
  callData: Hex;           // what account should execute
  callGasLimit: bigint;
  verificationGasLimit: bigint;
  preVerificationGas: bigint;
  maxFeePerGas: bigint;
  maxPriorityFeePerGas: bigint;
  paymasterAndData: Hex;   // paymaster for gas sponsorship
  signature: Hex;          // owner signature
}

Bundler collects UserOperations, packs in batch, sends through EntryPoint contract. EntryPoint validates signatures and executes operations.

Wallet interacts with Bundler via JSON-RPC API (ERC-4337 specific methods: eth_sendUserOperation, eth_getUserOperationByHash). Bundler providers: Pimlico, Alchemy, Biconomy.

Session Keys

Session key — limited key without full account access. User creates session key for specific dApp on specific period. dApp uses this key to sign operations — user doesn't need to confirm each transaction.

For GameFi: user creates session key for 8 hours of gaming. Key can only call game-specific contracts, can't transfer ETH or ERC20. After 8 hours — expires automatically.

Implementation: via SmartAccount validator module (ZeroDev Kernel, Safe modules). Session key policy stored in smart contract.

Security

Threats and Mitigation

XSS attacks. Content Security Policy (strict: script-src 'self' 'wasm-unsafe-eval'), Subresource Integrity for external scripts, avoid innerHTML, use React (automatic escaping). Regular npm audit and Snyk for supply chain analysis.

Clipboard attacks. User copies address, malware substitutes address in clipboard. Solution: show first and last N characters of address, ask to visually verify. Some wallets show address as identicon (unique avatar) — faster to notice substitution.

Phishing. Fake wallet website. Solution: PWA with verified domain, browser-level warnings, ENS for verification.

Seed phrase exposure. Showing seed phrase in UI — maximum risk. Never transmit seed over network, show only on creation, require explicit user action for viewing, add "are you alone?" warning.

Private key in memory. After decryption, key lives in JavaScript heap. Garbage collector doesn't guarantee immediate clearing. Mitigation: Uint8Array.fill(0) after use, store key in Worker where GC is more predictable.

Dependency Audit

Wallet uses many crypto libraries. Critical to check:

  • @noble/secp256k1 or @noble/curves — transaction signing
  • @scure/bip32, @scure/bip39 — HD derivation
  • ethers or viem — chain interaction

All @noble/* and @scure/* libraries from Paulmillr — audited, minimalist, no dependencies. Prefer them over more "corporate" alternatives.

Multi-chain Support

Ethereum-compatible chains (Polygon, Arbitrum, Base, Optimism) — one address, different RPCs. Sufficient to support EIP-155 chain ID and RPC switching.

Bitcoin or Solana — different signing algorithm (secp256k1 with different format / ed25519), different derivation path. Requires separate key logic.

For multi-chain wallet: abstract chain-specific logic behind common interface. IChainSigner with signTransaction(tx) method — each chain has own implementation.

Stack and Timeline

Component Technology Timeline
Key management Web Crypto API + @scure 3-4 weeks
HD wallet (BIP-39/44) @scure/bip32 + bip39 1-2 weeks
Transaction signing viem + ethers 2-3 weeks
ERC-4337 integration permissionless.js + Pimlico 3-4 weeks
WalletConnect v2 @walletconnect/web3wallet 2-3 weeks
UI (React + TypeScript) React + Tailwind 5-7 weeks
Security hardening CSP + Web Worker isolation 2-3 weeks
Multi-chain Chain abstraction layer 3-4 weeks
Testing + audit prep Vitest + Playwright 3-4 weeks

MVP web wallet (EOA, Ethereum, basic UI): 8-10 weeks. Production-ready with ERC-4337, multi-chain, WalletConnect, WebAuthn: 6-9 months.

Security audit mandatory before production launch: wallet stores user keys, one vulnerability = funds loss for entire user base. Recommend audit + bug bounty program (Immunefi) from launch.