Files

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.gradle for min_sdk_version
  • Language: Kotlin
  • UI: Jetpack Compose (migrating from XML, see ui/ vs uiv3/)

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 / StateFlow
  • BehaviorSubject → MutableStateFlow
  • PublishSubject → MutableStateFlow
  • Single → suspend fun
  • Completable → suspend fun returning Unit

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: ISendXxxAdapter in core/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 State or UiState
  • 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

  1. RxJava to Flow (legacy): If Observable completes, .asFlow() stops collecting silently — migrate to pure Flow
  2. Coroutine exceptions: Uncaught exceptions in launch blocks terminate silently — wrap in try-catch or use catch operator
  3. Mutex in Services: Use mutex.withLock for thread-safe state updates
  4. ViewModel cleanup: Call service.clear() in onCleared()
  5. 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.