Mobile Crypto Wallet Development (Android)

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
Mobile Crypto Wallet Development (Android)
Complex
from 2 weeks to 3 months
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

Browser Extension Wallet Development

Browser wallet is the most complex type of crypto client not because there's much math, but because it works in several isolated contexts simultaneously and must be security-first when interacting with any website on internet. MetaMask, Phantom, Rabby — each solves same problem: give dApp access to sign transactions without giving dApp access to private key.

Extension Architecture: Three Contexts

Browser extension exists in three isolated JavaScript contexts with different privileges:

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

Not implementation details — security model. Content script has no key access. Popup has no DOM access. Background — only place with keys, isolated from web content completely.

Manifest V3: Challenges

Chrome transition from MV2 to V3 created issues. Background page (persistent) replaced with service worker (ephemeral). Service worker terminated by browser anytime — doesn't hold state permanently.

{
  "manifest_version": 3,
  "background": { "service_worker": "background.js", "type": "module" },
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content-script.js"],
    "run_at": "document_start",
    "world": "ISOLATED"
  }],
  "permissions": ["storage"],
  "host_permissions": ["<all_urls>"]
}

Service worker termination solved via chrome.storage and keep-alive ping:

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') {
        // Keeps SW from terminating
      }
    });
  }
  
  private async restoreState() {
    const stored = await chrome.storage.session.get(['walletState']);
    if (stored.walletState) {
      this.state = stored.walletState;
    }
  }
}

Keystore: Key Storage and Encryption

Main security question: how to store private key. Keys never plaintext — only encrypted:

interface EncryptedKeystore {
  version: number;
  crypto: {
    ciphertext: string;
    cipherparams: { iv: string };
    kdf: string;
    kdfparams: {
      dklen: number;
      salt: string;
      n: number;
      r: number;
      p: number;
    };
    mac: string;
  };
}

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({ accounts });
  }
}

Auto-lock Mechanism

Keys shouldn't remain decrypted indefinitely:

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
    );
  }
}

EIP-1193 Provider: dApp Interface

window.ethereum is EIP-1193 provider. dApp calls window.ethereum.request({ method, params }) and gets response.

Content Script + Injected Script

Content script is isolated (no window access). Need second layer — injected script executed in page context:

// 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 — page context
class EIP1193Provider extends EventEmitter {
  private requestId = 0;
  private pendingRequests = new Map();
  
  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);
        if (error) {
          pending.reject(new Error(error.message));
        } else {
          pending.resolve(result);
        }
      }
    });
  }
  
  async request({ method, params }: { method: string; params?: unknown[] }): 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);
    });
  }
}

window.ethereum = new EIP1193Provider();

Request Handling in Background

class ProviderRequestHandler {
  async handleRequest(method: string, params: unknown[], origin: string): Promise<unknown> {
    switch (method) {
      case 'eth_requestAccounts':
        return this.requestAccounts(origin);
      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);
      default:
        return this.forwardToRPC(method, params);
    }
  }
  
  private async handleSendTransaction(tx: TransactionRequest, origin: string): Promise<string> {
    await this.openConfirmationPopup('transaction', {
      tx,
      origin,
      estimatedGas: await this.estimateGas(tx),
      gasPrices: await this.getGasPrices()
    });
    
    const approved = await this.waitForUserApproval();
    if (!approved) throw new Error('User rejected transaction');
    
    const wallet = this.sessionManager.getWallet(tx.from);
    const signedTx = await wallet.signTransaction(tx);
    return await this.provider.broadcastTransaction(signedTx);
  }
}

Popup UI: Confirmation Screens

interface TransactionDetails {
  to: string;
  value: bigint;
  data: string;
  estimatedGas: bigint;
  gasPrice: bigint;
  origin: string;
  decoded?: { function: string; args: Record<string, unknown> };
}

function TransactionConfirmation({ tx }: { tx: TransactionDetails }) {
  const isContract = tx.data !== '0x' && tx.data !== '';
  const totalCost = tx.value + tx.estimatedGas * tx.gasPrice;
  
  return (
    <div className="confirm-tx">
      <div className="origin-badge">
        <img src={getFavicon(tx.origin)} />
        <span>{tx.origin}</span>
      </div>
      
      {isContract && !isKnownContract(tx.to) && (
        <div className="warning">Unverified contract interaction</div>
      )}
      
      <div className="tx-details">
        <div>To: <Address address={tx.to} /></div>
        <div>Value: {formatETH(tx.value)} ETH</div>
        {tx.decoded && (
          <div className="decoded-call">
            <span>Function: {tx.decoded.function}</span>
          </div>
        )}
        <div>Gas fee: ~{formatETH(tx.estimatedGas * tx.gasPrice)} ETH</div>
        <div>Total: {formatETH(totalCost)} ETH</div>
      </div>
      
      <div className="actions">
        <button onClick={onReject}>Reject</button>
        <button onClick={onApprove}>Confirm</button>
      </div>
    </div>
  );
}

Phishing Protection

async function checkPhishingDomain(origin: string): Promise<boolean> {
  const domain = new URL(origin).hostname;
  const response = await fetch(
    `https://phishing-detection.metaswap.codefi.network/v1/domains/${domain}`
  );
  const data = await response.json();
  return data.result === 'blocked';
}

function PhishingWarning({ domain }: { domain: string }) {
  return (
    <div className="phishing-warning">
      <strong>Warning: Potential Phishing Site</strong>
      <p>Do not approve any transactions on this site.</p>
    </div>
  );
}

Development Stack

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

Development Stages

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 4–6 weeks
Security Phishing detection, simulation preview 2–3 weeks
Multi-chain Add Solana, TON or others 4–8 weeks
Testing E2E with real dApps, security review 3–4 weeks
Audit Crypto primitives + key storage 3–4 weeks

Store compatibility: Chrome Web Store strict MV3 requirements. Firefox uses MV2/MV3 with differences. Builds for both browsers — separate pipeline task.