Розробка десктопного криптовалютного гаманця
Десктопний криптовалютний гаманець — це не просто зручна обгортка над web3 бібліотекою. Це програма з претензією на високу безпеку яка зберігає приватні ключі у середовищі потенційно заразженому malware. Завдання: забезпечити безпечне зберігання ключів та зручний UX одночасно — завдання де ці вимоги часто суперечать один одному.
Приклади: Rabby Wallet (Electron + TypeScript), Ledger Live (Electron, інтеграція hardware wallet), Frame (нативний desktop, focus на privacy), Electrum (Bitcoin-специфічний, один з найстаріших).
Вибір технологічного стека
Electron vs Tauri vs Native
Electron: Node.js + Chromium. Найбільша екосистема, будь-які npm-залежності доступні. Недоліки: великий bundle (100–200MB), більший attack surface від Chromium, історичні CSP проблеми.
Tauri: Rust backend + системний WebView (WebKit/WebView2/GTK). Малий bundle (5–15MB), більш безпечний (Rust memory safety, менший surface), TypeScript/React frontend через IPC. Рекомендований для нових проектів.
Qt / Swift / WPF: нативний стек. Максимальна продуктивність та безпека, але високі затрати розробки, окремий codebase для кожної платформи.
Для більшості нових десктопних гаманців — вибір між Electron та Tauri залежить від вимог до розміру та безпеки.
// Tauri: backend команда для підпису транзакції
#[command]
async fn sign_transaction(
tx_hash: String,
wallet_id: String,
password: String
) -> Result<SignatureResult, String> {
let keystore = state.keystore.lock().await;
let wallet: LocalWallet = keystore
.load_wallet(&wallet_id, &password)
.map_err(|e| format!("Failed to load wallet: {}", e))?;
let hash = H256::from_slice(&hex::decode(&tx_hash)?);
let signature = wallet.sign_hash(hash).await?;
Ok(SignatureResult {
v: signature.v,
r: format!("{:x}", signature.r),
s: format!("{:x}", signature.s),
})
}
// Ключовий принцип: приватний ключ ніколи не передається в JavaScript
// Усі крипто-операції — тільки в Rust backend
Безпечне зберігання ключів
Формат Keystore та шифрування
Стандартний підхід — ethereum keystore формат (EIP-55): ключ шифрується паролем користувача, результат зберігається в JSON файлі.
pub struct KeystoreManager {
keystore_dir: PathBuf,
}
impl KeystoreManager {
pub fn create_wallet(&self, password: &str) -> Result<WalletInfo, KeystoreError> {
let mut rng = rand::thread_rng();
let mut private_key = [0u8; 32];
rng.fill_bytes(&mut private_key);
let signing_key = SigningKey::from_bytes(&private_key)?;
let wallet = LocalWallet::from(signing_key);
let address = wallet.address();
let filename = format!("wallet-{}.json", address);
let keystore_path = self.keystore_dir.join(&filename);
encrypt_key(&keystore_path, &mut rng, private_key, password, Some(&filename))?;
private_key.zeroize();
Ok(WalletInfo {
id: filename,
address: format!("{:?}", address),
})
}
}
Інтеграція OS-level Keychain
Додатковий рівень — зберігання мастер-пароля або ключа шифрування у системному keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service).
use keyring::Entry;
pub fn store_master_password(wallet_id: &str, password: &str) -> Result<(), KeyringError> {
let entry = Entry::new("crypto-wallet", wallet_id)?;
entry.set_password(password)?;
Ok(())
}
Захищає від наївного читання файлів — пароль недоступний без системного keychain. Але не захищає від malware з privilege escalation.
Seed phrase: зберігання та відображення
BIP-39 мнемоніка — єдиний спосіб відновити гаманець. Алгоритм: мнемоніка → seed (PBKDF2) → HD wallet (BIP-32).
// Показуємо seed phrase ТІЛЬКИ в захищеному вікні
import * as bip39 from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { HDKey } from '@scure/bip32';
function generateWallet() {
return window.__TAURI__.invoke('generate_new_wallet');
}
// Seed відображається тільки в ізольованому компоненті
// з явним user action та таймером auto-hide
function SeedPhraseDisplay({ onConfirmed }) {
const [revealed, setRevealed] = useState(false);
const [timeLeft, setTimeLeft] = useState(60);
useEffect(() => {
if (revealed) {
const timer = setInterval(() => {
setTimeLeft(t => {
if (t <= 1) {
setRevealed(false);
return 60;
}
return t - 1;
});
}, 1000);
return () => clearInterval(timer);
}
}, [revealed]);
return (
<div style={{ userSelect: 'none' }}>
{revealed ? (
<>
<p>Скрито за: {timeLeft}s</p>
<SeedWords />
</>
) : (
<button onClick={() => setRevealed(true)}>
Показати seed phrase
</button>
)}
</div>
);
}
HD Wallet та управління аккаунтами
BIP-44 визначає ієрархію: m/44'/60'/account'/change/index для Ethereum. Користувач бачить «аккаунти» — за ними стоїть дерево дочірніх ключів з однієї seed.
pub fn derive_accounts(
mnemonic: &str,
count: u32
) -> Result<Vec<DerivedAccount>, DerivationError> {
let mnemonic = Mnemonic::from_phrase(mnemonic, Language::English)?;
let seed = mnemonic.to_seed("");
let root = XPriv::root_from_seed(&seed, None)?;
let mut accounts = Vec::new();
for index in 0..count {
let path = format!("m/44'/60'/0'/0/{}", index);
let child_key = root.derive_path(&path)?;
let wallet = LocalWallet::from(SigningKey::from(child_key.as_ref()));
accounts.push(DerivedAccount {
index,
address: format!("{:?}", wallet.address()),
path,
});
}
Ok(accounts)
}
Підключення до мереж та RPC провайдери
Десктопний гаманець підтримує кілька мереж (Ethereum mainnet, L2: Arbitrum, Optimism, Base, Polygon) з можливістю додавання кастомних RPC.
interface NetworkConfig {
chainId: number;
name: string;
rpcUrls: string[];
nativeCurrency: { symbol: string; decimals: number };
blockExplorer?: string;
}
class MultiChainProvider {
private providers: Map<number, ethers.JsonRpcProvider[]> = new Map();
async getProvider(chainId: number): Promise<ethers.JsonRpcProvider> {
const providers = this.providers.get(chainId);
if (!providers?.length) throw new Error(`Unknown chain ${chainId}`);
for (const provider of providers) {
try {
await provider.getBlockNumber();
return provider;
} catch {
continue;
}
}
throw new Error(`No available RPC for chain ${chainId}`);
}
}
Privacy розумування: публічні RPC провайдери (Infura, Alchemy) логують IP-адреси та адреси гаманців. Privacy-орієнтовані гаманці використовують власні light client ноди або Tor.
Transaction Signing UI та захист від фішингу
Момент підпису транзакції — критична UX точка. Користувач повинен розуміти що підписується.
interface TransactionPreview {
type: 'ETH_TRANSFER' | 'TOKEN_TRANSFER' | 'CONTRACT_INTERACTION' | 'APPROVE';
to: string;
toLabel?: string;
toRisk: 'safe' | 'unknown' | 'suspicious';
value?: string;
tokenAmount?: string;
estimatedGasUSD: string;
totalCostUSD: string;
decodedCalldata?: { methodName: string; params: Record<string, string> };
warnings: string[];
}
async function buildTransactionPreview(
tx: ethers.TransactionRequest
): Promise<TransactionPreview> {
const warnings: string[] = [];
if (tx.data && tx.data !== '0x') {
const decoded = await decodeTransactionData(tx.data, tx.to);
if (decoded?.methodName === 'approve') {
const amount = decoded.params.amount;
if (BigInt(amount) === MaxUint256) {
warnings.push('⚠️ Unlimited approval');
}
}
}
const toRisk = await checkAddressRisk(tx.to);
if (toRisk === 'suspicious') {
warnings.push('🚨 Address flagged as suspicious');
}
return {
type: determineTransactionType(tx),
to: tx.to,
estimatedGasUSD: await estimateGasInUSD(tx),
totalCostUSD: await calculateTotalCostUSD(tx),
warnings
};
}
dApp Browser та WalletConnect
Сучасний десктопний гаманець взаємодіє з dApps через WalletConnect v2: стандартний протокол через QR-код або deep link. Найбільш універсальний — підтримується тисячами dApps.
import { Web3Wallet } from "@walletconnect/web3wallet";
const web3wallet = await Web3Wallet.init({
core: new Core({ projectId: WC_PROJECT_ID }),
metadata: { name: 'My Desktop Wallet', ... }
});
web3wallet.on('session_request', async ({ id, topic, params }) => {
const { request } = params;
if (request.method === 'eth_sendTransaction') {
const preview = await buildTransactionPreview(request.params[0]);
const approved = await showConfirmationDialog(preview);
if (approved) {
const signedTx = await signTransaction(request.params[0]);
await web3wallet.respondSessionRequest({
topic,
response: { id, result: signedTx, jsonrpc: "2.0" }
});
}
}
});
Інтеграція Hardware Wallets
Десктопний гаманець без підтримки Ledger та Trezor — нішевий продукт. Більшість серйозних користувачів зберігають значні суми на апаратних гаманцях.
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid';
import Eth from '@ledgerhq/hw-app-eth';
async function signWithLedger(
txHash: string,
derivationPath: string = "44'/60'/0'/0/0"
): Promise<{ v: number; r: string; s: string }> {
const transport = await TransportNodeHid.create(5000);
const eth = new Eth(transport);
const result = await eth.signTransaction(derivationPath, txHash, null);
return { v: parseInt(result.v, 16), r: result.r, s: result.s };
}
Auto-update та безпека поставок
Десктопна програма оновлюється — кожне оновлення потенційно небезпечне якщо процес скомпрометований. Обов'язкові заходи:
Code signing: Apple Developer (macOS) та EV code signing (Windows). ОС попереджає при запуску неподписаної програми.
Reproducible builds: CI/CD публічна, будь-хто може відтворити та верифікувати hash.
Auto-update через Sparkle (macOS) / Squirrel (Windows): оновлення з перевіркою підпису. Tauri має вбудований updater.
{
"tauri": {
"updater": {
"active": true,
"endpoints": ["https://releases.mydesktopwallet.com/{{target}}/{{current_version}}"],
"dialog": true,
"pubkey": "PUBLICKEYHERE"
}
}
}
Security Testing
- Верифікація що приватний ключ не потрапляє в JavaScript контекст
- Memory dump тести (ключ не залишається після використання)
- IPC permissions перевірка (Tauri capabilities)
- Penetration testing XSS → key extraction (критичен для Electron)
- RPC response spoofing тести (MITM на локальній мережі)
Стек розробки
Frontend: React 18 + TypeScript + Tailwind CSS, wagmi для web3 абстракцій.
Backend: Tauri + Rust для крипто та зберігання ключів, або Electron + Node.js з native addons для чутливих операцій.
Крипто-бібліотеки: ethers.js (JS), ethers-rs (Rust), @scure/bip39 та @scure/bip32 для HD wallet.
Тестування: Playwright для E2E, Vitest для unit.
MVP з однією мережею, базовим управлінням ключами, відправкою транзакцій — 3–4 місяці для команди 2–3 розробників. Production-ready з мультиланцюгом, WalletConnect, hardware wallet, code signing та security audit — 8–12 місяців.







