6.3 KiB
CLAUDE.md
Project Overview
Unstoppable Wallet - A multi-currency crypto wallet Android app supporting Bitcoin, Ethereum, and many other blockchains.
- Package:
io.horizontalsystems.bankwallet - Min SDK: See
build.gradleformin_sdk_version - Language: Kotlin
- UI: Jetpack Compose (migrating from XML, see
ui/vsuiv3/)
Build Commands
# Debug build
./gradlew assembleDebug
# Release build
./gradlew assembleRelease
# Run tests
./gradlew test
# Lint
./gradlew lint
Project Structure
app/src/main/java/io/horizontalsystems/bankwallet/
├── core/ # Core infrastructure
│ ├── managers/ # Business logic managers (singletons via App)
│ ├── adapters/ # Blockchain adapters
│ ├── factories/ # Factory classes
│ └── providers/ # Data providers
├── entities/ # Data models
├── modules/ # Feature modules (MVVM)
│ ├── balance/ # Wallet balance
│ ├── send/ # Send transactions
│ ├── multiswap/ # Token swaps
│ └── ...
├── ui/ # Legacy Compose components
└── uiv3/ # New Compose components (prefer this)
subscriptions-core/ # Subscription/paid features (IPaidAction)
subscriptions-google-play/ # Google Play billing
subscriptions-fdroid/ # F-Droid flavor
Architecture Patterns
ViewModel Pattern
class MyViewModel(...) : ViewModelUiState<MyUiState>() {
override fun createState() = MyUiState(...)
// Call emitState() after state changes
}
Service Pattern (for business logic)
class MyService(...) : ServiceState<MyServiceState>() {
override fun createState() = MyServiceState(...)
// Call emitState() after state changes
}
Clearable Interface
Implement Clearable for classes that need cleanup:
class MyService : Clearable {
override fun clear() {
coroutineScope.cancel()
}
}
Dependency Injection
Uses manual DI via App companion object:
// Declaration
companion object {
lateinit var myManager: MyManager
}
// Initialization in App.onCreate()
myManager = MyManager(dependency1, dependency2)
// Usage
App.myManager.doSomething()
Reactive Patterns
- Preferred: Kotlin Coroutines +
StateFlow/SharedFlow - Deprecated: RxJava (legacy code only, do not use in new code)
// Preferred pattern - use StateFlow
class MyRepository {
private val _itemsFlow = MutableStateFlow<List<Item>>(emptyList())
val itemsFlow: StateFlow<List<Item>> = _itemsFlow.asStateFlow()
}
// In ViewModel
viewModelScope.launch {
repository.itemsFlow.collect { items ->
// handle
}
}
Migration Note
Existing RxJava code should be migrated to Coroutines/Flow when modified. Replace:
Observable→Flow/StateFlowBehaviorSubject→MutableStateFlowPublishSubject→MutableStateFlowSingle→suspend funCompletable→suspend funreturningUnit
Storage
SharedPreferences via LocalStorageManager
// Add to ILocalStorage interface
var myPreference: Boolean
// Implement in LocalStorageManager
override var myPreference: Boolean
get() = preferences.getBoolean(KEY, defaultValue)
set(value) = preferences.edit().putBoolean(KEY, value).apply()
Room Database
- Database:
AppDatabase - DAOs in
appDatabase
Blockchain Support
Each blockchain has:
- Adapter:
ISendXxxAdapterincore/adapters/ - Kit: Separate module (e.g.,
ethereumkit,bitcoinkit) - SendTransactionService:
modules/multiswap/sendtransaction/
Supported chains: EVM (Ethereum, BSC, etc.), Bitcoin, Solana, Tron, TON, Zcash, Monero, Stellar
Subscription/Paid Features
// Check if action is allowed
UserSubscriptionManager.isActionAllowed(SwapProtection)
// Paid actions defined in IPaidAction
object SwapProtection : IPaidAction
object SecureSend : IPaidAction
// etc.
Conventions
- Naming: PascalCase for classes, camelCase for functions/variables
- State classes: Suffix with
StateorUiState - ViewModels: Suffix with
ViewModel - Services: Suffix with
Service - Repositories: Suffix with
Repository - Compose: New components go in
uiv3/components/
Preventing Double Clicks in Compose
Use local composable state (not ViewModel) to disable buttons during async operations. This prevents double-clicks because local state updates are synchronous, while ViewModel state requires an async round-trip.
// Correct: Local state — immediate, no race condition
var buttonEnabled by remember { mutableStateOf(true) }
ButtonPrimaryYellow(
enabled = buttonEnabled,
onClick = {
buttonEnabled = false // Immediate, same frame
coroutineScope.launch {
try {
viewModel.doAction()
} finally {
buttonEnabled = true
}
}
}
)
// Wrong: ViewModel state — race condition possible
// User can click twice before recomposition disables button
ButtonPrimaryYellow(
enabled = uiState.buttonEnabled, // Async update
onClick = { viewModel.doAction() }
)
Common Gotchas
- RxJava to Flow (legacy): If Observable completes,
.asFlow()stops collecting silently — migrate to pure Flow - Coroutine exceptions: Uncaught exceptions in
launchblocks terminate silently — wrap in try-catch or usecatchoperator - Mutex in Services: Use
mutex.withLockfor thread-safe state updates - ViewModel cleanup: Call
service.clear()inonCleared() - Flow collection: Always collect in a coroutine scope that matches the lifecycle (e.g.,
viewModelScope)
Testing
- Unit tests in
app/src/test/ - Instrumentation tests in
app/src/androidTest/
Git Commit Messages
Use plain imperative mood, no type prefixes (feat:, chore:, etc.):
Update provider data ✓
Fix apostrophe in strings.xml ✓
chore: update provider data ✗
The message should complete: "If I add this commit, it will [message]."
Summary line is 60 characters max. Single-line for small changes; add a blank line + body for context when the why is non-obvious.