This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
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:
DashWalletTestsruns against thedashpayhost app through thedashwallet-dashpayscheme, in the Debug configuration (the only one withENABLE_TESTABILITY, which every@testable import dashpayneeds). Cmd+U in Xcode, or:Every test file must be in the target: files underxcodebuild test -workspace DashWallet.xcworkspace -scheme dashwallet-dashpay -configuration Debug \ -destination 'platform=iOS Simulator,name=iPhone 17' ARCHS=arm64 ONLY_ACTIVE_ARCH=YES
DashWalletTests/Notifications/are picked up automatically (synchronized folder), everything else needs the fourproject.pbxprojentries.scripts/ci/check_test_target_membership.pyfails CI on a forgotten file. The host app needs the gitignored*-Info.plistfiles inDashWallet/; without the real ones,scripts/ci/make-placeholder-plists.shwrites inert placeholders. CI runs the suite on every PR (.github/workflows/unit-tests.yml) against the platform commit pinned in.platform-ref.
- 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)
swiftformat,swiftlint,clang-format(Objective-C),bartycrouch(localization)
../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 simIf the app fails with missing SDK symbols (e.g. "no member …"), verify the
../platform checkout and rebuild both XCFramework slices.
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).
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
WalletSendService.swift— the send boundary: auth → build+sign (SwiftDashSDKTransactionSender) → user confirm → broadcast.SendCoinsService.swiftis a thin programmatic wrapper over it.SwiftDashSDKHost.swift/SwiftDashSDKWalletRuntime.swift/SwiftDashSDKWalletState.swift/SwiftDashSDKSPVCoordinator.swift— SDK lifecycle ownership, published wallet state (balance), and chain syncCurrencyExchanger.swift— fiat currency conversionDatabaseConnection.swift+Migrations.bundle/— SQLite (schema changes require a new timestamped migration)MainTabbarController.swift— tab-based navigationUIHostingController+DashWallet.swift— UIKit ↔ SwiftUI bridge
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 liveDS*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 iswaitForEventsat progress ≈ 1.0 (.syncedis a transient window). Gate onSyncingActivityMonitor(.syncDone). - Sends: every spend goes through
WalletSendService(standard) orSwiftDashSDKTransactionSender(selected-input / sweep). Never callCoreTransactionBuilderfrom 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 throughSwiftDashSDKHost; deletion routes throughSwiftDashSDKWalletWiper. The migrator supports multiple wallets and never deletes DashSync-ownedorg.dashfoundation.dashentries. Don't add ad-hocWalletStorage()readers or choose the first mnemonic — resolve the active wallet throughSwiftDashSDKHost/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-devat 5.0.0-rc.4 (5b41ca376b, what testnet runs) or a later commit with the samev13.rs/v14.rs, or a later line whose protocol 14 is the one testnet runs (v5.1-devatd4750290ffhas the samev13.rs/v14.rsas 5.0.0-rc.4), or an explicitly selected release SHA/tag. Notv4.2-dev: its pre-release protocol 14 differs, and the testnet pin inSwiftDashSDKHost.platformVersion(for:)builds transitions from the linked SDK's v14. - Mainnet is the fresh-install default:
WalletEnvironment.networkKindreturns.mainnetwhenCURRENT_CHAIN_TYPE_KEYis 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.
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
ObservableObjectViewModels (@MainActor,@Publishedstate). - Concretely banned inside SwiftUI
Viewstructs: FFI/SDK calls, fee math, direct auth calls (useAuthenticationGate),DSChain→network mapping, protocol constants. Those live in the ViewModel or a service. - When integrating with existing UIKit navigation, use a thin
UIHostingControllerwrapper only. - Maintain existing UIKit code but don't extend it; migrate a screen to SwiftUI when substantially reworking it.
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 screenaccessibilityIdentifier is an XCUITest id and is never spoken — it does not
count as a label. .github/workflows/accessibility.yml enforces this on every PR.
Distilled from the 2026-07 branch review (ARCH_REVIEW_2026-07-03.md) — each rule exists because we shipped its violation:
- Never copy-then-adapt. If a helper you need is
private/fileprivatein another file (auth gate, main-thread trampoline, SwiftData lookup), promote it tointernaland 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;ScriptAddressCodecfor address↔script. - 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.) - 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/.unknowncase) — never fabricate plausible defaults (.classic,.ok,[],false). - New singletons need a reason. Don't add another
static let sharedinSources/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. - 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. - 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. UseTODO(label)and link docs instead. - Legacy compatibility stays bounded and honest. Do not restore staged DashSync/SDK paths. Only the frozen key migrator may read the old
org.dashfoundation.dashkeychain layout. A seam method's name must match its behavior (preparemust not spend;broadcastmust broadcast).
- Swift: SwiftFormat / SwiftLint (configs in
.swiftformat,.swiftlint.yml). Avoid wholesale@objcMembersexposure (enforced by a custom SwiftLint rule). - Objective-C: NYTimes Objective-C Style Guide.
- 4-space indentation, 180-character line limit (100 recommended).
DASHPAY— DashPay features (username registration, contacts, invitations, governance voting); built via thedashpayscheme.DASH_TESTNET— defined only in the dashwallet target's Debug + TestNet configurations. The dashpay scheme's Debug build hasDEBUG+DASHPAYbut notDASH_TESTNET— so anything that must apply to dev builds of both schemes gates on#if (DEBUG || DASH_TESTNET), neverDASH_TESTNETalone.
- 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).
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 -lA "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.mThe 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
endEncoding is not uniform across the repo, so never assume:
DashWallet/*.lproj/Localizable.stringsandLocalizable.stringsdict(main app) — UTF-8, no BOM. All 43 locales, both file types. Plaingrepworks. Do not runiconvon 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.stringsWatchApp/*.lproj/Interface.strings— UTF-16LE with BOM for 40 of 43 locales (en,zh-Hans,zh-Hant-TWare UTF-8). These are the files wheregrepreports "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 BOMTranslations 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/.