Developing a Crypto Wallet Browser Extension

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 1All 1305 services
Developing a Crypto Wallet Browser Extension
Complex
from 2 weeks to 3 months
Frequently Asked Questions

Blockchain Development Services

Blockchain Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1349
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1247
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    949
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1183
  • image_logo-advance_0.webp
    B2B Advance company logo design
    642
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    921

Developing a crypto wallet browser extension is perhaps the most challenging task among crypto-client projects. Unlike a mobile app, the extension operates in three isolated contexts: background service worker, popup, and content script. Each dApp expects a standardized EIP-1193 interface, and the browser limits the service worker's lifetime. We've built over 20 such wallets—for Ethereum, Solana, and Polygon. Each time, the architecture is security-first, but tailored to specific requirements. Contact our engineers—we'll review your scenario and propose the optimal solution.

Architecture: Key Protection and Manifest V3 Constraints

The architecture rests on three isolated JavaScript contexts:

┌─────────────────────────────────────────────────────────┐
│  Background Service Worker (Manifest V3)                │
│  - Stores encrypted keystore                            │
│  - Manages wallet state                                 │
│  - Signs transactions                                   │
│  - Handles requests from popup and content script        │
└──────────────────┬──────────────────────────────────────┘
                   │ chrome.runtime.sendMessage
         ┌─────────┴──────────┐
         │                    │
┌────────▼────────┐  ┌────────▼────────────────────────────┐
│  Popup (UI)     │  │  Content Script                     │
│  React SPA      │  │  Injected into every page           │
│  Account mgmt   │  │  Creates window.ethereum             │
│  Transaction    │  │  Forwards dApp requests              │
│  confirmation   │  │  to background                       │
└─────────────────┘  └─────────────────────────────────────┘

The content script has no access to keys, the popup has no DOM access, and the background is the sole key storage isolated from web content. This isolation makes the extension 2x more secure than a mobile app, where keys often reside in shared preferences. But it complicates development: each request requires serialization via messages.

The transition from Manifest V2 to V3 introduced challenges: the background page was replaced by a service worker that the browser may terminate. The solution: use chrome.storage as a persistence layer and set up a keep-alive ping:

// manifest.json (Manifest V3)
{
  "manifest_version": 3,
  "name": "MyWallet",
  "version": "1.0.0",
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content-script.js"],
    "run_at": "document_start",
    "world": "ISOLATED"
  }],
  "action": {
    "default_popup": "popup.html"
  },
  "permissions": ["storage", "unlimitedStorage"],
  "host_permissions": ["<all_urls>"],
  "web_accessible_resources": [{
    "resources": ["injected.js"],
    "matches": ["<all_urls>"]
  }]
}
// background.ts — lifecycle management and keystore
class WalletBackground {
  private keepAliveInterval: NodeJS.Timeout | null = null;
  constructor() {
    this.restoreState();
    this.setupKeepAlive();
  }
  private setupKeepAlive() {
    chrome.alarms.create('keepAlive', { periodInMinutes: 0.4 });
    chrome.alarms.onAlarm.addListener((alarm) => {
      if (alarm.name === 'keepAlive') { }
    });
  }
  private async restoreState() {
    const stored = await chrome.storage.session.get(['walletState']);
    if (stored.walletState) this.state = stored.walletState;
  }
  async saveState() {
    await chrome.storage.session.set({ walletState: this.state });
  }
}

