Skip to content

fix: improve shutdown of blockchainservice#1482

Open
HashEngineering wants to merge 18 commits into
masterfrom
fix/shutdown-blockchainservice
Open

fix: improve shutdown of blockchainservice#1482
HashEngineering wants to merge 18 commits into
masterfrom
fix/shutdown-blockchainservice

Conversation

@HashEngineering
Copy link
Copy Markdown
Collaborator

@HashEngineering HashEngineering commented Apr 29, 2026

Issue being fixed or feature implemented

Related PR's and Dependencies

Screenshots / Videos

How Has This Been Tested?

  • QA (Mobile Team)

Checklist:

  • I have performed a self-review of my own code and added comments where necessary
  • I have added or updated relevant unit/integration/functional/e2e tests

Summary by CodeRabbit

  • Bug Fixes

    • Fixed wallet state notifications during device wipe to ensure explicit cleared signals.
    • Prevented transaction cache inconsistencies after blockchain sync by triggering rebuilds when needed.
    • Added scrolling support to dialogs that may overflow the display area.
    • Improved GitHub link handling to use a safer URI conversion.
    • Made dialog collapse button compatible with AppCompatImageButton.
  • Improvements

    • Enhanced service shutdown sequence for more orderly app closure.
    • Improved coin-mixing shutdown coordination to await in-flight work.
    • Added total transaction count tracking to aid cache validation.

@HashEngineering HashEngineering self-assigned this Apr 29, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

Adds transaction-cache completeness checks with an automatic rebuild trigger, introduces a CoinJoin shutdown coordination hook awaited during service teardown, updates wallet wipe signaling, and applies small UI and dialog handling tweaks.

Changes

Transaction Cache Completeness Verification

Layer / File(s) Summary
Data/DAO
wallet/src/de/schildbach/wallet/database/dao/TxGroupCacheDao.kt
Adds getTotalTxCount() returning COUNT(*) from tx_group_cache.
State / Observer
wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt
Adds wasReplaying flag; observes blockchain replay→non-replay transitions and invokes completeness check on first non-replaying observation.
Completeness Check
wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt
Adds rebuildIfCacheIncomplete() that waits for walletData.wallet, computes wallet tx count, group-cache total tx count and group count, and display-cache row count; treats all-empty as complete and calls forceRebuildTransactionCache() when counts mismatch.
Incremental Update Metrics
wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt
updateWrappedListForTransactions() now measures tx_display_cache row count before/after insertAll(...) and logs changes.

Graceful Shutdown Coordination (CoinJoin + Networking)

Layer / File(s) Summary
Service API
wallet/src/de/schildbach/wallet/service/CoinJoinService.kt
Adds prepareForShutdown() to CoinJoinService API and implements it in CoinJoinMixingService: sets a shutdown AtomicBoolean, stores/times refreshKeysJob running wallet.coinJoin.refreshUnusedKeys, prevents new refresh during shutdown, and awaits active job (up to 10s).
Shutdown Sequencing
wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt
onDestroy() now calls coinJoinService.prepareForShutdown() before shutting down peers; replaces peerGroup!!.forceStop(7_000) with peerGroup!!.stop(), adds logs and Stopwatch timing, and updates network status transitions.

Miscellaneous UI / App State

Layer / File(s) Summary
Wallet Wipe Signaling
wallet/src/de/schildbach/wallet/WalletApplication.java
finalizeWipe() now sets walletStateFlow to null to explicitly signal cleared wallet state.
UI Scrolling
wallet/src/de/schildbach/wallet/ui/compose_views/CreateInstantUsernameDialog.kt
Main Column becomes vertically scrollable via verticalScroll(rememberScrollState()) (added imports).
URI Handling
wallet/src/de/schildbach/wallet/ui/more/AboutActivity.kt
GitHub link handler uses String.toUri() instead of Uri.parse().
Dialog Button Compatibility
common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt
Supports AppCompatImageButton by switching lookup to View? and guarding setImageResource with type check.

Sequence Diagram(s)

