This file provides guidance to AI agents like Cursor/Claude Code/Codex/WARP when working with code in this repository.
# compile
./gradlew compileDevDebugKotlin
# Build for dev
./gradlew assembleDevDebug
# Run unit tests
./gradlew testDevDebugUnitTest
# Run specific unit test file
./gradlew testDevDebugUnitTest --tests LightningRepoTest
# Run instrumented tests
./gradlew connectedDevDebugAndroidTest
# Build for E2E tests (UI hooks enabled, local Electrum by default)
E2E=true ./gradlew assembleDevRelease
# Build for E2E tests with geoblocking disabled
GEO=false E2E=true ./gradlew assembleDevRelease
# Build for E2E tests using network Electrum (not local; staging/mainnet based on flavor)
E2E=true E2E_BACKEND=network ./gradlew assembleTnetRelease
# Lint using detekt
./gradlew detekt
# Auto-format using detekt
./gradlew detekt --auto-correct
# Update detekt baseline
./gradlew detektBaseline
# Install dev build
./gradlew installDevDebug
# Clean build artifacts
./gradlew clean- Language: Kotlin
- UI Framework: Jetpack Compose with Material3
- Architecture: MVVM with Hilt dependency injection
- Database: Room
- Networking: Ktor
- Bitcoin/Lightning: LDK Node, bitkitcore library
- State Management: StateFlow, SharedFlow
- Navigation: Compose Navigation with strongly typed routes
- Push Notifications: Firebase
- Storage: DataStore with json files
- app/src/main/java/to/bitkit/
- App.kt: Application class with Hilt setup
- ui/: All UI components
- MainActivity.kt: Single activity hosting all screens
- screens/: Feature-specific screens organized by domain
- components/: Reusable UI components
- theme/: Material3 theme configuration
- viewmodels/: Shared ViewModels for business logic
- repositories/: Data access layer
- services/: Core services (Lightning, Currency, etc.)
- data/: Data layer: database, DTOs, and data stores
- di/: Dependency Injection: Hilt modules
- models/: Domain models
- ext/: Kotlin extensions
- utils/: Utility functions
- usecases/: Domain layer: use cases
- Single Activity Architecture: MainActivity hosts all screens via Compose Navigation
- Repository Pattern: Repositories abstract data sources from ViewModels
- Service Layer: Core business logic in services (LightningService, WalletService)
- Reactive State Management: ViewModels expose UI state via StateFlow
- Coroutine-based Async: All async operations use Kotlin coroutines
- dev: Regtest network for development
- tnet: Testnet network
- mainnet: Production
GlobalScope.launch { } // Use viewModelScope
val result = nullable!!.doSomething() // Use safe calls
Text("Send Payment") // Use string resources
class Service(@Inject val vm: ViewModel) // Never inject VMs
suspend fun getData() = runBlocking { } // Use withContextviewModelScope.launch { }
val result = nullable?.doSomething() ?: default
Text(stringResource(R.string.send_payment))
class Service {
fun process(data: Data)
}
suspend fun getData() = withContext(Dispatchers.IO) { }- Main Activity:
app/src/main/java/to/bitkit/ui/MainActivity.kt - Navigation:
app/src/main/java/to/bitkit/ui/ContentView.kt - Lightning Service:
app/src/main/java/to/bitkit/services/LightningService.kt - App ViewModel:
app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt - Wallet ViewModel:
app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt
private val _uiState = MutableStateFlow(InitialState)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun updateState(action: Action) {
viewModelScope.launch {
_uiState.update { it.copy(/* fields */) }
}
}suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
runCatching {
apiService.fetchData()
}.onFailure {
Logger.error("Failed", it, context = TAG)
}
}- USE coding rules from
.cursor/default.rules.mdc - ALWAYS run
./gradlew compileDevDebugKotlinafter code changes to verify code compiles - ALWAYS run
./gradlew testDevDebugUnitTestafter code changes to verify tests succeed and fix accordingly - ALWAYS run
./gradlew detektafter code changes to check for new lint issues and fix accordingly - ALWAYS ask clarifying questions to ensure an optimal plan when encountering functional or technical uncertainties in requests
- ALWAYS when fixing lint or test failures prefer to do the minimal amount of changes to fix the issues
- USE single-line commit messages under 50 chars; use conventional commit messages template format:
feat: add something new - USE
git diff HEAD sourceFilePathto diff an uncommitted file against the last commit - NEVER capitalize words in commit messages
- ALWAYS run
git statusto check ALL uncommitted changes after completing any code edits, then reply with 3 commit message suggestions covering the ENTIRE uncommitted diff - ALWAYS check existing code patterns before implementing new features
- USE existing extensions and utilities rather than creating new ones
- ALWAYS consider applying YAGNI (You Ain't Gonna Need It) principle for new code
- ALWAYS reuse existing constants
- ALWAYS ensure a method exist before calling it
- ALWAYS remove unused code after refactors
- ALWAYS follow Material3 design guidelines for UI components
- ALWAYS ensure proper error handling in coroutines
- ALWAYS acknowledge datastore async operations run synchronously in a suspend context
- NEVER use
runBlockingin suspend functions - ALWAYS pass the TAG as context to
Loggercalls, e.g.Logger.debug("message", context = TAG) - NEVER add
e =named parameter to Logger calls - NEVER manually append the
Throwable's message or any other props to the string passed as the 1st param ofLogger.*calls, its internals are already enriching the final log message with the details of theThrowablepassed via theearg - ALWAYS log errors at the final handling layer where the error is acted upon, not in intermediate layers that just propagate it
- ALWAYS use the Result API instead of try-catch
- NEVER wrap methods returning
Result<T>in try-catch - PREFER to use
itinstead of explicit named parameters in lambdas e.g.fn().onSuccess { log(it) }.onFailure { log(it) } - NEVER inject ViewModels as dependencies - Only android activities and composable functions can use viewmodels
- NEVER hardcode strings and always preserve string resources
- ALWAYS localize in ViewModels using injected
@ApplicationContext, e.g.context.getString() - ALWAYS use
rememberfor expensive Compose computations - ALWAYS add modifiers to the last place in the argument list when calling composable functions
- NEVER add parameters with default values BEFORE the
modifierparameter in composable functions - modifier must be the FIRST optional parameter - ALWAYS prefer
VerticalSpacer,HorizontalSpacer,FillHeightandFillWidthoverSpacerwhen applicable - PREFER declaring small dependant classes, constants, interfaces or top-level functions in the same file with the core class where these are used
- ALWAYS create data classes for state AFTER viewModel class in same file
- ALWAYS return early where applicable, PREFER guard-like
ifconditions likeif (condition) return - ALWAYS write the documentation for new features as markdown files in
docs/ - NEVER write code in the documentation files
- NEVER add code comments to private functions, classes, etc
- ALWAYS use
_uiState.update { }, NEVER use_stateFlow.value = - ALWAYS add the warranted changes in unit tests to keep the unit tests succeeding
- ALWAYS follow the patterns of the existing code in
app/src/testwhen writing new unit tests - ALWAYS be mindful of thread safety when working with mutable lists & state
- ALWAYS split screen composables into parent accepting viewmodel + inner private child accepting state and callbacks
Content() - ALWAYS name lambda parameters in a composable function using present tense, NEVER use past tense
- NEVER use
wheneverBlockingin unit test expression body functions wrapped in a= test {}lambda - ALWAYS wrap unit tests
setUpmethods mocking suspending calls withrunBlocking, e.gsetUp() = runBlocking {} - ALWAYS add business logic to Repository layer via methods returning
Result<T>and use it in ViewModels - ALWAYS order upstream architectural data flow this way:
UI -> ViewModel -> Repository -> RUSTand vice-versa for downstream - ALWAYS add new localizable string string resources in alphabetical order in
strings.xml - NEVER add string resources for strings used only in dev settings screens and previews and never localize acronyms
- ALWAYS use template in
.github/pull_request_template.mdfor PR descriptions - ALWAYS wrap
ULongnumbers withUSatin arithmetic operations, to guard against overflows - PREFER to use one-liners with
run {}when applicable, e.g.override fun someCall(value: String) = run { this.value = value } - ALWAYS add imports instead of inline fully-qualified names
- PREFER to place
@Suppress()annotations at the narrowest possible scope - ALWAYS wrap suspend functions in
withContext(bgDispatcher)if in domain layer, using ctor injected prop@BgDispatcher private val bgDispatcher: CoroutineDispatcher - ALWAYS position companion object at the top of the class
- App IDs per flavor:
to.bitkit.dev(dev/regtest),to.bitkit.tnet(testnet),to.bitkit(mainnet) - ALWAYS use
adb shell "run-as to.bitkit.dev ..."to access the app's private data directory (debug builds only) - App files root:
files/(relative, insiderun-ascontext) - Key paths:
files/logs/— app log files (e.g.bitkit_2026-02-09_21-04-16.log)files/bitcoin/wallet0/ldk/— LDK node storage (graph cache, dumps)files/bitcoin/wallet0/core/— bitkit-core storagefiles/datastore/— DataStore preferences and JSON stores
- To read a file:
adb shell "run-as to.bitkit.dev cat files/logs/bitkit_YYYY-MM-DD_HH-MM-SS.log" - To list files:
adb shell "run-as to.bitkit.dev ls -la files/logs/" - To find files:
adb shell "run-as to.bitkit.dev find files/ -name '*.log' -o -name '*.txt'" - ALWAYS download device files to
.ai/{name}_{timestamp}/when needed for debugging (e.g..ai/logs_1770671066/) - To download:
adb shell "run-as to.bitkit.dev cat files/path/to/file" > .ai/folder_timestamp/filename - ALWAYS try reading device logs automatically via adb BEFORE asking user to provide log files
- Use
LightningNodeServiceto manage background notifications while the node is running - Use
LightningServiceto wrap node's RUST APIs and manage the inner lifecycle of the node - Use
LightningRepoto defining the business logic for the node operations, usually delegating toLightningService - Use
WakeNodeWorkerto manage the handling of remote notifications received via cloud messages - Use
*Servicesto wrap rust library code exposed via bindings - Use CQRS pattern of Command + Handler like it's done in the
NotifyPaymentReceived+NotifyPaymentReceivedHandlersetup