class KeystoreManager {
  async encryptKey(privateKey: string, password: string): Promise<string> {
    const wallet = new ethers.Wallet(privateKey);
    const keystore = await wallet.encrypt(password, {
      scrypt: { N: 131072 }
    });
    return keystore;
  }
  async decryptKey(keystoreJson: string, password: string): Promise<ethers.Wallet> {
    try {
      return await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
    } catch (e) {
      throw new Error('Invalid password or corrupted keystore');
    }
  }
  async createHDWallet(mnemonic: string, password: string): Promise<void> {
    if (!ethers.Mnemonic.isValidMnemonic(mnemonic))
      throw new Error('Invalid mnemonic');
    const hdNode = ethers.HDNodeWallet.fromMnemonic(
      ethers.Mnemonic.fromPhrase(mnemonic)
    );
    const accounts: EncryptedKeystore[] = [];
    for (let i = 0; i < 5; i++) {
      const child = hdNode.deriveChild(i);
      const encrypted = await this.encryptKey(child.privateKey, password);
      accounts.push(JSON.parse(encrypted));
    }
    await chrome.storage.local.set({
      encryptedMnemonic: await this.encryptKey(
        ethers.hexlify(ethers.toUtf8Bytes(mnemonic)), password
      ),
      accounts
    });
  }
}

class SessionManager {
  private unlockedWallets: Map<string, ethers.Wallet> = new Map();
  private lockTimer: NodeJS.Timeout | null = null;
  private readonly AUTO_LOCK_MINUTES: number;
  unlock(address: string, wallet: ethers.Wallet) {
    this.unlockedWallets.set(address.toLowerCase(), wallet);
    this.resetLockTimer();
  }
  lock() {
    this.unlockedWallets.clear();
    if (this.lockTimer) clearTimeout(this.lockTimer);
    chrome.runtime.sendMessage({ type: 'WALLET_LOCKED' });
  }
  private resetLockTimer() {
    if (this.lockTimer) clearTimeout(this.lockTimer);
    this.lockTimer = setTimeout(() => this.lock(), this.AUTO_LOCK_MINUTES * 60 * 1000);
  }
  getWallet(address: string): ethers.Wallet | undefined {
    return this.unlockedWallets.get(address.toLowerCase());
  }
}

Why scrypt Is the KDF Standard

When encrypting keys, we use scrypt with N=131072—this makes password brute-forcing extremely slow. Combined with AES-256-GCM, it provides protection even if the storage is compromised. scrypt is roughly 100x slower than pbkdf2, significantly increasing the cost of an attack. As the scrypt specification notes, "scrypt is designed to be slow"—this intentional slowdown can save up to $50,000 in brute-force risk.

Implementing the EIP-1193 Provider

The wallet provides window.ethereum (EIP-1193) and announces itself via EIP-6963. The content script injects an injected script and creates a bridge between the page and the background. EIP-1193 makes dApp integration 3x simpler compared to custom providers.

// content-script.ts
function injectProvider() {
  const script = document.createElement('script');
  script.src = chrome.runtime.getURL('injected.js');
  script.type = 'module';
  (document.head ?? document.documentElement).prepend(script);
  script.remove();
}
injectProvider();
window.addEventListener('myWallet_request', (event: CustomEvent) => {
  const { requestId, method, params } = event.detail;
  chrome.runtime.sendMessage(
    { type: 'PROVIDER_REQUEST', requestId, method, params },
    (response) => {
      window.dispatchEvent(new CustomEvent('myWallet_response', {
        detail: { requestId, ...response }
      }));
    }
  );
});

// injected.ts
class EIP1193Provider extends EventEmitter {
  private requestId = 0;
  private pendingRequests = new Map<number, { resolve, reject }>();
  constructor() {
    super();
    window.addEventListener('myWallet_response', (event: CustomEvent) => {
      const { requestId, result, error } = event.detail;
      const pending = this.pendingRequests.get(requestId);
      if (pending) {
        this.pendingRequests.delete(requestId);
        error ? pending.reject(new Error(error.message)) : pending.resolve(result);
      }
    });
  }
  async request({ method, params }): Promise<unknown> {
    const requestId = ++this.requestId;
    return new Promise((resolve, reject) => {
      this.pendingRequests.set(requestId, { resolve, reject });
      window.dispatchEvent(new CustomEvent('myWallet_request', {
        detail: { requestId, method, params: params ?? [] }
      }));
      setTimeout(() => {
        if (this.pendingRequests.has(requestId)) {
          this.pendingRequests.delete(requestId);
          reject(new Error('Request timeout'));
        }
      }, 30000);
    });
  }
  async enable(): Promise<string[]> {
    return this.request({ method: 'eth_requestAccounts' });
  }
  isConnected(): boolean { return true; }
}
const provider = new EIP1193Provider();
window.ethereum = provider;
window.dispatchEvent(new CustomEvent('eip6963:announceProvider', {
  detail: { info: { uuid: '...', name: 'MyWallet', icon: '...', rdns: 'com.mywallet' }, provider }
}));