sequenceDiagram
    participant System as App/System
    participant CoinJoin as CoinJoinService
    participant WalletService as BlockchainServiceImpl
    participant PeerGroup as PeerGroup/Networking

    WalletService->>CoinJoin: call prepareForShutdown()
    CoinJoin->>CoinJoin: set shutdown flag
    CoinJoin->>CoinJoin: await refreshKeysJob completion (<=10s)
    alt refreshKeysJob finishes
        CoinJoin-->>WalletService: returned (ready)
    else timeout
        CoinJoin-->>WalletService: returned (timeout)
    end
    WalletService->>PeerGroup: peerGroup.stop()
    PeerGroup-->>WalletService: stopped
    WalletService->>System: shutdown Dash system
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰
I nibbled bugs and counted rows,
Waited while the shutdown glows,
Wipes now whisper “null” in stream,
Scrolls that fit a rabbit’s dream,
Caches rebuilt — hop, tap, and go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix: improve shutdown of blockchainservice' is partially related to the changeset. While it accurately describes changes to BlockchainServiceImpl.kt's shutdown sequence, the changeset includes 8 files with broader changes across wallet state management, transaction caching, UI dialogs, and CoinJoin service coordination that extend beyond just BlockchainService improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/shutdown-blockchainservice

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
wallet/src/de/schildbach/wallet/ui/more/AboutActivity.kt (2)

41-41: Check ktlint import ordering.

androidx.core.net.toUri is placed after non-AndroidX imports (e.g., org.slf4j.LoggerFactory). If this repo enforces a strict import order via ktlint, this may cause a formatting/lint failure. Consider reordering imports to match the project’s ktlint rules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/more/AboutActivity.kt` at line 41, The
import ordering in AboutActivity.kt is incorrect for ktlint: move the AndroidX
import androidx.core.net.toUri so it appears with other AndroidX/android imports
before third-party imports like org.slf4j.LoggerFactory; update the import block
in AboutActivity (the file containing AboutActivity class) to follow the
project's ktlint ordering rules (group standard Kotlin/Java, Android/AndroidX,
then third-party) to resolve the lint failure.

71-75: Harden the GitHub link click handling against malformed/blank URL text.

binding.githubLink.text.toString() is assumed to be a valid absolute URL. If the displayed text is ever changed (or contains whitespace / localized formatting), ACTION_VIEW can fail or the URI conversion can throw. Add a small guard (trim, isNullOrBlank) and optionally wrap in try/catch with logging + early return.

Suggested change
         binding.githubLink.setOnClickListener {
-            val i = Intent(ACTION_VIEW)
-            i.data = binding.githubLink.text.toString().toUri()
-            startActivity(i)
+            val url = binding.githubLink.text?.toString()?.trim().orEmpty()
+            if (url.isBlank()) {
+                log.warn("GitHub link text is blank; cannot launch ACTION_VIEW")
+                return@setOnClickListener
+            }
+            try {
+                startActivity(Intent(ACTION_VIEW, url.toUri()))
+            } catch (ex: Exception) {
+                log.error("Failed to open GitHub link: $url", ex)
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/more/AboutActivity.kt` around lines 71 -
75, Protect the click handler for binding.githubLink by trimming and validating
the link text before converting to a URI: inside the setOnClickListener for
binding.githubLink, call binding.githubLink.text?.toString()?.trim() and check
isNullOrBlank(), returning early if invalid; then wrap the
Intent(ACTION_VIEW)/toUri()/startActivity calls in a try/catch and log the
exception (or silently return) to avoid crashes on malformed URIs (reference
setOnClickListener, binding.githubLink, Intent(ACTION_VIEW), toUri,
startActivity).
build.gradle (1)

2-47: (Optional) Consider avoiding -SNAPSHOT on stable branches / ensure CI reproducibility.

Using *-SNAPSHOT versions can make builds non-reproducible over time unless CI strictly pins or uses a deterministic snapshot strategy. If this is intended to ship to users, consider whether the target branch should rely on a released version instead of a snapshot, or whether there’s an existing policy for snapshot usage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build.gradle` around lines 2 - 47, The gradle ext properties use snapshot
versions (dppVersion = "2.0.5-SNAPSHOT" and dashjVersion = '22.0.2-CJ-SNAPSHOT')
which harms reproducibility; update these properties to a released version
(remove the -SNAPSHOT) or, if snapshots are required, add a clear comment and CI
pinning strategy in the file (e.g., reference to a pinned artifact resolver or
job that publishes the snapshot) so dppVersion and dashjVersion are either fixed
to a release string or documented/pinned for deterministic CI builds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wallet/src/de/schildbach/wallet/service/CoinJoinService.kt`:
- Around line 105-106: prepareForShutdown()'s shutdown barrier is lossy because
isShuttingDown and refreshKeysJob are sampled without synchronization, allowing
prepareMixing()/startMixing() to slip in and call refreshUnusedKeys() after
shutdown decided it was safe; fix by making the shutdown flag and job
publication atomic/synchronized: use a single Mutex or AtomicBoolean + volatile
refreshKeysJob so prepareForShutdown() sets isShuttingDown atomically, then
publishes/captures refreshKeysJob under the same lock, awaits its completion,
and only clears/changes shutdown state after awaiting; update
startMixing()/prepareMixing() and refreshUnusedKeys() checks to test
isShuttingDown under that same mutex (or use compare-and-set on an
AtomicBoolean) so no new refreshKeysJob can be created after shutdown is
initiated, and remove the premature clearing of the flag (the clear at
refreshKeysJob completion should happen only when safe under the same lock).

