Skip to content

Latest commit

 

History

History
184 lines (141 loc) · 15 KB

CLAUDE.md

File metadata and controls

184 lines (141 loc) · 15 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Dash Wallet is an iOS cryptocurrency wallet for the Dash network — a fork of breadwallet implementing SPV (Simplified Payment Verification) for fast mobile performance. Advanced features include DashPay (user-to-user transactions), CoinJoin (privacy), and integrations with external services such as Uphold and Coinbase.

Build & Setup

pod install                   # Install dependencies (run after Podfile changes; avoid `pod update` unless needed)
open DashWallet.xcworkspace   # Always open the workspace, not the .xcodeproj
  • Schemes: dashpay (DashPay-enabled — the working scheme during the SwiftDashSDK migration), dashwallet (main app; not kept green on this branch)
  • Configurations: Debug, TestNet, Release, TestFlight
  • Canonical build — the SwiftDashSDK FFI xcframework ships an arm64-only simulator slice, so always force ARCHS=arm64:
    xcodebuild -workspace DashWallet.xcworkspace -scheme dashpay -sdk iphonesimulator \
      -destination 'generic/platform=iOS Simulator' ARCHS=arm64 build
  • Tests: DashWalletTests runs against the dashpay host app through the dashwallet-dashpay scheme, in the Debug configuration (the only one with ENABLE_TESTABILITY, which every @testable import dashpay needs). Cmd+U in Xcode, or:
    xcodebuild test -workspace DashWallet.xcworkspace -scheme dashwallet-dashpay -configuration Debug \
      -destination 'platform=iOS Simulator,name=iPhone 17' ARCHS=arm64 ONLY_ACTIVE_ARCH=YES
    Every test file must be in the target: files under DashWalletTests/Notifications/ are picked up automatically (synchronized folder), everything else needs the four project.pbxproj entries. scripts/ci/check_test_target_membership.py fails CI on a forgotten file. The host app needs the gitignored *-Info.plist files in DashWallet/; without the real ones, scripts/ci/make-placeholder-plists.sh writes inert placeholders. CI runs the suite on every PR (.github/workflows/unit-tests.yml) against the platform commit pinned in .platform-ref.

Required tools

  • Xcode 16+
  • CocoaPods (gem install cocoapods)
  • iOS deployment target: 18.0 (raised from 14.0 during the migration — Podfile platform :ios, '18.0' + post_install)
  • Rust toolchain (builds the SwiftDashSDK FFI)

Optional tools

  • swiftformat, swiftlint, clang-format (Objective-C), bartycrouch (localization)

External repo dependencies (expected as sibling directories)

../platform/        # dashpay/platform monorepo — SwiftDashSDK lives at packages/swift-sdk (local SPM dependency)
../dapi-grpc/       # gRPC API definitions
../dashwallet-ios/  # This repository

The SwiftDashSDK DashSDKFFI.xcframework is gitignored — after pulling ../platform, rebuild it:

cd ../platform/packages/swift-sdk && ./build_ios.sh --target ios --target sim

If the app fails with missing SDK symbols (e.g. "no member …"), verify the ../platform checkout and rebuild both XCFramework slices.

Figma

Figma Dev Mode MCP setup and cleaning Figma-exported SVG assets for iOS are covered by the figma-assets skill (.claude/skills/figma-assets/SKILL.md).

Architecture

  • DashWallet/Sources/Infrastructure/SwiftDashSDK/ — the SwiftDashSDK adapter layer (host, wallet runtime/state, SPV coordinator, transaction sender, identity coordinators) — the app-side boundary to the Rust SDK

Notable services & files

  • WalletSendService.swift — the send boundary: auth → build+sign (SwiftDashSDKTransactionSender) → user confirm → broadcast. SendCoinsService.swift is a thin programmatic wrapper over it.
  • SwiftDashSDKHost.swift / SwiftDashSDKWalletRuntime.swift / SwiftDashSDKWalletState.swift / SwiftDashSDKSPVCoordinator.swift — SDK lifecycle ownership, published wallet state (balance), and chain sync
  • CurrencyExchanger.swift — fiat currency conversion
  • DatabaseConnection.swift + Migrations.bundle/ — SQLite (schema changes require a new timestamped migration)
  • MainTabbarController.swift — tab-based navigation
  • UIHostingController+DashWallet.swift — UIKit ↔ SwiftUI bridge

SwiftDashSDK ownership and upgrade compatibility