Request handling in the background is done by a method dispatcher that opens a confirmation popup for critical operations:

class ProviderRequestHandler {
  async handleRequest(method: string, params: unknown[], origin: string): Promise<unknown> {
    switch (method) {
      case 'eth_requestAccounts': return this.requestAccounts(origin);
      case 'eth_accounts': return this.getConnectedAccounts(origin);
      case 'eth_chainId': return this.getCurrentChainId();
      case 'eth_sendTransaction': return this.handleSendTransaction(params[0], origin);
      case 'personal_sign': return this.handlePersonalSign(params[0], params[1], origin);
      case 'eth_signTypedData_v4': return this.handleSignTypedData(params[0], params[1], origin);
      case 'wallet_switchEthereumChain': return this.handleChainSwitch(params[0]);
      default: return this.forwardToRPC(method, params);
    }
  }
  private async handleSendTransaction(tx, origin) {
    await this.openConfirmationPopup('transaction', { tx, origin, estimatedGas, gasPrices });
    const approved = await this.waitForUserApproval();
    if (!approved) throw new Error('User rejected transaction');
    const wallet = this.sessionManager.getWallet(tx.from);
    if (!wallet) throw new Error('Account locked');
    const signedTx = await wallet.signTransaction(tx);
    return this.provider.broadcastTransaction(signedTx);
  }
}

Popup UI and Transaction Security

The popup is a React SPA. The critical screen is the transaction confirmation with decoded calldata and warnings about unknown contracts. The code includes a TransactionConfirmation component showing the amount, recipient, and gas estimate. The wallet checks domains against public phishing lists (MetaMask, Etherscan). Suspicious activity triggers a warning. For EIP-712 (Permit), the user sees detailed information about infinite approval.

Tech Stack and Tools

Component Technology
Extension framework Manifest V3, WXT (Vite-based) or CRXJS
UI (popup) React 18 + TypeScript + Tailwind
Crypto primitives ethers.js v6 or viem
Key derivation BIP-39 (mnemonic), BIP-44 (HD paths)
Storage encryption AES-256-GCM + scrypt KDF
State management Zustand or Recoil
Build Vite + rollup
Testing Playwright for E2E, Vitest for unit

WXT reduces build costs by approximately $5,000 compared to manual configuration.

Development Phases

Phase Content Timeline
Architecture MV3 design, IPC scheme, security model 2 weeks
Keystore Encrypt/decrypt, HD wallet, auto-lock 3–4 weeks
Provider (EIP-1193) window.ethereum, content script, injected 3–4 weeks
Background handler All RPC methods, chain management 3–4 weeks
Popup UI Account management, tx confirmation, signing 4–6 weeks
Security Phishing detection, simulation preview 2–3 weeks
Multi-chain Adding Solana, TON, or other VMs 4–8 weeks
Testing E2E with real dApps, security review 3–4 weeks
Audit Crypto primitives + key storage 3–4 weeks
Documentation Architecture, API, operations manual 1–2 weeks
Training 2–3 sessions for the client team 1 week
Support 1 month post-launch

Store compatibility: Chrome Web Store enforces strict MV3 checks; Firefox uses MV2/MV3 with differences. Separate builds for both browsers are a distinct pipeline task.

