Розробка криптовалютного гаманця
Криптовалютний гаманець — це не сховище монет. Монети живуть у блокчейні. Гаманець зберігає приватні ключі та підписує транзакції. Це принципіальне розходження визначає всю архітектуру: головне завдання — безпечне управління ключами, а не збереження балансів. Неправильна робота з ключами робить будь-який красивий UI безглуздим — якщо seed phrase зберігається в localStorage або передається на сервер, гаманець небезпечний незалежно від усього іншого.
Розробка гаманця ділиться на два принципово різні класи: кастодіальний (ключі в сервісу) та некастодіальний (ключи у користувача). Це не просто технічний вибір — це юридичне та бізнес-рішення з різними вимогами до compliance.
Типи гаманців та архітектурні рішення
HD Wallet (Hierarchical Deterministic, BIP-32/BIP-44)
Стандарт для більшості некастодіальних гаманців. З однієї seed phrase (12/24 слова BIP-39) виводиться дерево ключів. Кожен ланцюг, кожен аккаунт, кожна адреса — окремий дочірній ключ.
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> {
// Дериворуємо ключ шифрування з 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 ланцюги: один ключ
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 });
}
}
Баланси токенів: Пакетування запитів
Окремий RPC виклик на токен на ланцюг = величезна затримка. 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,
}));
}
Симуляція транзакцій
Перед відправкою показуємо користувачу що буде. 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 або неочікувано зменшує баланс — попередити користувача перед реальною відправкою.
Інтеграція 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 | PBKDF2 + AES-256-GCM, зберігання в Secure Enclave/Keystore |
| Безпека пам'яті | Приватний ключ в пам'яті тільки при розблокованому стані |
| Захоплення екрану | Блокування скриншотів при відображенні seed phrase |
| Clipboard | Очистка буфера через 60 секунд після копіювання |
| Симуляція транзакцій | Попередження при revert або drain |
| Захист від фішингу | Верифікація URL dApp, попередження про невідомі контракти |
| Біометрія | Опціональна біометрична захист відкриття |
| Transport | Тільки HTTPS/WSS, certificate pinning для мобільних |
| Аудит залежностей | 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 або розширення. Список підтримуваних ланцюгів та токенів. Інтеграція апаратного гаманця.
Розробка core (3-4 тижні). Управління ключами, HD wallet, підпис транзакцій, мультиланцюгова підтримка.
Розробка UI (2-3 тижні). Onboarding (генерація/імпорт seed), portfolio view, send/receive, історія транзакцій, dApp browser.
Security review (1-2 тижні). Penetration testing ключових функцій, аудит зберігання seed.
Тестування та запуск (1-2 тижні). TestFlight/Play Store beta, тести mainnet з малими сумами.
Повний цикл мобільного гаманця: 3-4 місяці. Вартість залежить від набору функцій та платформ.







