Розробка HD Wallet
HD-гаманець (Hierarchical Deterministic wallet) дозволяє з однієї seed phrase генерувати нескінченне дерево ключевих пар. Стандарт BIP-32/BIP-39/BIP-44 — основа всіх сучасних гаманців: MetaMask, Ledger, Trust Wallet, Phantom. Розробка власного HD-гаманця вимагає точної реалізації стандартів, інакше користувачі не зможуть відновити кошти в інших гаманцях.
Стандарти та специфікації
BIP-39: генерація мнемонічної фрази (12/24 слова) з ентропії та конвертація в binary seed через PBKDF2.
BIP-32: деривація дерева ключів з master seed. Визначає child key derivation функцію (CKD) для normal та hardened ключів.
BIP-44: стандартна схема шляхів деривації: m / purpose' / coin_type' / account' / change / index. Наприклад, перша Ethereum адреса: m/44'/60'/0'/0/0.
EIP-55: checksum кодування Ethereum адрес (змішаний регістр).
Генерація ключів: BIP-39 та BIP-32
Мнемоніка та seed
import * as bip39 from "bip39";
import { HDKey } from "@scure/bip32";
import { keccak256 } from "ethereum-cryptography/keccak";
import { secp256k1 } from "ethereum-cryptography/secp256k1";
// Генерація 12-словної мнемоніки (128 бітів ентропії)
// Або 24 слова для 256 бітів — рекомендується для cold storage
function generateMnemonic(strength: 128 | 256 = 128): string {
return bip39.generateMnemonic(strength);
}
// Конвертація мнемоніки в binary seed через PBKDF2
async function mnemonicToSeed(
mnemonic: string,
passphrase: string = ""
): Promise<Uint8Array> {
if (!bip39.validateMnemonic(mnemonic)) {
throw new Error("Invalid mnemonic");
}
// PBKDF2-HMAC-SHA512, 2048 ітерацій, 64 байти
return bip39.mnemonicToSeed(mnemonic, passphrase);
}
Деривація дерева ключів
interface DerivedAccount {
path: string;
privateKey: Uint8Array;
publicKey: Uint8Array;
address: string;
xpub: string; // розширений публічний ключ для watch-only
}
function deriveAccount(
seed: Uint8Array,
accountIndex: number = 0,
addressIndex: number = 0,
coinType: number = 60 // 60 = Ethereum, 0 = Bitcoin, 501 = Solana
): DerivedAccount {
const hdKey = HDKey.fromMasterSeed(seed);
// BIP-44 шлях: m/44'/coinType'/account'/change/index
// Апостроф = hardened деривація (захист: неможливо обчислити private key з public)
const path = `m/44'/${coinType}'/${accountIndex}'/0/${addressIndex}`;
const derived = hdKey.derive(path);
if (!derived.privateKey) {
throw new Error("Failed to derive private key");
}
const publicKey = secp256k1.getPublicKey(derived.privateKey, false);
const address = publicKeyToAddress(publicKey);
return {
path,
privateKey: derived.privateKey,
publicKey,
address,
xpub: derived.publicExtendedKey
};
}
function publicKeyToAddress(publicKey: Uint8Array): string {
// Видаляємо prefix байт (0x04 для uncompressed)
const pubKeyWithoutPrefix = publicKey.slice(1);
const hash = keccak256(pubKeyWithoutPrefix);
// Беремо останні 20 байтів
const addressBytes = hash.slice(-20);
return toChecksumAddress("0x" + Buffer.from(addressBytes).toString("hex"));
}
// EIP-55 checksum адреса
function toChecksumAddress(address: string): string {
const addr = address.toLowerCase().replace("0x", "");
const hash = Buffer.from(keccak256(Buffer.from(addr))).toString("hex");
return "0x" + addr
.split("")
.map((char, i) => (parseInt(hash[i], 16) >= 8 ? char.toUpperCase() : char))
.join("");
}
Безпечне зберігання ключів
Шифрування через Web Crypto API (Browser)
// Шифрування keystore по стандарту EIP-55 / Web3 Secret Storage
async function encryptKeystore(
privateKey: Uint8Array,
password: string
): Promise<EncryptedKeystore> {
const salt = crypto.getRandomValues(new Uint8Array(32));
const iv = crypto.getRandomValues(new Uint8Array(16));
// Деривація ключа з пароля: PBKDF2, 600000 ітерацій (NIST рекомендація 2023)
const passwordKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveBits", "deriveKey"]
);
const encryptionKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 600_000,
hash: "SHA-256"
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
encryptionKey,
privateKey
);
return {
version: 3,
crypto: {
ciphertext: Buffer.from(encrypted).toString("hex"),
cipher: "aes-256-gcm",
kdf: "pbkdf2",
kdfparams: {
dklen: 32,
salt: Buffer.from(salt).toString("hex"),
c: 600_000,
prf: "hmac-sha256"
},
iv: Buffer.from(iv).toString("hex"),
mac: ""
}
};
}
Мобільне зберігання
Для React Native — Secure Enclave (iOS) або Android Keystore:
import * as SecureStore from "expo-secure-store";
import * as Keychain from "react-native-keychain";
// Збереження мнемоніки в Secure Enclave / Android Keystore
// Ключі захищені біометрією пристрою
async function storeMnemonicSecurely(
mnemonic: string,
biometricPrompt: string
): Promise<void> {
await Keychain.setGenericPassword(
"hd_wallet_mnemonic",
mnemonic,
{
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET,
authenticationType: Keychain.AUTHENTICATION_TYPE.BIOMETRICS,
securityLevel: Keychain.SECURITY_LEVEL.SECURE_HARDWARE
}
);
}
async function retrieveMnemonic(prompt: string): Promise<string> {
const credentials = await Keychain.getGenericPassword({
authenticationPrompt: { title: prompt }
});
if (!credentials) throw new Error("No stored mnemonic");
return credentials.password;
}
Мультиаккаунт та watch-only режим
HD-гаманець підтримує кілька аккаунтів (різні BIP-44 account індекси) та watch-only режим через xpub:
class HDWalletManager {
private hdKey: HDKey;
constructor(seed: Uint8Array) {
this.hdKey = HDKey.fromMasterSeed(seed);
}
// Отримати аккаунт за індексом
getAccount(accountIndex: number): DerivedAccount {
return deriveAccount(
new Uint8Array(64), // placeholder
accountIndex
);
}
// xpub для watch-only гаманця — показує баланс без private key
getAccountXpub(accountIndex: number): string {
const accountKey = this.hdKey.derive(`m/44'/60'/${accountIndex}'`);
return accountKey.publicExtendedKey;
}
// Генерація адрес з xpub без private key (для інтеграції hardware wallet)
static deriveAddressFromXpub(
xpub: string,
addressIndex: number
): string {
const hdKey = HDKey.fromExtendedKey(xpub);
const derived = hdKey.derive(`m/0/${addressIndex}`);
return publicKeyToAddress(derived.publicKey!);
}
}
Watch-only режим дозволяє імпортувати тільки xpub — гаманець показує всі адреси та баланси, але не може підписувати транзакції. Використовується для моніторингу cold wallet.
Підпис транзакцій
import { Transaction, parseTransaction } from "viem";
import { privateKeyToAccount } from "viem/accounts";
async function signTransaction(
privateKey: Uint8Array,
txParams: {
to: string;
value: bigint;
data: string;
chainId: number;
nonce: number;
maxFeePerGas: bigint;
maxPriorityFeePerGas: bigint;
gas: bigint;
}
): Promise<string> {
const account = privateKeyToAccount(
`0x${Buffer.from(privateKey).toString("hex")}`
);
const signedTx = await account.signTransaction({
type: "eip1559",
...txParams
});
return signedTx;
}
Сумісність та тестування
Перед релізом, обов'язкова перевірка сумісності з іншими гаманцями:
| Тест | Перевірка |
|---|---|
| Імпорт в MetaMask | Та ж мнемоніка → ті ж адреси |
| Імпорт в Ledger Live | Через стандартний BIP-44 шлях |
| Імпорт в Trust Wallet | 12/24 слова, перша адреса збігається |
| Test vectors BIP-39 | Офіційні test-vectors з репозиторіїв |
Офіційні test vectors для BIP-39 та BIP-32 знаходяться в trezor/python-mnemonic та bitcoin/bips репозиторіях. Прогон всіх test vectors — обов'язковий крок перед production deploy.