The DashSync dependency unlink is complete. The app and SwiftDashSDK own all live wallet behavior. DASHSYNC_KEY_MIGRATION.md remains the specification for the frozen upgrade-time mnemonic import contract.

  • No DashSync runtime: do not restore the pod, direct imports, DWEnvironment, or live DS* wallet/transaction/identity objects. Read wallet state through the app-owned SwiftDashSDK boundaries.
  • Sync gating: never gate on SPV state == .synced — dash-spv's steady state when fully synced is waitForEvents at progress ≈ 1.0 (.synced is a transient window). Gate on SyncingActivityMonitor (.syncDone).
  • Sends: every spend goes through WalletSendService (standard) or SwiftDashSDKTransactionSender (selected-input / sweep). Never call CoreTransactionBuilder from UI code. prepare* never broadcasts; broadcast happens only on explicit confirm.
  • Mnemonic ownership: stored as plain keychain bytes via SwiftDashSDK WalletStorage (the iOS keychain is the security boundary — there is no PIN-encryption layer). Creation/import/migration route through SwiftDashSDKHost; deletion routes through SwiftDashSDKWalletWiper. The migrator supports multiple wallets and never deletes DashSync-owned org.dashfoundation.dash entries. Don't add ad-hoc WalletStorage() readers or choose the first mnemonic — resolve the active wallet through SwiftDashSDKHost / WalletEnvironment.activeWalletId(for:).
  • CrowdNode release posture: temporarily suspend/hide CrowdNode for the migration release. Its implementation is already app/SDK-owned, so it does not block DashSync teardown and must not regain a DashSync fallback.
  • Platform release pin: temporary local integration branches are development-only. Upstream all required changes and make the final migration/release build consume platform v5.0-dev at 5.0.0-rc.4 (5b41ca376b, what testnet runs) or a later commit with the same v13.rs/v14.rs, or a later line whose protocol 14 is the one testnet runs (v5.1-dev at d4750290ff has the same v13.rs/v14.rs as 5.0.0-rc.4), or an explicitly selected release SHA/tag. Not v4.2-dev: its pre-release protocol 14 differs, and the testnet pin in SwiftDashSDKHost.platformVersion(for:) builds transitions from the linked SDK's v14.
  • Mainnet is the fresh-install default: WalletEnvironment.networkKind returns .mainnet when CURRENT_CHAIN_TYPE_KEY is absent, in ALL build configurations; testnet requires an explicit in-app switch, which writes the key. (This reverses the temporary testnet default of 2026-07-03, taken while mainnet Platform DAPI was unreachable — revisited by the owner 2026-08-07.) An install that never used the switcher moves to mainnet on its next launch. When touching this, verify mainnet Platform DAPI is actually reachable in the SDK build being shipped.

UI Development — SwiftUI-First (Mandatory)

All new UI MUST be built in SwiftUI with a ViewModel. Do not add new Storyboards, XIBs, or UIViewControllers containing UI logic.

  • Keep views lightweight; put business logic in ObservableObject ViewModels (@MainActor, @Published state).
  • Concretely banned inside SwiftUI View structs: FFI/SDK calls, fee math, direct auth calls (use AuthenticationGate), DSChain→network mapping, protocol constants. Those live in the ViewModel or a service.
  • When integrating with existing UIKit navigation, use a thin UIHostingController wrapper only.
  • Maintain existing UIKit code but don't extend it; migrate a screen to SwiftUI when substantially reworking it.

Accessibility

Icon-only controls must carry an accessibilityLabel — see ACCESSIBILITY.md. Check your work before committing:

scripts/a11y_audit.py --check          # what CI runs; fails only on NEW findings
scripts/a11y_audit.py --path "DashWallet/Sources/UI/Payments"   # one screen

accessibilityIdentifier is an XCUITest id and is never spoken — it does not count as a label. .github/workflows/accessibility.yml enforces this on every PR.

Architecture Guardrails (check before every commit)

Distilled from the 2026-07 branch review (ARCH_REVIEW_2026-07-03.md) — each rule exists because we shipped its violation:

  1. Never copy-then-adapt. If a helper you need is private/fileprivate in another file (auth gate, main-thread trampoline, SwiftData lookup), promote it to internal and reuse it — do not paste a copy, even "temporarily". Copies drift: one clone silently lost the auth watchdog, another missed a cold-launch bug fix. Shared primitives to reuse by name: AuthenticationGate (WalletSendService.swift) for PIN/biometric prompts; ScriptAddressCodec for address↔script.
  2. Comments state what the code does, never what it should do. Before writing "X does Y", verify Y's symbol exists and is called (grep it). A deferred behavior is written as TODO(label): <the gap> — never as present-tense description. (We shipped a comment claiming a method "does the mirror writes" that didn't exist, and a "confirmation with the real fee" that was neither.)
  3. No stub-and-assert. A stubbed path must not report success: no UI copy claiming a check passed that never ran, no no-op methods that pretend to act, no silently discarded user input. Stubs throw, disable the control, or are compile-gated. Model unknown data as unknown (nil / .unknown case) — never fabricate plausible defaults (.classic, .ok, [], false).
  4. New singletons need a reason. Don't add another static let shared in Sources/Infrastructure/ without a protocol seam or a written justification in the type doc; prefer injecting. One file = one responsibility — a "coordinator" that accumulates published UI counters, storage wipes, and money movement gets split.
  5. Never re-emit another system's notification names. Re-posting DS* notification strings from new state poisons every future grep audit. New state gets a new typed publisher/notification; consumers migrate.
  6. No debug residue in commits: no session tags in log lines (à la CJTEST), no plan-diary comments ("Row #17 stage A", "M5/M6"), no stale line-number cross-references in doc comments. Use TODO(label) and link docs instead.
  7. Legacy compatibility stays bounded and honest. Do not restore staged DashSync/SDK paths. Only the frozen key migrator may read the old org.dashfoundation.dash keychain layout. A seam method's name must match its behavior (prepare must not spend; broadcast must broadcast).

