Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a692cbf
feat: use JetPack Paging 3 for the Home Screen and Transaction Group …
HashEngineering Feb 26, 2026
baf3ae6
feat: some improvements of parallelization
HashEngineering Feb 26, 2026
cdc18c9
feat: fix reset
HashEngineering Feb 26, 2026
fa2252c
fix: more improvements
HashEngineering Feb 26, 2026
aceb715
fix: more improvements
HashEngineering Feb 26, 2026
520d9be
fix: add tx display cache
HashEngineering Mar 12, 2026
ffde574
fix: fix race conditions and other bugs
HashEngineering Mar 13, 2026
bc97941
fix: improvements
HashEngineering Mar 13, 2026
2ea8431
fix: load wallet in separate thread
HashEngineering Mar 13, 2026
9815e57
fix: remove async wallet loading
HashEngineering Mar 13, 2026
5a8be07
fix: improve loading speed of home screen
HashEngineering Mar 14, 2026
df3d2e6
fix: add WalletLoadTest
HashEngineering Mar 14, 2026
3bda815
fix: add exchange rates to the cache
HashEngineering Mar 14, 2026
5432aec
chore: use dashj 22.0.1-SNAPSHOT
HashEngineering Mar 16, 2026
68ea52c
fix: store the contact request data in the tx cache
HashEngineering Mar 16, 2026
59fad6f
fix: collapse database versions
HashEngineering Mar 16, 2026
1ce55ad
fix: add tx_group_cache for the groups
HashEngineering Mar 16, 2026
fb795c6
docs: add async wallet loading proposal
HashEngineering Mar 17, 2026
a42307a
fix: support the home screen filter
HashEngineering Mar 17, 2026
515dd81
fix: simplify filter changes
HashEngineering Mar 17, 2026
5fa7b76
fix: simplify handling of groups
HashEngineering Mar 17, 2026
10a992f
fix: add a means to refresh the tx cache
HashEngineering Mar 17, 2026
0bc9093
fix: speed up display
HashEngineering Mar 17, 2026
5d8225a
fix: handle TX memos
HashEngineering Mar 17, 2026
a05942f
fix: add userId for contact payments
HashEngineering Mar 17, 2026
a5d6cf3
fix: zero out the balance with a wallet reset
HashEngineering Mar 17, 2026
963927b
fix: squash database versions 17 to 21
HashEngineering Mar 17, 2026
464fd1e
fix: resolve several issues
HashEngineering Mar 17, 2026
ea29cf1
fix: add a function to force rebuilding the cache
HashEngineering Mar 17, 2026
926157e
fix: fix a missing tx problem based on filters
HashEngineering Mar 17, 2026
0ba1fa6
fix: exclude coinjoin tx from Sent/Received filters
HashEngineering Mar 17, 2026
eb30810
fix: fix race condition, remove unnecessary code
HashEngineering Mar 17, 2026
4f9d881
fix: fix scroll observer
HashEngineering Mar 18, 2026
20b4e08
fix: catch, log or prevent exceptions
HashEngineering Mar 18, 2026
acda65c
fix: various other issues, catch exceptions
HashEngineering Mar 18, 2026
853b34d
fix: resolve contacts after each block of transactions is added
HashEngineering Mar 18, 2026
3278855
refactor: resolve contacts after each block of transactions is added
HashEngineering Mar 18, 2026
ad1726a
refactor: move the tx display cache code to a singleton service
HashEngineering Mar 18, 2026
159f819
fix: contact related race condition
HashEngineering Mar 18, 2026
8cd06fd
fix: improvements
HashEngineering Mar 18, 2026
7f25554
fix: improvements and fixes, cleanup
HashEngineering Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ buildscript {
kotlin_version = '2.1.0'
coroutinesVersion = '1.6.4'
ok_http_version = '4.12.0'
dashjVersion = '22.0.0'
dashjVersion = '22.0.1-SNAPSHOT'
dppVersion = "2.0.3"
hiltVersion = '2.53'
hiltCompilerVersion = '1.2.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ interface WalletDataProvider {
@Deprecated("The wallet is in here temporary and will be moved to a separate holder, limited to the the wallet module. In feature modules, use transactionBag instead.")
val wallet: Wallet?

fun observeWallet(): Flow<Wallet?>

val transactionBag: TransactionBag

val networkParameters: NetworkParameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ data class RateRetrievalState(
val volatile: Boolean
) {
val isStale = lastAttemptFailed || staleRates || volatile

companion object {
val DEFAULT = RateRetrievalState(lastAttemptFailed = false, staleRates = false, volatile = false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import android.content.res.Resources
import androidx.annotation.StringRes

data class ResourceString(
@StringRes val resourceId: Int,
val args: List<Any> = listOf()
@StringRes val resourceId: Int = 0,
val args: List<Any> = listOf(),
val resolvedText: String? = null
) {
fun format(resources: Resources): String {
if (resolvedText != null) return resolvedText
if (resourceId == 0) return ""
return resources.getString(resourceId, *args.toTypedArray())
}
}
97 changes: 97 additions & 0 deletions docs/proposals/async-wallet-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Proposal: Async Wallet Loading for Faster Home Screen Display

## Problem

`WalletProtobufSerializer.readWallet()` takes approximately 5–6 seconds on first launch
because it deserializes all transactions from disk. This call happens synchronously inside
`WalletApplication.onCreate()` via `fullInitialization()`, which blocks the main thread and
prevents any Activity from starting until the wallet is fully loaded.

As a result, even though the app has a display cache (`TxDisplayCacheEntry`) that can render
cached transaction rows in ~100 ms, users still see a blank screen for 5–7 seconds before
`MainActivity` starts and `MainViewModel` can show the cached data.

Profiling breakdown (measured on a testnet device with ~200 transactions):

| Step | Duration |
|------|----------|
| `WalletProtobufSerializer.readWallet()` | ~5,300 ms |
| `SecurityGuard` fallback setup | ~168 ms |
| `initDash()` (dashj system init) | ~12 ms |
| Integration inits (Uphold, Coinbase, etc.) | ~465 ms |
| **Total `fullInitialization()`** | **~7,000 ms** |

## Proposed Solution

Move `loadWalletFromProtobuf()` (which includes `finalizeInitialization()`) to a background
thread so that Activities can start immediately and show cached transaction data while the
wallet loads in parallel.

### Architecture

1. **`WalletApplication.onCreate()`** calls `initEnvironment()` synchronously (~36 ms — sets
up StrictMode, bitcoinj Context, etc.) then starts a `"wallet-init"` background thread for
`loadWalletFromProtobuf()`.

2. **`WalletReadyFlow`** — a new Kotlin `object` backed by `MutableStateFlow<Boolean>`.
Exposes `observe(): Flow<Unit>` that emits once when the wallet finishes loading, and
`setReady()` called at the end of the background thread.

3. **`WalletDataProvider.observeWalletReady(): Flow<Unit>`** — new interface method with a
default implementation of `flowOf(Unit)` (emits immediately; suitable for tests).
`WalletApplication` overrides it to delegate to `WalletReadyFlow`.

4. **`MainViewModel`** — uses `combine(_transactionsDirection, walletData.observeWalletReady())`
instead of just `_transactionsDirection.flatMapLatest` so that `rebuildWrappedList()` waits
for the wallet before trying to access `walletData.wallet!!`.

5. **`LockScreenActivity.onCreate()`** — when `walletData.wallet == null`, initializes
`binding`, shows `displayedChild = 1` (main content) immediately, and defers
`initView()` / `initViewModel()` / `applyLockScreenState()` to a `lifecycleScope` coroutine
that collects from `observeWalletReady()`. The original `finish()` call is replaced so
`MainActivity` can start and show cached transactions.

6. **`OnboardingActivity`** — the Android `LAUNCHER` Activity. When `walletFileExists()` but
`wallet == null` (still loading), calls `regularFlow()` immediately instead of waiting, so
`MainActivity` starts right away.

7. **`MainActivity.onCreate()`** — `upgradeWalletKeyChains()` and `upgradeWalletCoinJoin()`
(which call `walletData.wallet!!`) are deferred inside `lifecycleScope.launch { observeWalletReady().collect { ... } }`.

### Expected Result

- `MainActivity` starts in ~200–500 ms (after `initEnvironment()` and Activity inflation).
- Display-cache rows appear at ~1–2 s (after `MainViewModel` reads `TxDisplayCacheEntry` from Room).
- Live wallet data replaces cached rows at ~6–8 s (after background thread finishes).
- Lock screen (PIN/biometrics) is applied once the wallet is ready — acceptable since the
auto-logout timer is typically ≥1 minute and the wallet cannot be spent while loading.

## Why It Was Reverted

This approach was implemented and then reverted because it requires touching every code path
that assumes `walletData.wallet` is non-null at the time of first use. The app has many such
call sites across ViewModels, Activities, and Fragments. Introducing null-safety for the wallet
globally is a larger refactor than the scope of this fix warranted.

The two most critical blockers encountered during the experiment:

1. **`CheckPinViewModel`** accesses `walletData.wallet!!` at construction time via `by
viewModels<>()` in `LockScreenActivity`. Deferring `initViewModel()` to
`observeWalletReady()` sidesteps this, but creates subtle ordering risks.

2. **Many other ViewModels and call sites** use `walletData.wallet!!` without null checks and
would crash during the window between app start and wallet ready (~5–7 s). A full audit and
defensive null-check pass would be required before this approach is safe.

## Prerequisites for Revisiting

Before attempting this optimization again, the following work should be completed:

- [ ] Audit all `walletData.wallet!!` call sites; replace with `walletData.wallet ?:` guards
or restructure to be triggered from `observeWalletReady()`.
- [ ] Move wallet-access logic out of ViewModel constructors and into `init {}` blocks that
wait for `observeWalletReady()`.
- [ ] Add integration tests that cover the "wallet null at startup" window to prevent
regressions.
- [ ] Consider splitting `WalletDataProvider` into a "wallet-ready" interface that is only
available after initialization, and a "pre-wallet" interface for display-cache reads.
Loading