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:
- Real HD wallet creation (BIP-39 mnemonic → secp256k1 keys)
- Receive USDC (real address + QR code)
- 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 — neverDouble. 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/awaiteverywhere; wrap web3swift callbacks if needed. - One
AppStateinjected via.environment; services are protocol-backed for testability. - Every service method that touches the network returns typed errors (
WalletErrorenum), neverfatalError.
Security Requirements (non-negotiable)
- Mnemonic & private key never leave the device. No analytics, no telemetry, no network calls except JSON-RPC and explorer API.
- Store the mnemonic (or derived keystore) in Keychain with
kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Never in UserDefaults, files, or iCloud. - Gate seed export and every send behind LocalAuthentication (Touch ID / password).
- Never log mnemonics, private keys, or signatures. Never put them in error messages.
- Never hardcode keys, mnemonics, or API keys anywhere in the repo — including tests. Test wallets are generated at runtime.
- Onboarding must force a mnemonic backup verification step (user re-enters 3 random words) before showing the wallet.
- Validate recipient addresses: hex format + EIP-55 checksum. Warn (don't block) on all-lowercase addresses.
- Enable App Sandbox (with network client entitlement) and Hardened Runtime.
- Confirm screen before send must show: recipient, amount, network, estimated gas, and total — no one-tap sends.
- 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_getTransactionReceiptuntil 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)
- Create wallet → relaunch app → same address restored
- Fund via Circle faucet (USDC) + Base faucet (ETH for gas)
- Balance appears within one polling cycle
- Send 1 USDC to a second test wallet → confirm on sepolia.basescan.org
- Import that second wallet from mnemonic → balance matches
Conventions & Pitfalls
- All amounts are
BigUIntin 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.