Разработка криптокошелька
Криптокошелёк — это не хранилище монет. Монеты живут на блокчейне. Кошелёк хранит приватные ключи и подписывает транзакции. Это принципиальное различие определяет всю архитектуру: главная задача — безопасное управление ключами, а не хранение балансов. Неправильная работа с ключами делает любой красивый UI бессмысленным — если seed phrase хранится в localStorage или передаётся на сервер, кошелёк небезопасен независимо от остального.
Разработка кошелька делится на два принципиально разных класса: кастодиальный (ключи у сервиса) и некастодиальный (ключи у пользователя). Это не просто технический выбор — это юридическое и бизнес-решение с разными compliance требованиями.
Типы кошельков и архитектурные решения
HD Wallet (Hierarchical Deterministic, BIP-32/BIP-44)
Стандарт для большинства некастодиальных кошельков. Из одного seed phrase (12/24 слова BIP-39) деривируется дерево ключей. Каждая цепь, каждый аккаунт, каждый адрес — отдельный child key.
import { ethers } from 'ethers';
import * as bip39 from 'bip39';
// Генерация seed phrase
const mnemonic = bip39.generateMnemonic(256); // 24 слова
// "word1 word2 ... word24"
// Деривация HD кошелька
const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic);
// Аккаунт по стандартному derivation path (BIP-44)
// m/44'/60'/0'/0/0 — первый Ethereum аккаунт
const wallet = hdNode.derivePath("m/44'/60'/0'/0/0");
console.log(wallet.address); // 0x...
console.log(wallet.privateKey); // 0x... (НИКОГДА не показывать)
// Следующие аккаунты
const wallet1 = hdNode.derivePath("m/44'/60'/0'/0/1");
const wallet2 = hdNode.derivePath("m/44'/60'/0'/0/2");
// Разные цепи (SLIP-44 coin types):
// ETH: m/44'/60'/...
// BTC: m/44'/0'/...
// Solana: m/44'/501'/...
// Cosmos: m/44'/118'/...
Один seed phrase → все кошельки для всех цепей. Пользователю нужно запомнить только 12/24 слова.
Безопасное хранение ключей на устройстве
Самое критичное место. Несколько уровней защиты:
iOS/Android: Secure Enclave / Keystore
// React Native: expo-secure-store или @react-native-async-storage
// + шифрование на уровне ОС
import * as SecureStore from 'expo-secure-store';
import * as LocalAuthentication from 'expo-local-authentication';
import CryptoJS from 'crypto-js';
async function storeEncryptedMnemonic(mnemonic: string, pin: string): Promise<void> {
// Деривируем encryption key из PIN через PBKDF2
const salt = CryptoJS.lib.WordArray.random(128 / 8).toString();
const key = CryptoJS.PBKDF2(pin, salt, {
keySize: 256 / 32,
iterations: 100000, // 100K итераций — медленно для атакующего, приемлемо для пользователя
});
const encrypted = CryptoJS.AES.encrypt(mnemonic, key.toString()).toString();
// Сохраняем в Secure Enclave (iOS) или Android Keystore
await SecureStore.setItemAsync('encrypted_mnemonic', encrypted);
await SecureStore.setItemAsync('pbkdf2_salt', salt);
}
async function loadMnemonic(pin: string): Promise<string | null> {
// Опционально: биометрия перед расшифровкой
const biometricResult = await LocalAuthentication.authenticateAsync({
promptMessage: 'Confirm identity',
});
if (!biometricResult.success) return null;
const encrypted = await SecureStore.getItemAsync('encrypted_mnemonic');
const salt = await SecureStore.getItemAsync('pbkdf2_salt');
if (!encrypted || !salt) return null;
const key = CryptoJS.PBKDF2(pin, salt, { keySize: 256/32, iterations: 100000 });
const bytes = CryptoJS.AES.decrypt(encrypted, key.toString());
return bytes.toString(CryptoJS.enc.Utf8);
}
Web/Extension: не храним приватный ключ в localStorage
Приватный ключ в расширении браузера хранится в зашифрованном хранилище Chrome (chrome.storage.local) с паролем пользователя. MetaMask использует именно этот подход с PBKDF2 + AES-256-GCM.
Важно: ключ должен быть в памяти только пока кошелёк разблокирован. При блокировке — ключ из памяти удаляется, остаётся только зашифрованный blob.
Аппаратные кошельки: интеграция через HID/WebUSB
import TransportWebUSB from '@ledgerhq/hw-transport-webusb';
import Eth from '@ledgerhq/hw-app-eth';
async function signWithLedger(
derivationPath: string,
transaction: ethers.TransactionRequest
): Promise<string> {
const transport = await TransportWebUSB.create();
const eth = new Eth(transport);
// Получаем адрес с Ledger (верификация)
const { address } = await eth.getAddress(derivationPath);
console.log('Ledger address:', address);
// Сериализуем транзакцию для подписи
const unsignedTx = ethers.Transaction.from(transaction);
const serialized = ethers.getBytes(unsignedTx.unsignedSerialized);
// Подписываем на устройстве — приватный ключ никогда не покидает Ledger
const signature = await eth.signTransaction(derivationPath, Buffer.from(serialized).toString('hex'), null);
const signedTx = ethers.Transaction.from({
...transaction,
signature: {
r: '0x' + signature.r,
s: '0x' + signature.s,
v: parseInt(signature.v, 16),
},
});
return signedTx.serialized;
}
Мультицепочечность
Современный кошелёк поддерживает EVM-совместимые сети (Ethereum, Polygon, Arbitrum, BSC, Avalanche — один ключ, разные RPC) и non-EVM (Solana, Bitcoin, Cosmos — разные алгоритмы подписи).
EVM chains: единый ключ
class EVMWalletProvider {
private wallet: ethers.Wallet;
constructor(privateKey: string) {
this.wallet = new ethers.Wallet(privateKey);
}
getProvider(chainId: number): ethers.Provider {
const rpcUrls: Record<number, string> = {
1: 'https://eth-mainnet.alchemyapi.io/v2/...',
137: 'https://polygon-mainnet.alchemyapi.io/v2/...',
42161: 'https://arb-mainnet.g.alchemy.com/v2/...',
8453: 'https://base-mainnet.g.alchemy.com/v2/...',
};
return new ethers.JsonRpcProvider(rpcUrls[chainId]);
}
async sendTransaction(
tx: ethers.TransactionRequest,
chainId: number
): Promise<ethers.TransactionResponse> {
const provider = this.getProvider(chainId);
const connectedWallet = this.wallet.connect(provider);
return connectedWallet.sendTransaction({ ...tx, chainId });
}
}
Token balances: батчинг запросов
Отдельный RPC вызов per token per chain = огромная задержка. Multicall3 позволяет получить все балансы в одном вызове:
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { erc20Abi } from 'viem';
async function getAllTokenBalances(
address: `0x${string}`,
tokenAddresses: `0x${string}`[]
) {
const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
// Multicall: один запрос для N токенов
const results = await client.multicall({
contracts: tokenAddresses.map(token => ({
address: token,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address],
})),
});
return tokenAddresses.map((addr, i) => ({
token: addr,
balance: results[i].status === 'success' ? results[i].result : 0n,
}));
}
Transaction simulation
Перед отправкой транзакции показываем пользователю что произойдёт. Tenderly Simulation API или Alchemy's alchemy_simulateAssetChanges:
async function simulateTransaction(tx: ethers.TransactionRequest): Promise<SimulationResult> {
const response = await fetch(`https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 1,
jsonrpc: '2.0',
method: 'alchemy_simulateAssetChanges',
params: [{
from: tx.from,
to: tx.to,
data: tx.data,
value: tx.value ? `0x${BigInt(tx.value).toString(16)}` : '0x0',
}],
}),
});
const result = await response.json();
// result.result.changes: список изменений балансов
// result.result.error: если транзакция reverts
return {
willSucceed: !result.result.error,
balanceChanges: result.result.changes,
gasEstimate: result.result.gasUsed,
};
}
Если симуляция показывает что транзакция reverts или неожиданно drain-ит баланс — предупреждаем пользователя до реальной отправки.
WalletConnect v2 интеграция
Стандарт подключения кошелька к dApp:
import { WalletKit } from '@reown/walletkit';
import { Core } from '@walletconnect/core';
const core = new Core({ projectId: PROJECT_ID });
const walletKit = await WalletKit.init({ core, metadata: { name: 'My Wallet', ... } });
// Обработка запроса подключения от dApp
walletKit.on('session_proposal', async ({ id, params }) => {
// Показываем пользователю: dApp запрашивает подключение
const userApproved = await showConnectionModal(params);
if (userApproved) {
await walletKit.approveSession({
id,
namespaces: {
eip155: {
chains: ['eip155:1', 'eip155:137'],
methods: ['eth_sendTransaction', 'personal_sign', 'eth_signTypedData_v4'],
events: ['accountsChanged', 'chainChanged'],
accounts: ['eip155:1:' + walletAddress, 'eip155:137:' + walletAddress],
},
},
});
}
});
// Обработка запроса подписи от dApp
walletKit.on('session_request', async ({ id, params }) => {
const { method, params: reqParams } = params.request;
if (method === 'eth_sendTransaction') {
const tx = reqParams[0];
const simulation = await simulateTransaction(tx);
// Показываем пользователю: что делает транзакция
const userApproved = await showTransactionModal(tx, simulation);
if (userApproved) {
const signedTx = await wallet.sendTransaction(tx);
await walletKit.respondSessionRequest({ id, response: { result: signedTx.hash } });
}
}
});
Безопасность: чеклист
| Пункт | Описание |
|---|---|
| Seed storage | PBKDF2 + AES-256-GCM, хранение в Secure Enclave/Keystore |
| Memory security | Приватный ключ в памяти только при разблокированном состоянии |
| Screen capture | Блокировка скриншотов при отображении seed phrase |
| Clipboard | Очистка буфера через 60 секунд после копирования |
| Transaction simulation | Предупреждение при revert или drain |
| Phishing protection | Верификация URL dApp, предупреждение о неизвестных контрактах |
| Biometrics | Опциональная биометрическая защита открытия |
| Transport | Только HTTPS/WSS, certificate pinning для мобильных |
| Dependency audit | npm audit / Snyk на все зависимости |
Технический стек
Мобильное (React Native): React Native + expo-secure-store + ethers.js v6 + viem + WalletConnect SDK + Reown AppKit.
Браузерное расширение (Chrome/Firefox): React + WebExtension API + chrome.storage + MetaMask Snap API (для расширения существующих кошельков).
Web-based (PWA): Next.js + wagmi + viem + WalletConnect — для кастодиальных или MPC-based кошельков, где ключи не хранятся в браузере.
Процесс работы
Архитектурное решение (1 неделя). Кастодиальный или некастодиальный. Мобильный, web или расширение. Список поддерживаемых цепей и токенов. Hardware wallet интеграция.
Разработка core (3-4 недели). Key management, HD wallet, transaction signing, multi-chain support.
Разработка UI (2-3 недели). Onboarding (seed generation/import), portfolio view, send/receive, transaction history, dApp browser.
Security review (1-2 недели). Penetration testing ключевых функций, seed storage аудит.
Тестирование и launch (1-2 недели). TestFlight/Play Store бета, mainnet тесты с малыми суммами.
Полный цикл мобильного кошелька: 3-4 месяца. Стоимость зависит от набора функций и платформ.







