SPB Git

spb/os-vault Public

Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.

Swift 96% Shell 3.4% Makefile 0.6%
7.6 KB

# CLAUDE.md — Zyquo Wallet

# Project Overview

Zyquo Wallet is a native macOS self-custody stablecoin wallet built in Swift + SwiftUI. It manages real on-chain USDC on Base (Ethereum L2 by Coinbase — chosen for sub-cent gas fees and native Circle-issued USDC).

This is NOT a mock/demo app. Every feature must work against a real blockchain:

  1. Real HD wallet creation (BIP-39 mnemonic → secp256k1 keys)
  2. Receive USDC (real address + QR code)
  3. Send USDC (real signed ERC-20 transfers broadcast via JSON-RPC)

Development rule: build and test everything on Base Sepolia testnet first. Mainnet support is enabled last, behind an explicit network switch.


# Tech Stack

Layer Choice
Language Swift 5.10+ (strict concurrency where possible)
UI SwiftUI, macOS 14+ target
Project Xcode project, dependencies via Swift Package Manager only
Web3 web3swift (https://github.com/web3swift-team/web3swift) — actively maintained, macOS support, BIP-39/BIP-32, ERC-20, EIP-1559
QR codes CoreImage (CIQRCodeGenerator) — no extra dependency
Key storage macOS Keychain (Security framework)
Persistence UserDefaults for settings; no database needed for v1 (history read from chain/explorer API)

If web3swift causes build issues on macOS, fallback is argentlabs/web3.swift. Never mix both.


# Network Configuration (source of truth)

swift
enum Network {
    case baseSepolia   // DEFAULT during development
    case baseMainnet
}
Base Sepolia (testnet) Base Mainnet
Chain ID 84532 8453
RPC (dev) https://sepolia.base.org https://mainnet.base.org
USDC contract 0x036CbD53842c5426634e7929541eC2318f3dCF7e 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Explorer https://sepolia.basescan.org https://basescan.org
Faucets Circle faucet (https://faucet.circle.com — 20 USDC / 2h) + Base Sepolia ETH faucet for gas
  • USDC has 6 decimals. Always convert with BigUInt / decimal math — never Double. 1 USDC = 1_000_000 base units.
  • Public RPCs are fine for dev. For production, make the RPC URL configurable (Alchemy/QuickNode) via Settings — but never require an API key to run the app.
  • Gas is paid in ETH, not USDC. The UI must clearly warn when the ETH balance is too low to send.

# Architecture

MVVM + service layer. Keep views dumb; all blockchain logic lives in services.

text
ZyquoWallet/
├── App/                 ZyquoWalletApp.swift, AppState (ObservableObject / @Observable)
├── Models/              Wallet, Transaction, Network, TokenAmount
├── Services/
│   ├── KeyManager.swift        // mnemonic gen/import, keystore, Keychain I/O
│   ├── WalletService.swift     // address derivation, wallet lifecycle
│   ├── RPCService.swift        // web3 provider, network switching
│   ├── BalanceService.swift    // ETH + USDC balances, polling every 15s
│   └── TransactionService.swift// build, estimate gas, sign, send, track receipt
├── ViewModels/          OnboardingVM, HomeVM, SendVM, ReceiveVM, SettingsVM
├── Views/
│   ├── Onboarding/      Welcome, CreateWallet (show + verify mnemonic), ImportWallet
│   ├── Home/            Balance card, tx list, Send/Receive buttons
│   ├── Send/            recipient + amount + gas preview + confirm sheet
│   ├── Receive/         address display, QR, copy button
│   └── Settings/        network switch, RPC override, export seed (auth-gated), reset
└── Utils/               formatting, address validation (EIP-55), errors
  • Use async/await everywhere; wrap web3swift callbacks if needed.
  • One AppState injected via .environment; services are protocol-backed for testability.
  • Every service method that touches the network returns typed errors (WalletError enum), never fatalError.

# Security Requirements (non-negotiable)

  1. Mnemonic & private key never leave the device. No analytics, no telemetry, no network calls except JSON-RPC and explorer API.
  2. Store the mnemonic (or derived keystore) in Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Never in UserDefaults, files, or iCloud.
  3. Gate seed export and every send behind LocalAuthentication (Touch ID / password).
  4. Never log mnemonics, private keys, or signatures. Never put them in error messages.
  5. Never hardcode keys, mnemonics, or API keys anywhere in the repo — including tests. Test wallets are generated at runtime.
  6. Onboarding must force a mnemonic backup verification step (user re-enters 3 random words) before showing the wallet.
  7. Validate recipient addresses: hex format + EIP-55 checksum. Warn (don't block) on all-lowercase addresses.
  8. Enable App Sandbox (with network client entitlement) and Hardened Runtime.
  9. Confirm screen before send must show: recipient, amount, network, estimated gas, and total — no one-tap sends.
  10. If the clipboard is used for addresses, never auto-paste into the recipient field without user action (clipboard-hijack protection).

# Feature Phases (implement in order, each must compile + pass tests)

# Phase 1 — Wallet core

  • Generate 12-word BIP-39 mnemonic, derive account at m/44'/60'/0'/0/0
  • Import from mnemonic
  • Persist via KeyManager/Keychain; app relaunch restores wallet
  • Onboarding flow with backup verification

# Phase 2 — Receive

  • Show checksummed address, copy button, QR code (address only, no ethereum: URI needed for v1)

# Phase 3 — Balances

  • ETH balance (eth_getBalance) + USDC balance (balanceOf(address))
  • Poll every 15s while app is active; manual refresh; loading/error states

# Phase 4 — Send USDC

  • ERC-20 transfer(to, amount) with EIP-1559 fees
  • Gas estimation shown in ETH + rough USD
  • Sign locally, broadcast, then poll eth_getTransactionReceipt until confirmed
  • Pending/confirmed/failed states surfaced in UI, link to BaseScan

# Phase 5 — History & polish

  • Transaction list from BaseScan API (USDC transfers in/out) with graceful fallback if API unavailable
  • Network switcher (Sepolia ⇄ Mainnet) with a prominent "TESTNET" badge on Sepolia
  • Settings: custom RPC, export seed (auth-gated), delete wallet (typed confirmation)

# Build & Test Commands

bash
# Build
xcodebuild -scheme ZyquoWallet -destination 'platform=macOS' build

# Tests
xcodebuild -scheme ZyquoWallet -destination 'platform=macOS' test
  • Unit-test: mnemonic → address derivation (against known BIP-39 test vectors), USDC decimal conversion, address validation, amount formatting.
  • Never write tests that send real mainnet funds. Network-dependent tests run against Base Sepolia and must be skippable offline.

# Manual test checklist (Base Sepolia)

  1. Create wallet → relaunch app → same address restored
  2. Fund via Circle faucet (USDC) + Base faucet (ETH for gas)
  3. Balance appears within one polling cycle
  4. Send 1 USDC to a second test wallet → confirm on sepolia.basescan.org
  5. Import that second wallet from mnemonic → balance matches

# Conventions & Pitfalls

  • All amounts are BigUInt in base units internally; convert to display strings only at the UI edge.
  • USDC = 6 decimals, ETH = 18 decimals. Mixing these up is the #1 bug source.
  • Chain ID must match the selected network in every signed transaction (replay protection).
  • Handle RPC failures with retry + user-visible error; the app must never crash on network errors.
  • Comments and UI strings in English; French localization can come later via String Catalog.
  • Small, focused commits per phase. Do not start a later phase before the earlier one builds and passes tests.