Common Development Mistakes
  • Forgetting keep-alive—background unloads and wallet loses state.
  • Not isolating the injected script from the page—vulnerabilities via prototype pollution.
  • Not verifying the origin domain—phishing through iframes.

Get a consultation from our engineers—we'll evaluate your project, propose an architecture and timeline. Contact us—we guarantee a security-first approach and transparency at every stage. Use scrypt for key protection—it's a proven standard.

We develop crypto wallets turnkey — from custodial solutions for fintech to smart contract accounts on EIP-4337. 5+ years in blockchain development, 40+ projects implemented. Let's examine which architecture to choose for your task and why MPC or Account Abstraction solve the private key problem that MetaMask and classic HD wallets could not close.

Why are classic wallets dangerous for business?

A seed phrase in a browser extension is the only way to restore access. For retail users, this is a barrier to entry (lost phrase = lost money). For corporate treasuries, it is incompatible with compliance (KYC/AML, role model, multisignature). Any single key leak compromises all funds. These risks are built into the architecture, not poor UX.

We eliminate them at the protocol level: MPC wallets (key never fully assembled), smart contract wallets (authorization logic in code), hardware HSM for institutional storage. Details below.

What is the real difference between custodial and non-custodial?

Custodial — the provider stores the private key. User authenticates via email/password/OAuth. Recovery is trivial, KYC/AML built-in. For centralized financial applications, often the only regulatory acceptable option. Risk: single point of failure (e.g., Bitfinex hack — $72M, FTX — $600M+ client funds).

Non-custodial — keys are with the user. Provider has no access to funds. Storage responsibility falls on the user. For 99% of people, this model is unworkable without additional protection — hence MPC.

MPC wallets: the key that doesn't exist

Multi-Party Computation (MPC) is a cryptographic protocol that allows multiple parties to jointly sign a transaction without revealing their partial secrets. The private key never exists in its assembled form.

Standard scheme: 2-of-3 MPC between user (share on device), provider server, and backup cloud storage. Transaction is signed by any two of three parties. Lost phone — recovery via server + cloud. Server compromised — attacker holds only one share, signing impossible.

TSS (Threshold Signature Scheme) is a concrete implementation of MPC for ECDSA/EdDSA. Algorithms: GG18, GG20, CGGMP21 (the latter is faster and has better security proofs). Libraries: tss-lib (Go, from Binance), multi-party-sig (Go, from Coinbase), ZenGo-X/multi-party-ecdsa (Rust).

MPC requires no on-chain changes — to the blockchain, the signature looks like a normal single-key signature. This saves gas and keeps the key management scheme confidential (not published in chain) — unlike multisig.

Account Abstraction (EIP-4337): smart contract as wallet

EIP-4337 completely changes the model: instead of EOA (Externally Owned Account), a smart contract Account is used. Authorization logic is in contract code, not in protocol cryptography. This opens up arbitrary signing logic, social recovery, session keys, sponsored transactions, and batch operations.

How the EIP-4337 stack works:

User → UserOperation → Bundler → EntryPoint contract → Account contract
                                          ↑
                                    Paymaster (optional, pays gas)

UserOperation — a new type of object (not an L1 transaction). Bundler collects UserOps from an alternative mempool, packs them into one transaction, and sends to EntryPoint. EntryPoint calls validateUserOp on the Account contract — Account decides if the signature is valid.

Practical capabilities:

Social recovery. The contract stores a list of guardians (other addresses or a service). Lost key — guardians vote for replacement. Argent has used this scheme since 2020.

Session keys. A temporary key with limited rights: interaction only with a specific contract, until a certain date, up to a certain amount. For GameFi and dApps — user does not sign every micro-transaction.

Paymaster. A third-party contract pays gas for the user. Onboarding pattern: user does not hold ETH, gas is sponsored by dApp or taken from ERC-20 tokens.

