Разработка веб-криптокошелька
Криптокошелёк — это не место хранения монет. Монеты хранятся on-chain, кошелёк хранит приватный ключ для их подписания. Это принципиальное отличие от банковского счёта, и оно определяет всю архитектуру: где и как хранить ключ, как подписывать транзакции, как защитить от компрометации.
Веб-кошелёк — наиболее доступный вариант (не нужно устанавливать расширение или приложение), но и наиболее атакуемый: browser environment, XSS угрозы, supply chain атаки на JavaScript зависимости. MetaMask, Coinbase Wallet, Rainbow — браузерные расширения, а не чистый web. Чистый web-кошелёк требует дополнительных мер безопасности.
Типы кошельков и архитектурный выбор
EOA vs Smart Account
EOA (Externally Owned Account) — классический кошелёк. Один приватный ключ → один адрес. Просто, совместимо со всем. Проблема: потеря ключа = потеря доступа навсегда. Нет возможности social recovery, multi-factor auth, spending limits.
Smart Account (ERC-4337 Account Abstraction) — смарт-контракт кошелёк. Логика валидации транзакций программируема. Возможности: social recovery (восстановление через trusted contacts), session keys (ограниченные ключи для dApp без полного доступа), batched transactions (несколько операций в одном вызове), gas sponsorship (paymaster платит gas за пользователя), signature verification любым алгоритмом (не только ECDSA secp256k1).
Для потребительского web-кошелька в 2024-2025 году — рекомендую ERC-4337. Учите пользователей не seed phrases, а recovery методам понятным обычному человеку.
Хранение ключей в браузере
Ключевой вопрос безопасности. Варианты от наименее к наиболее безопасному:
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
// 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 рекомендует 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 + hardware key — наиболее безопасный вариант. Приватный ключ кошелька никогда не покидает WebAuthn authenticator (платформенный ключ в TPM/Secure Enclave, или hardware key типа 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: приватный ключ никогда не существует целиком в одном месте. Он split на shares между несколькими сторонами (пользователь + сервер, или пользователь + устройство 1 + устройство 2). Подпись требует threshold участников, но ни один не знает полного ключа.
Провайдеры MPC-as-a-service: Privy, Dynamic, Web3Auth, Fireblocks Web3. Для non-custodial MPC — пользователь держит один share, сервис другой. Компрометация сервиса не означает компрометацию ключа.
Минус MPC: signing более медленное (multi-round protocol) и требует доступности обоих участников. Если сервис недоступен — нельзя подписать.
Key Derivation и HD кошельки
BIP-39 и BIP-44
Стандартный Web3 кошелёк использует seed phrase (мнемонику) — 12 или 24 слова из BIP-39 словаря. Из seed через PBKDF2 (2048 iterations) выводится master private key. Из master key через 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 — modern, audited, tree-shakeable замены noble-secp256k1 и bip39.
Архитектура веб-кошелька
Separation of concerns: UI vs Signing
Критическое правило: код, который имеет доступ к приватному ключу, должен быть изолирован от кода, который взаимодействует с dApps и интернетом. XSS в UI layer не должен компрометировать ключ.
Реализация через Web Worker или Service Worker: signing операции происходят в изолированном 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 server: dApp создаёт pairing QR-код или deep link, кошелёк сканирует, устанавливается encrypted session. Все запросы (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"],
},
});
// Обработка incoming requests от 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
Для прямой интеграции с dApps через window.ethereum: кошелёк инжектирует EIP-1193 совместимый provider в DOM. Это то, что делает MetaMask. Для web-кошелька в tab (не расширения) — это ограничено тем же origin, что не идеально.
Альтернатива: Service Worker как background process (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, упаковывает в батч, отправляет через 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 часов — истекает автоматически.
Реализация: через 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 для supply chain анализа.
Clipboard атаки. Пользователь копирует адрес, malware подменяет адрес в clipboard. Решение: показывать первые и последние N символов адреса, просить визуально проверить. Некоторые кошельки показывают адрес как идентicon (уникальный аватар) — быстрее заметить подмену.
Phishing. Поддельный сайт кошелька. Решение: PWA с verified domain, browser-level warnings, ENS для верификации.
Seed phrase exposure. Показ seed phrase в UI — максимальный риск. Никогда не передавать seed через сеть, показывать только при создании, требовать явного user action для просмотра, добавить "are you alone?" предупреждение.
Private key в памяти. После расшифровки ключ живёт в JavaScript heap. Garbage collector не гарантирует немедленное очищение. Митигация: Uint8Array.fill(0) после использования, хранить ключ в Worker где GC более предсказуем.
Аудит зависимостей
Кошелёк использует много crypto-библиотек. Критичные для проверки:
-
@noble/secp256k1или@noble/curves— подпись транзакций -
@scure/bip32,@scure/bip39— HD derivation -
ethersилиviem— взаимодействие с chain
Все @noble/* и @scure/* библиотеки от Paulmillr — аудированы, minimalist, без зависимостей. Предпочитать их более "корпоративным" альтернативам.
Мультичейн поддержка
Ethereum-совместимые chain (Polygon, Arbitrum, Base, Optimism) — один адрес, разные RPC. Достаточно поддерживать EIP-155 chain ID и переключение RPC.
Bitcoin или Solana — другой алгоритм подписи (secp256k1 с разным форматом / ed25519), другой derivation path. Требует отдельной ключевой логики.
Для мультичейн кошелька: абстракция chain-specific логики за общим интерфейсом. IChainSigner с методом signTransaction(tx) — каждый chain имеет свою реализацию.
Стек и сроки
| Компонент | Технология | Срок |
|---|---|---|
| Key management | Web Crypto API + @scure | 3-4 недели |
| HD wallet (BIP-39/44) | @scure/bip32 + bip39 | 1-2 недели |
| Transaction signing | viem + ethers | 2-3 недели |
| ERC-4337 integration | 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 недели |
| Multi-chain | Chain abstraction layer | 3-4 недели |
| Testing + audit prep | Vitest + Playwright | 3-4 недели |
MVP веб-кошелёк (EOA, Ethereum, базовый UI): 8-10 недель. Production-ready с ERC-4337, мультичейн, WalletConnect, WebAuthn: 6-9 месяцев.
Обязателен security audit перед production запуском: кошелёк хранит ключи пользователей, одна уязвимость = потеря средств всей базы пользователей. Рекомендую аудит + bug bounty программу (Immunefi) с момента запуска.







