Разработка десктопного криптокошелька

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка десктопного криптокошелька
Сложная
от 2 недель до 3 месяцев
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка десктопного криптокошелька

Десктопный криптокошелёк — это не просто удобная обёртка над web3 библиотекой. Это приложение с претензией на высокую безопасность, которое хранит приватные ключи в среде, потенциально заражённой malware. Задача: обеспечить безопасное хранение ключей и удобный UX одновременно — задача, в которой эти два требования часто противоречат друг другу.

Примеры эталонных решений: Rabby Wallet (Electron + TypeScript), Ledger Live (Electron, интеграция с hardware wallet), Frame (нативный desktop, фокус на privacy), Electrum (Bitcoin-специфичный, один из старейших).

Выбор технологического стека

Electron vs Tauri vs нативный

Electron: Node.js + Chromium. Наибольшая экосистема, любые npm-зависимости доступны. Недостатки: большой размер бандла (100–200MB), выше attack surface за счёт Chromium, исторически проблемы с CSP.

Tauri: Rust backend + системный WebView (WebKit/WebView2/GTK). Маленький бандл (5–15MB), более безопасный (Rust memory safety, меньший surface), TypeScript/React frontend через IPC. Рекомендован для новых проектов.

Qt / Swift / WPF: нативный стек. Максимальная производительность и безопасность, но высокие затраты разработки, отдельный codebase для каждой платформы.

Для большинства новых десктопных кошельков выбор между Electron и Tauri зависит от требований к размеру и security posture.

// Tauri: backend команда для подписания транзакции
// src-tauri/src/main.rs

use tauri::command;
use ethers::signers::{LocalWallet, Signer};
use ethers::types::{Transaction, H256};
use keystore::KeystoreManager;

#[command]
async fn sign_transaction(
    tx_hash: String,
    wallet_id: String,
    password: String,
    state: tauri::State<'_, AppState>
) -> 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).map_err(|e| e.to_string())?
    );
    
    // Подпись происходит в Rust процессе, TypeScript не видит ключ
    let signature = wallet.sign_hash(hash).await
        .map_err(|e| format!("Signing failed: {}", e))?;
    
    Ok(SignatureResult {
        v: signature.v,
        r: format!("{:x}", signature.r),
        s: format!("{:x}", signature.s),
    })
}

// Ключевой принцип: приватный ключ никогда не передаётся в JavaScript контекст
// Все криптографические операции — только в Rust backend

Безопасное хранение ключей

Keystore формат и шифрование

Стандартный подход — ethereum keystore формат (EIP-55): ключ шифруется паролем пользователя, результат хранится в JSON файле.

use eth_keystore::{encrypt_key, decrypt_key};
use rand::RngCore;

pub struct KeystoreManager {
    keystore_dir: PathBuf,
}

impl KeystoreManager {
    pub fn create_wallet(&self, password: &str) -> Result<WalletInfo, KeystoreError> {
        // Генерация ключа с OS CSPRNG
        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)
        )?;
        
        // Очищаем ключ из памяти (best effort в Rust без зашифрованной памяти)
        private_key.zeroize();
        
        Ok(WalletInfo {
            id: filename,
            address: format!("{:?}", address),
        })
    }
    
    pub fn load_wallet(&self, wallet_id: &str, password: &str) 
        -> Result<LocalWallet, KeystoreError> 
    {
        let keystore_path = self.keystore_dir.join(wallet_id);
        let private_key = decrypt_key(&keystore_path, password)?;
        
        let wallet = LocalWallet::from(
            SigningKey::from_bytes(&private_key)?
        );
        
        Ok(wallet)
    }
}

OS-level Keychain интеграция

Для дополнительного слоя защиты — хранение мастер-пароля или encryption key в системном keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service / KWallet).

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(())
}

pub fn load_master_password(wallet_id: &str) -> Result<String, KeyringError> {
    let entry = Entry::new("crypto-wallet", wallet_id)?;
    entry.get_password()
}

Это защищает от наивного чтения файлов — без системного keychain пароль недоступен. Но не защищает от malware с privilege escalation.

Seed phrase: хранение и отображение

BIP-39 mnemonic — единственный способ восстановить кошелёк. Алгоритм деривации: mnemonic → seed (PBKDF2) → HD wallet (BIP-32).

// Frontend: показываем seed phrase ТОЛЬКО в защищённом окне
// Не копируем в clipboard без явного разрешения пользователя

import * as bip39 from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { HDKey } from '@scure/bip32';

function generateWallet() {
    // Генерация происходит через Tauri backend
    // Frontend получает только адреса и публичные ключи
    return window.__TAURI__.invoke('generate_new_wallet');
}

// Seed phrase отображается только в изолированном компоненте
// с явным user action для отображения и таймером автоскрытия
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]);
    
    // Блокируем скриншоты через CSS (не 100% защита, но лучше чем ничего)
    return (
        <div style={{ userSelect: 'none', WebkitUserSelect: 'none' }}>
            {revealed ? (
                <>
                    <p>Скрывается через: {timeLeft}с</p>
                    <SeedWords />
                </>
            ) : (
                <button onClick={() => setRevealed(true)}>
                    Показать seed phrase
                </button>
            )}
        </div>
    );
}

HD Wallet и управление аккаунтами

BIP-44 определяет иерархию деривации: m/44'/60'/account'/change/index для Ethereum. Пользователь видит «аккаунты», за которыми стоят дерево дочерних ключей от одного seed.

use bip39::{Mnemonic, Language};
use hdpath::StandardHDPath;
use coins_bip32::XPriv;

pub fn derive_accounts(
    mnemonic: &str, 
    password: &str,
    count: u32
) -> Result<Vec<DerivedAccount>, DerivationError> {
    let mnemonic = Mnemonic::from_phrase(mnemonic, Language::English)?;
    let seed = mnemonic.to_seed(password);
    
    let root = XPriv::root_from_seed(&seed, None)?;
    let mut accounts = Vec::new();
    
    for index in 0..count {
        // BIP-44 путь: m/44'/60'/0'/0/index
        let path = format!("m/44'/60'/0'/0/{}", index);
        let child_key = root.derive_path(&path)?;
        let signing_key = child_key.as_ref();
        
        let wallet = LocalWallet::from(SigningKey::from(signing_key));
        
        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[];      // Несколько URL для failover
    nativeCurrency: {
        symbol: string;
        decimals: number;
    };
    blockExplorer?: string;
}

const DEFAULT_NETWORKS: NetworkConfig[] = [
    {
        chainId: 1,
        name: 'Ethereum Mainnet',
        rpcUrls: [
            'https://eth.llamarpc.com',
            'https://cloudflare-eth.com',
            'https://rpc.ankr.com/eth'
        ],
        nativeCurrency: { symbol: 'ETH', decimals: 18 },
        blockExplorer: 'https://etherscan.io'
    },
    {
        chainId: 42161,
        name: 'Arbitrum One',
        rpcUrls: ['https://arb1.arbitrum.io/rpc', 'https://arbitrum.llamarpc.com'],
        nativeCurrency: { symbol: 'ETH', decimals: 18 },
        blockExplorer: 'https://arbiscan.io'
    }
];

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}`);
        
        // Проверяем провайдеры по очереди (failover)
        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, QuickNode) логируют IP-адреса и адреса кошельков. Privacy-ориентированные кошельки используют собственные light client ноды или onion-routing (через Tor).

Transaction signing UI и защита от phishing

Момент подписания транзакции — критическая точка UX. Пользователь должен понимать, что именно подписывает.

interface TransactionPreview {
    type: 'ETH_TRANSFER' | 'TOKEN_TRANSFER' | 'CONTRACT_INTERACTION' | 'APPROVE';
    to: string;
    toLabel?: string;           // ENS или адресная книга
    toRisk: 'safe' | 'unknown' | 'suspicious'; // Проверка по базам scam адресов
    value?: string;             // Количество ETH
    tokenAmount?: string;       // Количество токена
    tokenSymbol?: string;
    estimatedGasUSD: string;
    totalCostUSD: string;
    decodedCalldata?: {         // Декодированные параметры вызова
        methodName: string;
        params: Record<string, string>;
    };
    warnings: string[];         // Предупреждения (unlimited approve, suspicious contract и т.д.)
}

async function buildTransactionPreview(
    tx: ethers.TransactionRequest,
    chainId: number
): Promise<TransactionPreview> {
    const warnings: string[] = [];
    
    // Декодируем calldata если есть
    let decodedCalldata;
    if (tx.data && tx.data !== '0x') {
        decodedCalldata = await decodeTransactionData(tx.data, tx.to, chainId);
        
        // Предупреждение на unlimited approve
        if (decodedCalldata?.methodName === 'approve') {
            const amount = decodedCalldata.params.amount;
            if (BigInt(amount) === MaxUint256) {
                warnings.push('⚠️ Unlimited approval: это позволяет контракту тратить все ваши токены');
            }
        }
    }
    
    // Проверка адреса получателя по blocklist
    const toRisk = await checkAddressRisk(tx.to, chainId);
    if (toRisk === 'suspicious') {
        warnings.push('🚨 Этот адрес отмечен как подозрительный в базах scam-репортов');
    }
    
    // ENS резолюция
    const toLabel = await resolveENS(tx.to, chainId);
    
    return {
        type: determineTransactionType(tx),
        to: tx.to,
        toLabel,
        toRisk,
        estimatedGasUSD: await estimateGasInUSD(tx, chainId),
        totalCostUSD: await calculateTotalCostUSD(tx, chainId),
        decodedCalldata,
        warnings
    };
}

DApp Browser и WalletConnect

Современный десктопный кошелёк должен взаимодействовать с dApps. Два пути:

WalletConnect v2: стандартный протокол соединения кошелька с dApp через QR-код или deep link. Наиболее универсален — поддерживается тысячами dApps.

import { WalletConnectModal } from '@walletconnect/modal';
import { Web3Wallet } from '@walletconnect/web3wallet';

const web3wallet = await Web3Wallet.init({
    core: new Core({ projectId: WALLETCONNECT_PROJECT_ID }),
    metadata: {
        name: 'My Desktop Wallet',
        description: 'Secure Desktop Crypto Wallet',
        url: 'https://mydesktopwallet.com',
        icons: ['https://mydesktopwallet.com/icon.png']
    }
});

// Обработка запросов от dApp
web3wallet.on('session_request', async ({ id, topic, params }) => {
    const { request } = params;
    
    if (request.method === 'eth_sendTransaction') {
        // Показываем TransactionPreview пользователю
        const preview = await buildTransactionPreview(request.params[0], chainId);
        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' }
            });
        } else {
            await web3wallet.respondSessionRequest({
                topic,
                response: { 
                    id, 
                    error: { code: 4001, message: 'User rejected' }, 
                    jsonrpc: '2.0' 
                }
            });
        }
    }
});

Интеграция с Hardware Wallets

Десктопный кошелёк без поддержки Ledger и Trezor — это нишевый продукт. Большинство серьёзных пользователей хранят значительные суммы на hardware.

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);
    
    try {
        // Показываем пользователю инструкцию подтвердить на устройстве
        const result = await eth.signTransaction(
            derivationPath,
            txHash,
            null
        );
        
        return { 
            v: parseInt(result.v, 16), 
            r: result.r, 
            s: result.s 
        };
    } finally {
        await transport.close();
    }
}

Auto-update и безопасность поставок

Десктопное приложение обновляется — и каждое обновление потенциально небезопасно, если процесс скомпрометирован. Обязательные меры:

Code signing: подпись бинарников сертификатами Apple Developer (macOS) и EV code signing certificate (Windows). Операционная система предупреждает пользователя при запуске неподписанного приложения.

Reproducible builds: CI/CD конфигурация публична, любой может воспроизвести сборку и верифицировать hash бинарника.

Auto-update через Sparkle (macOS) / Squirrel (Windows): обновления с проверкой подписи. Tauri имеет встроенный updater.

// tauri.conf.json — updater config
{
  "tauri": {
    "updater": {
      "active": true,
      "endpoints": [
        "https://releases.mydesktopwallet.com/{{target}}/{{current_version}}"
      ],
      "dialog": true,
      "pubkey": "PUBLICKEYHERE"
    }
  }
}

Тестирование безопасности

Специфические тесты для десктопного кошелька:

  • Проверка, что приватный ключ не попадает в JavaScript контекст
  • Тест на memory dumps (ключ не остаётся в памяти после использования)
  • Проверка IPC permissions (Tauri: capabilities config — какие backend команды доступны frontend)
  • Penetration testing на XSS → key extraction (для Electron — наиболее критичный вектор)
  • Тест на подмену RPC ответов (man-in-the-middle на уровне локальной сети)

Стек и сроки разработки

Frontend: React 18 + TypeScript + Tailwind CSS, wagmi для web3 абстракций.

Backend: Tauri + Rust для криптографии и хранения ключей, или Electron + Node.js с native addons для sensitive операций.

Крипто-библиотеки: ethers.js (JS), ethers-rs / alloy (Rust), @scure/bip39 и @scure/bip32 для HD wallet.

Тестирование: Playwright для E2E, Vitest для unit тестов.

MVP с поддержкой одной сети, базовым управлением ключами и отправкой транзакций — 3–4 месяца для команды из 2–3 разработчиков. Production-ready кошелёк с multi-chain, WalletConnect, hardware wallet интеграцией, code signing и аудитом безопасности — 8–12 месяцев.