In `@wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt`:
- Around line 413-426: The early-return in rebuildIfCacheIncomplete() that
checks txGroupCacheDao.getTotalTxCount() == 0 causes a missed rebuild when the
app crashed before any group-cache write but the wallet already contains
transactions; instead, first obtain the wallet (walletData.wallet or
observeWallet()) and its wallet.getTransactionCount(true), and only return early
when the wallet has zero transactions; otherwise proceed to compute
cachedTxCount (txGroupCacheDao.getTotalTxCount()), groupCount
(txGroupCacheDao.getGroupCount()) and displayRowCount
(txDisplayCacheDao.getCount()) and trigger the rebuild logic—i.e., remove or
change the initial total-tx-count short-circuit to depend on
wallet.getTransactionCount(true) == 0.

---

Nitpick comments:
In `@build.gradle`:
- Around line 2-47: The gradle ext properties use snapshot versions (dppVersion
= "2.0.5-SNAPSHOT" and dashjVersion = '22.0.2-CJ-SNAPSHOT') which harms
reproducibility; update these properties to a released version (remove the
-SNAPSHOT) or, if snapshots are required, add a clear comment and CI pinning
strategy in the file (e.g., reference to a pinned artifact resolver or job that
publishes the snapshot) so dppVersion and dashjVersion are either fixed to a
release string or documented/pinned for deterministic CI builds.

In `@wallet/src/de/schildbach/wallet/ui/more/AboutActivity.kt`:
- Line 41: The import ordering in AboutActivity.kt is incorrect for ktlint: move
the AndroidX import androidx.core.net.toUri so it appears with other
AndroidX/android imports before third-party imports like
org.slf4j.LoggerFactory; update the import block in AboutActivity (the file
containing AboutActivity class) to follow the project's ktlint ordering rules
(group standard Kotlin/Java, Android/AndroidX, then third-party) to resolve the
lint failure.
- Around line 71-75: Protect the click handler for binding.githubLink by
trimming and validating the link text before converting to a URI: inside the
setOnClickListener for binding.githubLink, call
binding.githubLink.text?.toString()?.trim() and check isNullOrBlank(), returning
early if invalid; then wrap the Intent(ACTION_VIEW)/toUri()/startActivity calls
in a try/catch and log the exception (or silently return) to avoid crashes on
malformed URIs (reference setOnClickListener, binding.githubLink,
Intent(ACTION_VIEW), toUri, startActivity).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bf5eda34-cda1-43e0-9b41-8a7d38eb48f9

📥 Commits

Reviewing files that changed from the base of the PR and between 58b2e97 and 8962db2.

📒 Files selected for processing (8)
  • build.gradle
  • wallet/src/de/schildbach/wallet/WalletApplication.java
  • wallet/src/de/schildbach/wallet/database/dao/TxGroupCacheDao.kt
  • wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt
  • wallet/src/de/schildbach/wallet/service/CoinJoinService.kt
  • wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt
  • wallet/src/de/schildbach/wallet/ui/compose_views/CreateInstantUsernameDialog.kt
  • wallet/src/de/schildbach/wallet/ui/more/AboutActivity.kt

Comment thread wallet/src/de/schildbach/wallet/service/CoinJoinService.kt
Comment thread wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt (1)

63-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bottom padding accumulates on every insets dispatch — causes ever-growing padding.

v.paddingBottom + sys.bottom adds to whatever padding is currently set. Each time the insets listener fires (keyboard toggle, window focus change, re-attach) the bottom padding grows unboundedly. The original padding baseline must be captured once before the listener is set.

