Desktop Crypto Wallet Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Desktop Crypto Wallet Development
Complex
from 2 weeks to 3 months
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Desktop Crypto Wallet Development

Desktop crypto wallet is not just a convenient wrapper over web3 library. It's an application claiming high security that stores private keys in environment potentially infected with malware. Task: ensure secure key storage and convenient UX simultaneously — task where these requirements often contradict.

Examples: Rabby Wallet (Electron + TypeScript), Ledger Live (Electron, hardware wallet integration), Frame (native desktop, privacy focus), Electrum (Bitcoin-specific, one of oldest).

Technology Stack Choice

Electron vs Tauri vs Native

Electron: Node.js + Chromium. Largest ecosystem, any npm-dependencies available. Downsides: large bundle (100–200MB), higher attack surface from Chromium, historical CSP issues.

Tauri: Rust backend + system WebView (WebKit/WebView2/GTK). Small bundle (5–15MB), more secure (Rust memory safety, smaller surface), TypeScript/React frontend via IPC. Recommended for new projects.

Qt / Swift / WPF: native stack. Maximum performance and security, but high development costs, separate codebase for each platform.

For most new desktop wallets — choice between Electron and Tauri depends on size and security requirements.

// Tauri: backend command for transaction signing
// src-tauri/src/main.rs

#[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),
    })
}

// Key principle: private key never passed to JavaScript
// All crypto operations — only in Rust backend

Secure Key Storage

Keystore Format and Encryption

Standard approach — ethereum keystore format (EIP-55): key encrypted with user password, result stored in JSON file.

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 Integration

Additional layer — master password or encryption key in system 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(())
}

Protects from naive file reading — password inaccessible without system keychain. But doesn't protect from malware with privilege escalation.

Seed Phrase: Storage and Display

BIP-39 mnemonic — only way to recover wallet. Algorithm: mnemonic → seed (PBKDF2) → HD wallet (BIP-32).

// Show seed phrase ONLY in protected window
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 displayed only in isolated component
// with explicit user action and auto-hide timer
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>Hidden in: {timeLeft}s</p>
                    <SeedWords />
                </>
            ) : (
                <button onClick={() => setRevealed(true)}>
                    Show seed phrase
                </button>
            )}
        </div>
    );
}

HD Wallet and Account Management

BIP-44 defines hierarchy: m/44'/60'/account'/change/index for Ethereum. User sees "accounts" — behind them is key tree from single 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)
}

Network Connection and RPC Providers

Desktop wallet supports multiple networks (Ethereum mainnet, L2: Arbitrum, Optimism, Base, Polygon) with custom RPC ability.

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 consideration: public RPC providers (Infura, Alchemy) log IP-addresses and wallet addresses. Privacy-oriented wallets use own light client nodes or Tor.

Transaction Signing UI and Phishing Protection

Transaction signing moment — critical UX point. User must understand what's being signed.

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 and WalletConnect

Modern desktop wallet interacts with dApps via WalletConnect v2: standard protocol via QR-code or deep link. Most universal — supported by thousands 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 Wallet Integration

Desktop wallet without Ledger and Trezor support — niche product. Most serious users store significant amounts on 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);
    
    const result = await eth.signTransaction(derivationPath, txHash, null);
    return { v: parseInt(result.v, 16), r: result.r, s: result.s };
}

Auto-update and Supply Chain Security

Desktop app updates — each potentially unsafe if compromised. Mandatory measures:

Code signing: Apple Developer (macOS) and EV code signing (Windows). OS warns on unsigned app.

Reproducible builds: CI/CD public, anyone can reproduce and verify hash.

Auto-update via Sparkle (macOS) / Squirrel (Windows): updates with signature check. Tauri has built-in updater.

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

Security Testing

  • Verify private key doesn't reach JavaScript context
  • Memory dump tests (key not left after use)
  • IPC permissions check (Tauri capabilities)
  • Penetration testing XSS → key extraction (critical for Electron)
  • RPC response spoofing tests (MITM on local network)

Development Stack

Frontend: React 18 + TypeScript + Tailwind CSS, wagmi for web3 abstractions.

Backend: Tauri + Rust for crypto and key storage, or Electron + Node.js with native addons for sensitive ops.

Crypto libraries: ethers.js (JS), ethers-rs (Rust), @scure/bip39 and @scure/bip32 for HD wallet.

Testing: Playwright for E2E, Vitest for unit.

MVP with one network, basic key management, transaction sending — 3–4 months for 2–3 developer team. Production-ready with multi-chain, WalletConnect, hardware wallet, code signing and security audit — 8–12 months.