Implementations: Safe{Core} Protocol, Biconomy SDK (Stackup), ZeroDev (Kernel), Alchemy (Rundler bundler). EntryPoint v0.6/v0.7 is deployed and active on Ethereum mainnet, Polygon, Arbitrum, Optimism. We guarantee compatibility with the latest contract versions.

What is a Hardware Security Module for corporate wallets?

For treasuries and institutional storage: HSM (Hardware Security Module). The key is generated and never leaves the secure chip. Signing happens inside the HSM. Hardware attestation is supported. Solutions used: AWS CloudHSM, Azure Dedicated HSM, Thales Luna, YubiHSM 2 (for small volumes). Integration via PKCS#11 or cloud-specific API.

A combination of HSM + MPC is optimal for institutional use: key shares are stored in HSMs on different servers/jurisdictions, signing via TSS. This ensures compliance with regulatory requirements (e.g., for crypto custodians).

Integration with dApps: WalletConnect and standards

Any wallet must be able to interact with dApps. Standard: WalletConnect v2 (Sign API): QR code or deep link, peer-to-peer encrypted channel via relay server. For browser extensions: EIP-1193 (Ethereum Provider API).

On the frontend, we use wagmi + viem — one interface for MetaMask, WalletConnect, Coinbase Wallet, injected providers. For Account Abstraction: EIP-5792 (wallet capabilities) and EIP-7677 (paymaster service).

Development process

  1. Threat model — who is the user (B2C, B2B, institutional), what operations, what is the acceptable risk model. Architecture depends on this.
  2. Selection and design of key storage scheme — MPC, HSM, multisig, or a combination.
  3. Development of Account contract (if EIP-4337) or integration of MPC library.
  4. Backend — MPC coordination, session management, paymaster service (if needed).
  5. Mobile/browser application — UI with WalletConnect integration, biometrics, QR.
  6. Integration with dApps — EIP-1193, WalletConnect v2.
  7. Audit of contracts and cryptographic implementations — mandatory step. MPC libraries have known vulnerabilities (GG18 susceptible to attack with malicious participant without abort protocol). We use libraries with up-to-date security reviews (CGGMP21). Experience passing audits with Certik, Hacken, Trail of Bits — we have certificates.

What is included in the work (deliverables)

  • Source code of smart contracts (Solidity/Rust) with documentation
  • Backend MPC coordination service (Go or Rust) with API
  • Mobile application (iOS/Android) or browser extension
  • Integration with WalletConnect, Ledger/Trezor (if required)
  • Preparation for security audit (vulnerability report)
  • Administrator and user documentation
  • Access to repository, CI/CD, monitoring (Tenderly, Etherscan API)
  • Training of your team (2-3 sessions)
  • Post-launch support — 1 month

Timeline and cost

Solution type Timeline (working weeks)
Custodial with basic UI 4–8
Non-custodial with MPC integration 8–16
EIP-4337 Account with paymaster 6–12
Institutional (HSM + MPC + compliance) from 16

Cost is calculated individually for your project. We will estimate within one day — contact us by email or Telegram. We provide a guarantee on code and timeline.

Typical mistakes in crypto wallet development (and how to avoid them)

  • Using outdated MPC libraries — GG18 without abort protocol. Choose CGGMP21 or tss-lib with up-to-date audit reports.
  • Tight coupling to a single blockchain — not abstracting for L2/sidechains. Use viem/wagmi for cross-chain.
  • Ignoring MEV attacks — when using multisig without timelocks. Add tx simulation (Tenderly) and sandwiching protection.
  • Lack of fallback recovery mechanism — for Account Abstraction, not setting up social recovery. Include from the first release.

We eliminate these pitfalls at the design stage — for each project, we create a threat model and security checklist.

Need a reliable wallet with no compromises? Get a consultation from our architect — we will analyze your task and propose an architecture with a precise estimate. Leave a request — we will respond within a day.