🐛 Proposed fix
+        val originalBottomPadding = view.paddingBottom
         ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
             val sys = insets.getInsets(WindowInsetsCompat.Type.systemBars())
             v.setPadding(
                 v.paddingLeft,
                 v.paddingTop,
                 v.paddingRight,
-                v.paddingBottom + sys.bottom
+                originalBottomPadding + sys.bottom
             )
             WindowInsetsCompat.CONSUMED
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt`
around lines 63 - 72, The insets listener incorrectly accumulates bottom padding
by adding sys.bottom to the current padding on each dispatch; capture the
original bottom padding once before calling
ViewCompat.setOnApplyWindowInsetsListener (e.g. read v.paddingBottom into an
initialBottomPadding variable in OffsetDialogFragment) and inside the listener
call v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight,
initialBottomPadding + sys.bottom) so you always base adjustments off the
original baseline rather than the mutated padding.
🧹 Nitpick comments (2)
common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt (2)

112-115: ⚡ Quick win

is AppCompatImageButton check silently skips icon assignment for non-AppCompat views.

If any collapse_button in a layout is a plain ImageButton (or another ImageView subclass), the setImageResource call is never reached and the button retains whatever src was set in XML. Consider broadening the check to ImageView, which covers all common button types that expose setImageResource.

♻️ Proposed refactor
-import androidx.appcompat.widget.AppCompatImageButton
+import android.widget.ImageView

         view.findViewById<View?>(R.id.collapse_button)?.apply {
-            if (this is AppCompatImageButton) {
+            if (this is ImageView) {
                 setImageResource(R.drawable.ic_popup_close_circle)
             }
             setOnClickListener { dismiss() }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt`
around lines 112 - 115, The current check in OffsetDialogFragment for the
collapse_button only handles AppCompatImageButton, so replace the type guard
with a broader ImageView check: locate the view lookup using
view.findViewById(R.id.collapse_button) in OffsetDialogFragment and change the
conditional from "if (this is AppCompatImageButton)" to "if (this is ImageView)"
(or remove the specific class check and safely cast to ImageView) and then call
setImageResource(R.drawable.ic_popup_close_circle) so all ImageView-based
buttons (including plain ImageButton and AppCompatImageButton) receive the icon.

123-127: ⚡ Quick win

launchWhenResumed is deprecated and scheduled for removal.

Lifecycle.launchWhenX methods have been deprecated as the use of a pausing dispatcher can lead to wasted resources in some cases; it is recommended to use Lifecycle.repeatOnLifecycle instead. For this one-shot "show dialog" use-case, withResumed is the idiomatic replacement.

♻️ Proposed fix
     fun show(activity: FragmentActivity) {
-        activity.lifecycleScope.launchWhenResumed {
-            show(activity.supportFragmentManager, "offset_dialog")
+        activity.lifecycleScope.launch {
+            activity.lifecycle.withResumed {
+                show(activity.supportFragmentManager, "offset_dialog")
+            }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt`
around lines 123 - 127, The use of the deprecated launchWhenResumed inside
OffsetDialogFragment.show should be replaced with the one-shot withResumed
pattern: call activity.lifecycleScope.withResumed {
show(activity.supportFragmentManager, "offset_dialog") } so the dialog is shown
when resumed without using the deprecated pausing dispatcher; update the imports
to use androidx.lifecycle.lifecycleScope and androidx.lifecycle.withResumed (or
the corresponding lifecycle-runtime-ktx extension) and remove launchWhenResumed
usage in the show(activity: FragmentActivity) method.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt`:
- Around line 63-72: The insets listener incorrectly accumulates bottom padding
by adding sys.bottom to the current padding on each dispatch; capture the
original bottom padding once before calling
ViewCompat.setOnApplyWindowInsetsListener (e.g. read v.paddingBottom into an
initialBottomPadding variable in OffsetDialogFragment) and inside the listener
call v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight,
initialBottomPadding + sys.bottom) so you always base adjustments off the
original baseline rather than the mutated padding.

---

Nitpick comments:
In
`@common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt`:
- Around line 112-115: The current check in OffsetDialogFragment for the
collapse_button only handles AppCompatImageButton, so replace the type guard
with a broader ImageView check: locate the view lookup using
view.findViewById(R.id.collapse_button) in OffsetDialogFragment and change the
conditional from "if (this is AppCompatImageButton)" to "if (this is ImageView)"
(or remove the specific class check and safely cast to ImageView) and then call
setImageResource(R.drawable.ic_popup_close_circle) so all ImageView-based
buttons (including plain ImageButton and AppCompatImageButton) receive the icon.
- Around line 123-127: The use of the deprecated launchWhenResumed inside
OffsetDialogFragment.show should be replaced with the one-shot withResumed
pattern: call activity.lifecycleScope.withResumed {
show(activity.supportFragmentManager, "offset_dialog") } so the dialog is shown
when resumed without using the deprecated pausing dispatcher; update the imports
to use androidx.lifecycle.lifecycleScope and androidx.lifecycle.withResumed (or
the corresponding lifecycle-runtime-ktx extension) and remove launchWhenResumed
usage in the show(activity: FragmentActivity) method.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76ecb716-872f-4363-b195-e23a15ac19cf

📥 Commits

Reviewing files that changed from the base of the PR and between 28ece25 and a1a104b.

📒 Files selected for processing (1)
  • common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant