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 · 158 lines markdown
Rendered Raw Blame History
1# CLAUDE.md — Zyquo Wallet23## Project Overview45**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).67This is NOT a mock/demo app. Every feature must work against a real blockchain:81. Real HD wallet creation (BIP-39 mnemonic → secp256k1 keys)92. Receive USDC (real address + QR code)103. Send USDC (real signed ERC-20 transfers broadcast via JSON-RPC)1112**Development rule: build and test everything on Base Sepolia testnet first. Mainnet support is enabled last, behind an explicit network switch.**1314---1516## Tech Stack1718| Layer | Choice |19|---|---|20| Language | Swift 5.10+ (strict concurrency where possible) |21| UI | SwiftUI, macOS 14+ target |22| Project | Xcode project, dependencies via Swift Package Manager only |23| Web3 | `web3swift` (https://github.com/web3swift-team/web3swift) — actively maintained, macOS support, BIP-39/BIP-32, ERC-20, EIP-1559 |24| QR codes | CoreImage (`CIQRCodeGenerator`) — no extra dependency |25| Key storage | macOS Keychain (Security framework) |26| Persistence | UserDefaults for settings; no database needed for v1 (history read from chain/explorer API) |2728If `web3swift` causes build issues on macOS, fallback is `argentlabs/web3.swift`. Never mix both.2930---3132## Network Configuration (source of truth)3334```swift35enum Network {36    case baseSepolia   // DEFAULT during development37    case baseMainnet38}39```4041| | Base Sepolia (testnet) | Base Mainnet |42|---|---|---|43| Chain ID | 84532 | 8453 |44| RPC (dev) | https://sepolia.base.org | https://mainnet.base.org |45| USDC contract | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |46| Explorer | https://sepolia.basescan.org | https://basescan.org |47| Faucets | Circle faucet (https://faucet.circle.com — 20 USDC / 2h) + Base Sepolia ETH faucet for gas | — |4849- **USDC has 6 decimals.** Always convert with `BigUInt` / decimal math — never `Double`. 1 USDC = 1_000_000 base units.50- 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.51- Gas is paid in **ETH**, not USDC. The UI must clearly warn when the ETH balance is too low to send.5253---5455## Architecture5657MVVM + service layer. Keep views dumb; all blockchain logic lives in services.5859```60ZyquoWallet/61├── App/                 ZyquoWalletApp.swift, AppState (ObservableObject / @Observable)62├── Models/              Wallet, Transaction, Network, TokenAmount63├── Services/64│   ├── KeyManager.swift        // mnemonic gen/import, keystore, Keychain I/O65│   ├── WalletService.swift     // address derivation, wallet lifecycle66│   ├── RPCService.swift        // web3 provider, network switching67│   ├── BalanceService.swift    // ETH + USDC balances, polling every 15s68│   └── TransactionService.swift// build, estimate gas, sign, send, track receipt69├── ViewModels/          OnboardingVM, HomeVM, SendVM, ReceiveVM, SettingsVM70├── Views/71│   ├── Onboarding/      Welcome, CreateWallet (show + verify mnemonic), ImportWallet72│   ├── Home/            Balance card, tx list, Send/Receive buttons73│   ├── Send/            recipient + amount + gas preview + confirm sheet74│   ├── Receive/         address display, QR, copy button75│   └── Settings/        network switch, RPC override, export seed (auth-gated), reset76└── Utils/               formatting, address validation (EIP-55), errors77```7879- Use `async/await` everywhere; wrap web3swift callbacks if needed.80- One `AppState` injected via `.environment`; services are protocol-backed for testability.81- Every service method that touches the network returns typed errors (`WalletError` enum), never `fatalError`.8283---8485## Security Requirements (non-negotiable)86871. **Mnemonic & private key never leave the device.** No analytics, no telemetry, no network calls except JSON-RPC and explorer API.882. Store the mnemonic (or derived keystore) in **Keychain** with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`. Never in UserDefaults, files, or iCloud.893. Gate seed export and every send behind **LocalAuthentication** (Touch ID / password).904. **Never log** mnemonics, private keys, or signatures. Never put them in error messages.915. Never hardcode keys, mnemonics, or API keys anywhere in the repo — including tests. Test wallets are generated at runtime.926. Onboarding must force a **mnemonic backup verification** step (user re-enters 3 random words) before showing the wallet.937. Validate recipient addresses: hex format + EIP-55 checksum. Warn (don't block) on all-lowercase addresses.948. Enable **App Sandbox** (with network client entitlement) and **Hardened Runtime**.959. Confirm screen before send must show: recipient, amount, network, estimated gas, and total — no one-tap sends.9610. If the clipboard is used for addresses, never auto-paste into the recipient field without user action (clipboard-hijack protection).9798---99100## Feature Phases (implement in order, each must compile + pass tests)101102### Phase 1 — Wallet core103- Generate 12-word BIP-39 mnemonic, derive account at `m/44'/60'/0'/0/0`104- Import from mnemonic105- Persist via KeyManager/Keychain; app relaunch restores wallet106- Onboarding flow with backup verification107108### Phase 2 — Receive109- Show checksummed address, copy button, QR code (address only, no `ethereum:` URI needed for v1)110111### Phase 3 — Balances112- ETH balance (`eth_getBalance`) + USDC balance (`balanceOf(address)`)113- Poll every 15s while app is active; manual refresh; loading/error states114115### Phase 4 — Send USDC116- ERC-20 `transfer(to, amount)` with EIP-1559 fees117- Gas estimation shown in ETH + rough USD118- Sign locally, broadcast, then poll `eth_getTransactionReceipt` until confirmed119- Pending/confirmed/failed states surfaced in UI, link to BaseScan120121### Phase 5 — History & polish122- Transaction list from BaseScan API (USDC transfers in/out) with graceful fallback if API unavailable123- Network switcher (Sepolia ⇄ Mainnet) with a prominent "TESTNET" badge on Sepolia124- Settings: custom RPC, export seed (auth-gated), delete wallet (typed confirmation)125126---127128## Build & Test Commands129130```bash131# Build132xcodebuild -scheme ZyquoWallet -destination 'platform=macOS' build133134# Tests135xcodebuild -scheme ZyquoWallet -destination 'platform=macOS' test136```137138- Unit-test: mnemonic → address derivation (against known BIP-39 test vectors), USDC decimal conversion, address validation, amount formatting.139- Never write tests that send real mainnet funds. Network-dependent tests run against Base Sepolia and must be skippable offline.140141## Manual test checklist (Base Sepolia)1421. Create wallet → relaunch app → same address restored1432. Fund via Circle faucet (USDC) + Base faucet (ETH for gas)1443. Balance appears within one polling cycle1454. Send 1 USDC to a second test wallet → confirm on sepolia.basescan.org1465. Import that second wallet from mnemonic → balance matches147148---149150## Conventions & Pitfalls151152- All amounts are `BigUInt` in base units internally; convert to display strings only at the UI edge.153- USDC = 6 decimals, ETH = 18 decimals. Mixing these up is the #1 bug source.154- Chain ID must match the selected network in every signed transaction (replay protection).155- Handle RPC failures with retry + user-visible error; the app must never crash on network errors.156- Comments and UI strings in English; French localization can come later via String Catalog.157- Small, focused commits per phase. Do not start a later phase before the earlier one builds and passes tests.158