Code Style

  • Swift: SwiftFormat / SwiftLint (configs in .swiftformat, .swiftlint.yml). Avoid wholesale @objcMembers exposure (enforced by a custom SwiftLint rule).
  • Objective-C: NYTimes Objective-C Style Guide.
  • 4-space indentation, 180-character line limit (100 recommended).

Feature Flags (conditional compilation)

  • DASHPAY — DashPay features (username registration, contacts, invitations, governance voting); built via the dashpay scheme.
  • DASH_TESTNET — defined only in the dashwallet target's Debug + TestNet configurations. The dashpay scheme's Debug build has DEBUG + DASHPAY but not DASH_TESTNET — so anything that must apply to dev builds of both schemes gates on #if (DEBUG || DASH_TESTNET), never DASH_TESTNET alone.

Security

  • Legacy surface: jailbreak detection, hardware encryption, Secure Enclave integration (DashSync-era).
  • Migration posture changes to keep in mind: the wallet mnemonic is plain keychain data (WalletStorage) — device passcode/keychain is the boundary, with no app-side PIN encryption of the seed; ChainLock/InstantSend status is trusted from SwiftDashSDK's context byte (the app no longer does local BLS quorum verification).

Gotchas (read before touching these areas)

Versioning — never hardcode versions

All targets use $(MARKETING_VERSION). Never edit version strings in Info.plist directly — update Marketing Version in Xcode target settings (General tab). When bumping for release, update every target (dashwallet, dashpay, TodayExtension, WatchApp, WatchApp Extension). Verify all entries agree:

# Should print exactly 1
grep -o "MARKETING_VERSION = [^;]*" DashWallet.xcodeproj/project.pbxproj | sort -u | wc -l

DWUpholdMainnetConstants.m gets reformatted on build

A "Run Script - clang-format" build phase adds a stray blank line to this file. If git status shows only-whitespace changes there, restore it before committing:

git restore DashWallet/Sources/Models/Uphold/DWUpholdMainnetConstants.m

CocoaPods deployment targets (post_install)

The Podfile post_install script must set deployment targets per platform — setting IPHONEOS_DEPLOYMENT_TARGET on watchOS targets breaks the build (CocoaPods 1.15.2+ is strict):

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      if target.platform_name == :ios
        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '18.0'
      elsif target.platform_name == :watchos
        config.build_settings['WATCHOS_DEPLOYMENT_TARGET'] = '4.0'
      end
    end
  end
end

Localization file encoding differs by target — check before converting

Encoding is not uniform across the repo, so never assume:

  • DashWallet/*.lproj/Localizable.strings and Localizable.stringsdict (main app) — UTF-8, no BOM. All 43 locales, both file types. Plain grep works. Do not run iconv on these and do not "restore" them to UTF-16 — that would break the plain-text tooling (bartycrouch, plutil, review diffs) the repo relies on.
    grep '"Spend"' DashWallet/de.lproj/Localizable.strings
  • WatchApp/*.lproj/Interface.strings — UTF-16LE with BOM for 40 of 43 locales (en, zh-Hans, zh-Hant-TW are UTF-8). These are the files where grep reports "Binary file matches". They are storyboard-derived, so the keys are ObjectIDs rather than English text:
    iconv -f UTF-16LE -t UTF-8 WatchApp/de.lproj/Interface.strings | grep '.title'

Check first rather than trusting this list — and note file reports pure-ASCII UTF-8 as us-ascii, which is still UTF-8:

head -c 4 DashWallet/de.lproj/Localizable.strings | xxd   # 2f2a 204e -> "/* N" = UTF-8
head -c 4 WatchApp/de.lproj/Interface.strings     | xxd   # fffe ...  = UTF-16LE BOM

Translations sync via Transifex: tx push -s (push source) / tx pull -a (pull all). Let Xcode and BartyCrouch manage the files, and keep each file in whatever encoding it already has.

A Transifex pull can hand back UTF-16 or BOM-prefixed catalogs. ./scripts/convert_strings_to_utf8.sh normalises them back — it is scoped to DashWallet/*.lproj on purpose and is idempotent, so it leaves already-correct files byte-identical. Keep that scope: a bare find . -name '*.strings' would also rewrite the UTF-16LE WatchApp catalogs and anything under Pods/.