Розробка мобільного криптогаманця (Android)

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка мобільного криптогаманця (Android)
Складний
від 2 тижнів до 3 місяців
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1218
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    920
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1147
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    610
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    885

Розробка розширення браузера для гаманця

Браузерний гаманець — найскладніший тип крипто-клієнта не тому що там багато математики, а тому що він працює в кількох ізольованих контекстах одночасно та повинен бути security-first при взаємодії з будь-яким сайтом в інтернеті. MetaMask, Phantom, Rabby — кожен вирішує одну й ту ж задачу: дати dApp доступ до підпису транзакцій не даючи dApp доступ до приватного ключа.

Архітектура розширення: три контексти

Браузерне розширення існує у трьох ізольованих JavaScript контекстах з різними привілеями:

┌─────────────────────────────────────────────────────────┐
│  Background Service Worker (Manifest V3)                │
│  - Зберігає keystore (зашифрований)                     │
│  - Керує станом гаманця                                │
│  - Підписує транзакції                                 │
│  - Відповідає на запити від popup та content script    │
└──────────────────┬──────────────────────────────────────┘
                   │ chrome.runtime.sendMessage
         ┌─────────┴──────────┐
         │                    │
┌────────▼────────┐  ┌────────▼────────────────────────────┐
│  Popup (UI)     │  │  Content Script                     │
│  React SPA      │  │  Інжектується в кожну сторінку      │
│  Управління     │  │  Створює window.ethereum             │
│  аккаунтами     │  │  Передає запити від dApp            │
│  Підтвердження  │  │  до background                      │
│  транзакцій     │  │                                      │
└─────────────────┘  └─────────────────────────────────────┘

Не деталі реалізації — це security model. Content script не має доступу до ключів. Popup не має доступу до DOM сторінки. Background — єдине місце з ключами, ізольоване від web content повністю.

Manifest V3: проблеми

Перехід Chrome з MV2 на V3 створив серйозні проблеми. Background page (постійна) замінена на service worker (короткочасна). Service worker може бути terminated браузером в будь-який момент — не тримає стан постійно.

{
  "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 вирішується через chrome.storage та 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') {
        // Запобігає termination SW
      }
    });
  }
  
  private async restoreState() {
    const stored = await chrome.storage.session.get(['walletState']);
    if (stored.walletState) {
      this.state = stored.walletState;
    }
  }
}

Keystore: зберігання та шифрування ключів

Головне питання безпеки: як зберігати приватний ключ. Ключі ніколи не plaintext — тільки зашифровані:

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

Ключі не повинні залишатися розшифрованими нескінченно:

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

window.ethereum — це EIP-1193 provider. dApp викликає window.ethereum.request({ method, params }) та отримує відповідь.

Content Script + Injected Script

Content script ізольований (немає доступу до window). Потрібен другий рівень — injected script виконується в контексті сторінки:

// 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();
  
  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();

Обробка запитів у 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: екрани підтвердження

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

Захист від фішингу

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

Стек розробки

Компонент Технологія
Extension framework Manifest V3, WXT або CRXJS
UI (popup) React 18 + TypeScript + Tailwind
Crypto primitives ethers.js v6 або viem
Key derivation BIP-39, BIP-44
Storage encryption AES-256-GCM + scrypt
State management Zustand або Recoil
Build Vite + rollup
Testing Playwright для E2E, Vitest для unit

Етапи розробки

Фаза Вміст Графік
Архітектура MV3 design, IPC схема, security model 2 тижні
Keystore Encrypt/decrypt, HD wallet, auto-lock 3–4 тижні
Provider (EIP-1193) window.ethereum, content script, injected 3–4 тижні
Background handler Всі RPC методи, управління ланцюгом 3–4 тижні
Popup UI Управління аккаунтами, підтвердження tx 4–6 тижнів
Security Phishing detection, preview симуляції 2–3 тижні
Мультиланцюг Додати Solana, TON чи інші 4–8 тижнів
Тестування E2E з реальними dApps, security review 3–4 тижні
Аудит Crypto primitives + key storage 3–4 тижні

Сумісність зі store: Chrome Web Store строгі вимоги MV3. Firefox використовує MV2/MV3 з різницями. Збірки для обох браузерів — окремої задачі в pipeline.