Розробка веб-криптовалютного гаманця
Криптовалютний гаманець — це не місце для зберігання монет. Монети зберігаються on-chain, гаманець зберігає приватний ключ для їх підпису. Це принципова відмінність від банківського рахунку і визначає всю архітектуру: де і як зберігати ключ, як підписувати транзакції, як захистити від компрометації.
Веб-гаманець — найбільш доступний варіант (не потрібно встановлювати розширення або програму), але також найбільш атакуваний: браузерне середовище, XSS загрози, атаки ланцюга поставок на JavaScript залежності. MetaMask, Coinbase Wallet, Rainbow — розширення браузера, а не чистий веб. Чистий веб-гаманець вимагає додаткових заходів безпеки.
Типи гаманців та архітектурний вибір
EOA vs Smart Account
EOA (Externally Owned Account) — класичний гаманець. Один приватний ключ → одна адреса. Просто, сумісно зі всім. Проблема: втрата ключа = постійна втрата доступу. Немає соціального відновлення, мультифакторної аутентифікації, обмежень витрат.
Smart Account (ERC-4337 Account Abstraction) — смарт-контракт гаманець. Логіка валідації транзакцій програмована. Можливості: соціальне відновлення (відновлення через довірені контакти), session keys (обмежені ключі для dApp без повного доступу), пакетні транзакції (кілька операцій в одному викликі), спонсорство газу (paymaster платить газ за користувача), верифікація підпису будь-яким алгоритмом (не тільки ECDSA secp256k1).
Для потребительського веб-гаманця у 2024-2025 — рекомендую ERC-4337. Учіть користувачів методам відновлення зрозумілим звичайній людині замість seed phrases.
Зберігання ключів у браузері
Ключове питання безпеки. Варіанти від найменш до найбільш безпечного:
localStorage / sessionStorage — ніколи не використовуйте для приватних ключів. Доступний будь-якому JavaScript на сторінці, будь-якому XSS.
IndexedDB з шифруванням — приватний ключ шифрується через Web Crypto API (AES-GCM 256-bit) з ключем виведеним з пароля користувача (PBKDF2 або Argon2 з високим cost factor). Зашифрований blob зберігається в IndexedDB. Це стандарт для браузерних гаманців.
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 для GCM
// Дериворуємо ключ шифрування з пароля
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 рекомендує 600k для 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 + апаратний ключ — найбільш безпечний варіант. Приватний ключ гаманця ніколи не покидає WebAuthn authenticator (платформеничний ключ в TPM/Secure Enclave, або апаратний ключ типу YubiKey). Операція підпису відбувається всередині пристрою.
Реалізація через Passkey: користувач створює passkey (WebAuthn credential), приватний ключ генерується в Secure Enclave iPhone/Android/Windows TPM. Для ERC-4337 гаманця — смарт-контракт може верифікувати P-256 (secp256r1) підписи WebAuthn. Daimo та Coinbase Smart Wallet використовують саме цей підхід.
MPC гаманці
Multi-Party Computation: приватний ключ ніколи не існує повністю в одному місці. Розділений на shares між кількома сторонами (користувач + сервер, або користувач + пристрій 1 + пристрій 2). Підпис вимагає threshold учасників, але жоден не знає повного ключа.
Постачальники MPC-as-a-service: Privy, Dynamic, Web3Auth, Fireblocks Web3. Для non-custodial MPC — користувач має один share, сервіс інший. Компрометація сервісу не означає компрометацію ключа.
Мінус MPC: підпис більш повільний (multi-round протокол) і вимагає доступності обох учасників. Якщо сервіс недоступний — не можна підписати.
Key Derivation та HD гаманці
BIP-39 та BIP-44
Стандартний Web3 гаманець використовує seed phrase (мнемоніку) — 12 або 24 слова з BIP-39 словника. З seed через PBKDF2 (2048 ітерацій) виводиться master приватний ключ. З master ключа через BIP-32 derivation виводяться дочірні ключі по шляху.
BIP-44 визначає стандартний derivation path: m/purpose'/coin_type'/account'/change/address_index.
Для Ethereum: m/44'/60'/0'/0/0 — перший аккаунт. m/44'/60'/0'/0/1 — другий. Це дозволяє одній seed phrase керувати нескінченною кількістю адрес детерміністично. Імпортуйте seed в MetaMask, Rainbow, Ledger — отримайте ті ж адреси.
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!),
};
}
Бібліотеки: @scure/bip39 та @scure/bip32 — сучасні, аудовані, tree-shakeable замінники noble-secp256k1 та bip39.
Архітектура веб-гаманця
Поділ відповідальності: UI vs Signing
Критичне правило: код, який має доступ до приватного ключа, повинен бути ізольований від коду, який взаємодіє з dApps та інтернетом. XSS в UI層не повинен компрометувати ключ.
Реалізація через Web Worker або Service Worker: операції підпису відбуваються в ізольованому worker, UI層не має прямого доступу до ключа. Worker отримує запит на підпис, показує користувачу що підписується, отримує підтвердження, повертає підпис (не ключ).
// 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 — ізольований worker з доступом до ключа
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 та інтеграція dApp
WalletConnect v2 — стандарт для підключення гаманця до dApp. Працює через relay сервер: dApp створює паринг QR-код або deep link, гаманець сканує, встановлюється зашифрована сесія. Всі запити (eth_signTypedData, eth_sendTransaction) йдуть через цей канал.
Для веб-гаманця: @walletconnect/web3wallet SDK. Гаманець виступає як "wallet" (не "dapp") в WalletConnect терміології.
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"],
},
});
// Обробка вхідних запитів від 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
Для прямої інтеграції dApp через window.ethereum: гаманець інжектує EIP-1193 сумісний provider в DOM. Це те, що робить MetaMask. Для веб-гаманця в tab (не розширення) — обмежено тим же origin, не ідеально.
Альтернатива: Service Worker як фоновий процес (Progressive Web App), який може інжектувати provider та обробляти запити від інших tabs того ж origin.
Реалізація ERC-4337 Account Abstraction
UserOperation flow
Замість звичайної Ethereum транзакції, ERC-4337 вводить UserOperation — структуру даних, яку підписує гаманець та відправляє Bundler (не безпосередньо в mempool):
interface UserOperation {
sender: string; // адреса smart account
nonce: bigint;
initCode: Hex; // для створення нового аккаунту
callData: Hex; // що повинен виконати аккаунт
callGasLimit: bigint;
verificationGasLimit: bigint;
preVerificationGas: bigint;
maxFeePerGas: bigint;
maxPriorityFeePerGas: bigint;
paymasterAndData: Hex; // paymaster для спонсорства газу
signature: Hex; // підпис власника
}
Bundler збирає UserOperations, упаковує в batch, відправляє через EntryPoint контракт. EntryPoint валідує підписи та виконує операції.
Гаманець взаємодіє з Bundler через JSON-RPC API (ERC-4337 специфічні методи: eth_sendUserOperation, eth_getUserOperationByHash). Постачальники bundler: Pimlico, Alchemy, Biconomy.
Session Keys
Session key — обмежений ключ без повного доступу до аккаунту. Користувач створює session key для конкретного dApp на конкретний період. dApp використовує цей ключ для підпису операцій — користувачу не потрібно підтверджувати кожну транзакцію.
Для GameFi: користувач створює session key на 8 годин ігрової сесії. Key може тільки викликати game-specific контракти, не може переводити ETH або ERC20. Після 8 годин — автоматично expires.
Реалізація: через SmartAccount validator module (ZeroDev Kernel, Safe modules). Session key policy зберігається в смарт-контракті.
Безпека
Загрози та мітигація
XSS атаки. Content Security Policy (сувора: script-src 'self' 'wasm-unsafe-eval'), Subresource Integrity для зовнішніх скриптів, уникаємо innerHTML, використовуємо React (автоматичний escaping). Регулярний npm audit та Snyk для аналізу ланцюга поставок.
Clipboard атаки. Користувач копіює адресу, malware замінює адресу в clipboard. Рішення: показуємо перші та останні N символів адреси, просимо візуально перевірити. Деякі гаманці показують адресу як identicon (унікальний аватар) — швидше помітити підміну.
Фішинг. Поддельний сайт гаманця. Рішення: PWA з верифікованим доменом, попередження на рівні браузера, ENS для верифікації.
Експозиція seed phrase. Показ seed phrase в UI — максимальний ризик. Ніколи не передавайте seed через мережу, показуйте тільки при створенні, вимагайте явної user action для перегляду, додайте попередження "чи ви самотні?".
Приватний ключ в пам'яті. Після розшифровки ключ живе в JavaScript heap. Garbage collector не гарантує негайне очищення. Мітигація: Uint8Array.fill(0) після використання, зберігайте ключ в Worker де GC більш передбачуваний.
Аудит залежностей
Гаманець використовує багато крипто-бібліотек. Критичні для перевірки:
-
@noble/secp256k1або@noble/curves— підпис транзакцій -
@scure/bip32,@scure/bip39— HD derivation -
ethersабоviem— взаємодія з ланцюгом
Всі @noble/* та @scure/* бібліотеки від Paulmillr — аудовані, мінімалістичні, без залежностей. Віддавайте їм перевагу більш "корпоративним" альтернативам.
Мультиланцюгова підтримка
Ethereum-сумісні ланцюги (Polygon, Arbitrum, Base, Optimism) — одна адреса, різні RPC. Достатньо підтримувати EIP-155 chain ID та переключення RPC.
Bitcoin або Solana — різний алгоритм підпису (secp256k1 з різним форматом / ed25519), різний derivation path. Вимагає окремої ключової логіки.
Для мультиланцюгового гаманця: абстрагуйте ланцюг-специфічну логіку за загальним інтерфейсом. IChainSigner з методом signTransaction(tx) — кожен ланцюг має свою реалізацію.
Стек та графік
| Компонент | Технологія | Графік |
|---|---|---|
| Управління ключами | Web Crypto API + @scure | 3-4 тижні |
| HD wallet (BIP-39/44) | @scure/bip32 + bip39 | 1-2 тижні |
| Підпис транзакцій | viem + ethers | 2-3 тижні |
| ERC-4337 інтеграція | permissionless.js + Pimlico | 3-4 тижні |
| WalletConnect v2 | @walletconnect/web3wallet | 2-3 тижні |
| UI (React + TypeScript) | React + Tailwind | 5-7 тижнів |
| Security hardening | CSP + Web Worker isolation | 2-3 тижні |
| Мультиланцюг | Chain abstraction layer | 3-4 тижні |
| Тестування + підготовка аудиту | Vitest + Playwright | 3-4 тижні |
MVP веб-гаманець (EOA, Ethereum, базовий UI): 8-10 тижнів. Production-ready з ERC-4337, мультиланцюгом, WalletConnect, WebAuthn: 6-9 місяців.
Security audit обов'язковий перед production запуском: гаманець зберігає ключі користувачів, одна вразливість = втрата коштів для всієї бази користувачів. Рекомендую аудит + bug bounty програму (Immunefi) з запуску.







