This file provides guidance to AI agents like Claude Code (claude.ai/code) when working with code in this repository.
This file is symlinked for cross-agents compatibility to the following paths:
CLAUDE.md
Bitkit iOS is a native Swift implementation of a Bitcoin and Lightning Network wallet. This is a work-in-progress repository that is NOT the live production app. The production app uses React Native and is at github.com/synonymdev/bitkit.
This app integrates with:
- LDK Node (Lightning Development Kit) for Lightning Network functionality
- BitkitCore (Rust-based core library) for Bitcoin operations
- Electrum/Esplora for blockchain data
- Blocktank for Lightning channel services
# Standard build - Open Bitkit.xcodeproj in Xcode and build
# E2E test build (uses local Electrum backend by default)
xcodebuild -workspace Bitkit.xcodeproj/project.xcworkspace \
-scheme Bitkit \
-configuration Debug \
SWIFT_ACTIVE_COMPILATION_CONDITIONS='$(inherited) E2E_BUILD' \
build
# E2E test build with network Electrum and regtest (Info.plist build setting)
E2E_BACKEND=network E2E_NETWORK=regtest \
xcodebuild -workspace Bitkit.xcodeproj/project.xcworkspace \
-scheme Bitkit \
-configuration Debug \
SWIFT_ACTIVE_COMPILATION_CONDITIONS='$(inherited) E2E_BUILD' \
buildFrom Terminal:
# List connected devices
xcrun devicectl list devices
# Set device ID from the list above
DEVICE_ID="<device-identifier-from-list>"
# Build for device
xcodebuild -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug \
-destination 'generic/platform=iOS' -derivedDataPath build build
# Install app on device
xcrun devicectl device install app --device $DEVICE_ID build/Build/Products/Debug-iphoneos/Bitkit.app
# Launch app with console output
xcrun devicectl device process launch --device $DEVICE_ID to.bitkit --consoleFrom Xcode:
- Open
Bitkit.xcodeproj - Select your device from the destination dropdown
- Press Cmd+R to build and run
Note: The project includes a "Remove Static Framework Stubs" build phase that removes empty LDKNodeFFI.framework from the app bundle. This is needed because LDKNodeFFI is a static library (linked at compile time), not a dynamic framework.
# Install SwiftFormat
brew install swiftformat
# Format all Swift code
swiftformat .
# Setup git hooks for automatic formatting on commits
npm install -g git-format-staged
./scripts/setup-hooks.sh# Validate translations (checks for missing translations and validates translation keys)
node scripts/validate-translations.jsNote: Localization files are synced from Transifex using bitkit-transifex-sync.
# Run tests via Xcode Test Navigator or:
# Cmd+U in XcodeThis project follows modern SwiftUI patterns and explicitly AVOIDS traditional MVVM with ViewModels. The architecture uses:
-
@Observable Objects for Business Logic
- Use
@Observable classfor shared business logic instead of ViewModels - Inject via
.environment(businessLogic) - Retrieve with
@Environment(BusinessLogic.self) - Example:
@Observable class UserManager { var users: [User] = []; func loadUsers() async { } }
- Use
-
Native SwiftUI Data Flow
@Statefor local view state only@Bindingfor two-way data flow between parent/child views@Observablefor shared business logic objects- All state mutations must happen on
@MainActor
-
Lifecycle Management
- Use
.taskmodifier for async operations (NOT.onAppear) .taskautomatically cancels when view disappears- Async operations should delegate to
@Observablebusiness logic objects
- Use
-
Component Design
- Decompose views into small, focused, single-purpose components
- Use descriptive names (e.g.,
UserProfileCardnotCard) - Prefer composition over deep view hierarchies
- Components should be independent and reusable with generic data types
┌─────────────────────────────────────────────────┐
│ Views (SwiftUI) │
│ - MainNavView, Activity, Wallet, Settings │
│ - Small, focused components │
└─────────────────┬───────────────────────────────┘
│
┌─────────────────▼───────────────────────────────┐
│ @Observable Business Logic │
│ - AppViewModel, WalletViewModel, etc. │
│ - Injected via .environment() │
└─────────────────┬───────────────────────────────┘
│
┌─────────────────▼───────────────────────────────┐
│ Services │
│ - CoreService (BitkitCore bridge) │
│ - LightningService (LDK Node) │
│ - TransferService, CurrencyService │
└─────────────────┬───────────────────────────────┘
│
┌─────────────────▼───────────────────────────────┐
│ External Dependencies │
│ - BitkitCore (Rust): Bitcoin operations │
│ - LDKNode: Lightning Network operations │
│ - Electrum/Esplora: Blockchain data │
└─────────────────────────────────────────────────┘
App Entry Point:
BitkitApp.swift: Main app entry, handles AppDelegate setup, push notifications, quick actionsAppScene.swift: Root scene coordinator, manages app-wide ViewModels and lifecycleContentView.swift: Root content view
Services Layer:
CoreService: Bridge to BitkitCore (Rust), handles Bitcoin operations and activity storageLightningService: Manages LDK Node lifecycle, Lightning operations, channel managementTransferService: Orchestrates Bitcoin/Lightning transfers (send/receive)TransferStorage: Persists pending transfer stateCurrencyService: Currency conversion and exchange ratesElectrumConfigService,RgsConfigService: Backend configurationServiceQueue: Queue system for background operations (.core,.ldkqueues)
Managers:
SessionManager: User session statePushNotificationManager: Push notification handling for incoming paymentsScannerManager: QR code scanning for paymentsToastWindowManager: App-wide toast notificationsTransferTrackingManager: Tracks pending transfers (new feature)TimedSheets/: Timed sheet management (backup reminders, high balance warnings)SuggestionsManager,TagManager,LanguageManager,NetworkMonitor
ViewModels (Legacy):
While the project is transitioning away from traditional ViewModels, these still exist but should follow @Observable patterns:
AppViewModel: App-wide state (toasts, errors)WalletViewModel: Wallet state, balance, node lifecycleActivityListViewModel: Transaction/payment historyTransferViewModel: Transfer flows (send/receive)NavigationViewModel,SheetViewModel: UI navigation stateBlocktankViewModel: Lightning channel ordering via Blocktank
Key Directories:
Components/: Reusable UI components (buttons, sliders, widgets)Views/: Feature-specific views (Onboarding, Backup, Security, Wallets, Settings, Transfer)Extensions/: Swift extensions for utilities and mock dataUtilities/: Helper utilities (Logger, Keychain, Crypto, Haptics, StateLocker)Models/: Data models (Toast, ElectrumServer, NodeLifecycleState, etc.)Styles/: Fonts and sheet styles
Operations that interact with CoreService or LightningService must use ServiceQueue:
// For BitkitCore operations
try await ServiceQueue.background(.core) {
// Core operations here
}
// For LDK Node operations
try await ServiceQueue.background(.ldk) {
// Lightning operations here
}Node Lifecycle:
The Lightning node has distinct lifecycle states tracked via NodeLifecycleState:
.notStarted→.initializing→.running→.stopped- Error states:
.errorStarting(String)
Transfer Tracking:
New feature (TransferTrackingManager) tracks pending transfers to handle edge cases where transfers are initiated but not completed.
- Use proper Bitcoin/Lightning terminology in code and naming
- All Bitcoin/Lightning operations belong in the service layer, never in views
- The app uses
StateLockerto prevent concurrent Lightning operations (.lightninglock) - Keychain is used for sensitive data (mnemonics, passphrases)
- The app currently runs on regtest only (see
LightningService.swift:92guard) - VSS (Versioned Storage Service) authentication is not yet implemented
- Electrum/Esplora server URLs are configurable via
Env - E2E builds use local Electrum backend via
E2E_BUILDcompilation flag (override withE2E_BACKEND/E2E_NETWORKbuild settings)
- Use
do-catchblocks for async operations - Provide user feedback via toasts:
app.toast(type: .error, title: "...", description: "...") - Handle loading, error, and empty states comprehensively
- Consider using
enum LoadingState<T> { case idle, loading, loaded(T), error(Error) }
- Minimum deployment target is iOS 17.0.
- Xcode previews work with the minimum target (iOS 17); they may not work on iOS 18+ due to Rust dependencies.
- Use availability checks only for iOS 18+ features:
if #available(iOS 18.0, *) { // Use iOS 18+ features } else { // Fallback }
- Avoid expensive operations in view body
- Move heavy computations to
@Observableobjects - Use proper state granularity to minimize view updates
- Use
@ViewBuilderfor repetitive view code
Ensure accessibility modifiers and labels are added to custom components.
- SwiftFormat configuration in
.swiftformat - Max line width: 150 characters
- Swift version: 5.10
- Use descriptive names:
isLoadingUsersnotloading - Follow Apple's SwiftUI best practices
- ALWAYS add exactly ONE entry per PR under
## [Unreleased]inCHANGELOG.mdforfeat:andfix:PRs; skip forchore:,ci:,refactor:,test:,docs:unless the change is user-facing - NEVER add multiple changelog lines for the same PR — summarize all changes in a single concise entry
- USE standard Keep a Changelog categories:
### Added,### Changed,### Deprecated,### Removed,### Fixed,### Security - ALWAYS append
#PR_NUMBERat the end of each changelog entry when the PR number is known - ALWAYS place new entries at the top of their category section (newest first)
- NEVER modify released version sections — only edit
## [Unreleased] - ALWAYS create category headings on demand (don't add empty stubs)
- Identify if business logic should live in an
@Observableobject or existing ViewModel - Create UI components in
Components/or feature-specific views inViews/ - Wire up via
.environment()injection inAppScene.swift - Use
.taskfor async initialization - Add error handling and user feedback (toasts)
- All Lightning operations go through
LightningService.shared - Lock the Lightning state with
StateLocker.lock(.lightning)for critical operations - Listen to LDK events via
wallet.addOnEvent(id:)pattern - Sync activity list after Lightning events
- Use
CoreServicefor Bitcoin operations - Activity tracking handles both on-chain and Lightning payments
- RBF (Replace-By-Fee) is tracked via
ActivityService.replacementTransactions
- Update translation keys in code
- Run
node scripts/validate-translations.jsto check for issues - Sync with Transifex using
bitkit-transifex-synctool