Skip to content

feat: add transfer key to new and existing identities#1470

Merged
HashEngineering merged 15 commits intomasterfrom
feat/add-transfer-key
Mar 10, 2026
Merged

feat: add transfer key to new and existing identities#1470
HashEngineering merged 15 commits intomasterfrom
feat/add-transfer-key

Conversation

@HashEngineering
Copy link
Collaborator

@HashEngineering HashEngineering commented Mar 8, 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

  • Refactor

    • Centralized identity handling across DashPay: unified username/search, contacts, profiles, notifications, top-ups, invites, send/receive flows and restore/upgrade paths for more consistent identity behavior.
  • Chores

    • Bumped version constants: DashJ → 22.0.1-SNAPSHOT; DashPay Platform → 2.0.4-SNAPSHOT.

@HashEngineering HashEngineering self-assigned this Mar 8, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

Adds a new IdentityRepository (interface + implementation), migrates identity-related logic and DI wiring from PlatformRepo into IdentityRepository across services, workers, ViewModels, and UI, trims PlatformRepo surface, and bumps two build version constants.

Changes

Cohort / File(s) Summary
Version Updates
build.gradle
Bumped dashjVersion22.0.1-SNAPSHOT and dppVersion2.0.4-SNAPSHOT.
New Identity Repository
wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt
Adds IdentityRepository interface and IdentityRepositoryImpl with extensive coroutine/Flow APIs for identity lifecycle, balance, usernames/contacts, profiles, alerts, upgrades, frequent-contacts, and DB clear operations.
DI Wiring
wallet/src/de/schildbach/wallet/di/...
AppModule.kt, DashPayModule.kt
Wires IdentityRepository into DI: AppModule uses it when constructing services; DashPayModule binds IdentityRepositoryImplIdentityRepository.
PlatformRepo Surface Reduction
wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt
Removes large identity lifecycle / search / notification surface and constructor storage dependency; retains limited profile/balance/profile-observe APIs and adds exception-format overloads.
Core Services
wallet/src/de/schildbach/wallet/service/...
BlockchainServiceImpl.kt, PlatformBroadcastService.kt, PlatformSyncService.kt, CoinJoinService.kt
Injects IdentityRepository; replaces platformRepo.identity usages with identityRepository equivalents; PlatformSyncService adds initialization to register identity state; CoinJoinService adds null-safe WalletEx guard.
Payments & Task Runners
wallet/src/de/schildbach/wallet/payments/...
SendCoinsTaskRunner.kt
Adds IdentityRepository dependency; contact lookup uses identityRepository.blockchainIdentity?.getContactForTransaction(...).
TopUp & Related Repos
wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt
Replaces platformRepo identity usage with identityRepository across top-up creation, submission, and checks; constructor updated.
Workers (restore/topup/send-invite/update-profile)
wallet/src/de/schildbach/wallet/service/platform/work/*, ui/dashpay/work/*
RestoreIdentityWorker.kt, TopupIdentityWorker.kt, SendInviteWorker.kt, UpdateProfileWorker.kt, SendInviteWorker.kt, TopupIdentityWorker.kt
Injects IdentityRepository, routes identity updates/loads/state persistence via IdentityRepository, updates constructors and error handling flows.
WalletApplication
wallet/src/de/schildbach/wallet/WalletApplication.java, WalletApplicationExt.kt
Injects IdentityRepository; clearDatabases now calls identityRepository.clearDatabase(...).
UI / ViewModels / LiveData / Fragments
wallet/src/de/schildbach/wallet/ui/... (many files)
Adds IdentityRepository dependency to numerous ViewModels, LiveData, and fragments; replaces platformRepo.* identity calls (search, observe, balance, profile, contact lookups, state updates) with identityRepository equivalents; adjusts constructors and some method signatures.
Invite / Create Identity Flow
wallet/src/de/schildbach/wallet/ui/invite/InviteHandler.kt, CreateIdentityService.kt
Replaces platformRepo clearing/updates with identityRepository calls; InviteHandler.handleError signature updated to accept IdentityRepository.
Config
wallet/src/de/schildbach/wallet/ui/dashpay/utils/DashPayConfig.kt
Adds UPGRADE_IDENTITY_REQUIRED boolean preference key.

Sequence Diagram(s)

sequenceDiagram
  participant UI as View / ViewModel
  participant ID as IdentityRepository
  participant DB as AppDatabase
  participant PL as PlatformRepo
  participant API as Platform API

  UI->>ID: request user / balance / search / contact lookup
  ID->>DB: read cached identity/profile/contacts
  DB-->>ID: cached data
  alt cached sufficient
    ID-->>UI: return cached result / Flow update
  else need remote resolution
    ID->>PL: resolve username / profile / documents
    PL-->>ID: platform documents / identifiers
    ID->>API: call platform API for balance / upgrades
    API-->>ID: balances / responses
    ID->>DB: persist updates
    DB-->>ID: ack
    ID-->>UI: return aggregated result / Flow update
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I hopped through branches, found a tidy key,
A new Identity home where identities be.
Services sing, workers hum in tune,
Platform trimmed back under a friendly moon.
A carrot clap — code hops forward, soon!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add transfer key to new and existing identities' directly matches the primary change: extracting identity-related operations from PlatformRepo into a new IdentityRepository, affecting both new and existing identity management.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-transfer-key

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

@HashEngineering
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
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: 10

Caution

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

⚠️ Outside diff range comments (8)
wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt (4)

382-389: ⚠️ Potential issue | 🟡 Minor

Potential NPE: blockchainIdentity!! without null guard.

The non-null assertion on blockchainIdentity can crash if identity is null. Consider adding an early guard or using a more descriptive exception.

🛡️ Proposed fix with explicit null check
 private suspend fun addTopUp(txId: Sha256Hash): TopUp {
+    val identity = identityRepository.blockchainIdentity
+        ?: throw IllegalStateException("Cannot add top-up without blockchain identity")
     val topUp = TopUp(
         txId,
-        identityRepository.blockchainIdentity!!.uniqueIdString
+        identity.uniqueIdString
     )
     topUpsDao.insert(topUp)
     return topUp
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt` around
lines 382 - 389, The addTopUp function uses
identityRepository.blockchainIdentity!! which can throw an NPE; update addTopUp
to null-check identityRepository.blockchainIdentity before using it (or retrieve
it into a local val and check), and if null either throw a descriptive
IllegalStateException (e.g., "blockchain identity not initialized") or return a
recoverable error; ensure the TopUp constructor still receives a non-null
uniqueIdString only after the guard and keep the topUpsDao.insert(topUp) call
after the check.

539-554: ⚠️ Potential issue | 🟠 Major

Higher-risk NPE in fallback path.

When topUp is null, the code falls back to identityRepository.blockchainIdentity!!.uniqueIdentifier. This fallback path will definitely be executed for new transactions, making the NPE risk more likely.

🛡️ Proposed fix with early guard
         log.info("checking all topup transactions")
+        val blockchainIdentity = identityRepository.blockchainIdentity
+        if (blockchainIdentity == null) {
+            log.warn("Cannot check topup transactions: no blockchain identity")
+            checkedPreviousTopUps = true
+            return
+        }
         authExtension.topupFundingTransactions.forEach { assetLockTx ->
             val topUp = topUpsDao.getByTxId(assetLockTx.txId)
             if (topUp == null || topUp.notUsed()) {
-                val identity = topUp?.toUserId ?: identityRepository.blockchainIdentity!!.uniqueIdentifier.toString()
+                val identity = topUp?.toUserId ?: blockchainIdentity.uniqueIdentifier.toString()
                 if (topUp == null) {
                     topUpsDao.insert(TopUp(assetLockTx.txId, identity))
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt` around
lines 539 - 554, The null-check logic can still NPE because it force-unwraps
identityRepository.blockchainIdentity and platformRepo.getWalletEncryptionKey();
modify the branch handling when topUp == null to safely obtain the identity and
wallet key before using them: first retrieve val identityObj =
identityRepository.blockchainIdentity and if it's null skip or log and continue,
then compute val identity = topUp?.toUserId ?:
identityObj.uniqueIdentifier.toString() and only then call
topUpsDao.insert(TopUp(...)) and topUpIdentity(...); also avoid force-unwrapping
platformRepo.getWalletEncryptionKey() by obtaining the key into a val and
skipping/handling the operation if it is null so topUpIdentity is never called
with a null key.

419-426: ⚠️ Potential issue | 🟡 Minor

Same NPE risk pattern with blockchainIdentity!!.

This follows the same pattern as addTopUp. If the entry points to top-up flows don't guarantee identity existence, this will crash.

🛡️ Proposed fix with explicit null check
+    val identity = identityRepository.blockchainIdentity
+        ?: throw IllegalStateException("Cannot top up without blockchain identity")
     try {
-        identityRepository.blockchainIdentity!!.topUp(
+        identity.topUp(
             topUpTx,
             aesKeyParameter,
             useISLock = true,
             waitForChainlock = true
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt` around
lines 419 - 426, The code uses identityRepository.blockchainIdentity!! when
calling topUp (with topUpTx and aesKeyParameter), which can NPE if
blockchainIdentity is null; change this to an explicit null check before calling
identityRepository.blockchainIdentity.topUp (or use a safe-call with elvis): if
identityRepository.blockchainIdentity == null then log an error (including
context like topUpTx.txId) and abort/return an error instead of invoking topUp,
otherwise call blockchainIdentity.topUp(topUpTx, aesKeyParameter, useISLock =
true, waitForChainlock = true); apply the same pattern you used for addTopUp to
ensure no NPE.

674-686: ⚠️ Potential issue | 🟡 Minor

NPE risk with dashPayProfile!! on line 678.

getLocalUserProfile() may return null, but dashPayProfile!! is used without a prior null check. The existing assetLockTx != null check doesn't guarantee profile existence.

🛡️ Proposed fix with null check
-                    val dashPayProfile = identityRepository.getLocalUserProfile()
                     val assetLockTx = fundingTxes[invitation.txid]
-                    if (assetLockTx != null) {
-                        val appsFlyerLink = createAppsFlyerLink(dashPayProfile!!, assetLockTx, encryptionKey)
+                    val dashPayProfile = identityRepository.getLocalUserProfile()
+                    if (assetLockTx != null && dashPayProfile != null) {
+                        val appsFlyerLink = createAppsFlyerLink(dashPayProfile, assetLockTx, encryptionKey)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt` around
lines 674 - 686, The code calls identityRepository.getLocalUserProfile() and
then force-unwraps dashPayProfile with dashPayProfile!! before passing it to
createAppsFlyerLink, which can NPE; change this to safely handle a null profile
by checking dashPayProfile for null (e.g., if (dashPayProfile != null) { ... }
or return/skip when null) before calling createAppsFlyerLink and
updateInvitation so createAppsFlyerLink and updateInvitation are only invoked
with a non-null profile; reference dashPayProfile, getLocalUserProfile(),
createAppsFlyerLink(...), updateInvitation(...) and invitation.copy(...) when
applying the fix.
wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt (1)

555-565: ⚠️ Potential issue | 🟠 Major

Use the active/latest account reference when deriving the contact address.

Line 562 resolves the next contact address from mostRecentContactRequest!!.accountReference, but the code above computes firstTimestamp = min(...), so this is actually the oldest request. With transfer keys keyed by accountReference, re-requested contacts can be resolved against a stale key/address here.

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

In `@wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt` around lines
555 - 565, The code uses firstTimestamp = min(...) and then selects
mostRecentContactRequest = map[firstTimestamp], which actually picks the oldest
request; change the logic to select the latest contact request (use max of
contactRequest.timestamp or compute lastTimestamp) and then pass that request's
accountReference into
identityRepository.getNextContactAddress(dashPayProfile.userId, ...). Update
references to firstTimestamp/mostRecentContactRequest to reflect the latest
timestamp so the derived contact address uses the active/latest accountReference
from dashPayContactRequests.
wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt (1)

91-112: ⚠️ Potential issue | 🟠 Major

Verify transfer key upgrade path for existing contacts.

Lines 102-108 skip addPaymentKeyChainFromContact() once a receiving keychain exists. upgradeIdentity() only adds missing keys to the user's own identity, not to contact keychains. If addPaymentKeyChainFromContact() is the only place that attaches transfer keys to contacts, existing identities will never receive upgrades when contact request parameters change (e.g., new accountReference). Confirm whether keychains are mutated in-place during sync, or implement an explicit upgrade mechanism for already-linked contacts.

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

In `@wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt`
around lines 91 - 112, The code skips calling addPaymentKeyChainFromContact(...)
when hasReceivingKeyChain(contact) is true, which prevents existing contacts
from receiving transfer key upgrades; update the logic so that after creating
the contact request (platform.contactRequests.create) you always verify and
apply any missing transfer/transfer-account keys for existing contacts (not only
on missing receiving chain) by invoking an explicit upgrade path—either call a
new helper (e.g., upgradeContactKeyChainIfNeeded) or call
addPaymentKeyChainFromContact(...) conditionally to add missing transfer keys
for EvolutionContact instances even when
walletDataProvider.wallet!!.hasReceivingKeyChain(contact) is true, and ensure
you still propagate context (Context.propagate(...)), persist changes via
identityRepository.updateDashPayContactRequest(...) and trigger
platformSyncService.postUpdateBloomFilters() as appropriate; also consider
leveraging upgradeIdentity(...) semantics to detect and merge missing keys
rather than relying solely on in-place mutation.
wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt (1)

309-312: ⚠️ Potential issue | 🟡 Minor

Add a descriptive message to the exception.

Similar to the other case, this IllegalStateException() lacks context. This pattern appears in both createIdentity and createIdentityFromInvitation methods.

Proposed fix
         else -> {
-            throw IllegalStateException()
+            throw IllegalStateException("Username must be provided when no existing blockchain identity data exists")
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt` around
lines 309 - 312, The IllegalStateException thrown in the else branches of
createIdentity and createIdentityFromInvitation lacks context; update those
throw sites to include a descriptive message explaining why this state is
illegal (e.g., include the unexpected enum/value or operation name) mirroring
the pattern used elsewhere in the file so logs show meaningful diagnostics;
locate the throw in methods createIdentity and createIdentityFromInvitation and
replace IllegalStateException() with IllegalStateException("descriptive message
about the unexpected state or input").
wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt (1)

699-708: ⚠️ Potential issue | 🔴 Critical

Guard blockchainIdentity before resolving transaction contacts.

observeContacts() is backed by the local DB, so it can emit before IdentityRepository.init() finishes. In that window, Line 703 dereferences blockchainIdentity!! and crashes the home-screen refresh.

Suggested fix
         viewModelWorkerScope.launch {
+            val blockchainIdentity = identityRepository.blockchainIdentity ?: return@launch
             val contactsMap = if (this@MainViewModel.contacts.isNotEmpty()) {
                 txs.filterNot { it.isEntirelySelf(walletData.transactionBag) }
                     .mapNotNull { tx ->
-                        identityRepository.blockchainIdentity!!.getContactForTransaction(tx)?.let { contactId ->
+                        blockchainIdentity.getContactForTransaction(tx)?.let { contactId ->
                             contacts[contactId]?.let { contact ->
                                 tx.txId to contact
                             }
                         }
                     }.toMap()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 699 -
708, The crash occurs because observeContacts()'s coroutine dereferences
identityRepository.blockchainIdentity!! before IdentityRepository.init() may
have set it; update the mapping inside MainViewModel's
viewModelWorkerScope.launch so it first checks
identityRepository.blockchainIdentity for null and only resolves contacts when
non-null (e.g., skip or return empty map if blockchainIdentity is null),
replacing the forced !! with a safe check or safe-call against
identityRepository.blockchainIdentity.getContactForTransaction(tx) to avoid NPEs
in getContactForTransaction.
🧹 Nitpick comments (3)
wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsLiveData.kt (1)

31-31: Remove unused platformRepo parameter from NotificationsLiveData and update NotificationsViewModel.

The platformRepo parameter in NotificationsLiveData (line 31) is never used in the class. After migrating shouldShowAlert() and searchContacts() to identityRepository, this parameter can be safely removed. Additionally, update the instantiation in NotificationsViewModel to remove platformRepo from the constructor call.

Required changes

wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsLiveData.kt:

 open class NotificationsLiveData(
     private val identityRepository: IdentityRepository,
-    val platformRepo: PlatformRepo,
     platformSyncService: PlatformSyncService,
     protected val scope: CoroutineScope,
     private val userAlertDao: UserAlertDao
 ): ContactsBasedLiveData<Resource<List<NotificationItem>>>(platformSyncService) {

wallet/src/de/schildbach/wallet/ui/dashpay/notification/NotificationsViewModel.kt:

-    val notificationsLiveData = NotificationsLiveData(identityRepository, platformRepo, platformSyncService, viewModelScope, userAlertDao)
+    val notificationsLiveData = NotificationsLiveData(identityRepository, platformSyncService, viewModelScope, userAlertDao)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsLiveData.kt` at line
31, Remove the unused constructor parameter platformRepo from the
NotificationsLiveData class and update all call sites (notably the
NotificationsViewModel instantiation) to stop passing platformRepo; since
shouldShowAlert() and searchContacts() are now on identityRepository, delete the
platformRepo parameter declaration in NotificationsLiveData and remove it from
the NotificationsViewModel constructor call where NotificationsLiveData(...) is
created, ensuring the class compiles and no other references to platformRepo
remain in NotificationsLiveData.
wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt (1)

63-63: Consider making identityRepository private.

The property is exposed as public (val without private), which differs from other injected dependencies in this class (e.g., analytics, dashSystemService, walletData are private). Unless there's a specific need for external access, consider making it private for consistency.

Proposed fix
-    val identityRepository: IdentityRepository,
+    private val identityRepository: IdentityRepository,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt` at line
63, The injected IdentityRepository property in DashPayUserActivityViewModel is
currently public (val identityRepository) while other injected dependencies are
private; make it private to match consistency by changing the declaration to a
private val identityRepository so external access is prevented unless
required—update any internal usages within DashPayUserActivityViewModel to
reference the now-private identityRepository and ensure no external callers rely
on it before making the change.
wallet/src/de/schildbach/wallet/ui/dashpay/NotificationCountLiveData.kt (1)

28-28: Remove unused platformRepo parameter from NotificationCountLiveData.

This parameter was passed to the constructor but is never used in the class. After the migration to identityRepository, it can be safely removed from both the class constructor and the instantiation in MainViewModel.kt (line 294).

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

In `@wallet/src/de/schildbach/wallet/ui/dashpay/NotificationCountLiveData.kt` at
line 28, Remove the unused constructor parameter platformRepo from the
NotificationCountLiveData class definition (symbol: NotificationCountLiveData)
and update its constructor call sites to no longer pass platformRepo —
specifically remove the argument in the instantiation inside MainViewModel
(symbol: MainViewModel, around the previous call at the old line 294). Ensure
any imports or references to PlatformRepo in NotificationCountLiveData are
deleted if unused, and run a build to confirm no other call sites pass
platformRepo.
🤖 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/platform/IdentityRepository.kt`:
- Around line 123-145: The singleton must clear its in-memory identity and the
upgrade flag when storage is cleared or identity is absent: change
_blockchainIdentity from a lateinit var to a nullable var (e.g., private var
_blockchainIdentity: BlockchainIdentity? = null), update the blockchainIdentity
getter to return _blockchainIdentity directly and hasBlockchainIdentity to check
_blockchainIdentity != null, then in clearBlockchainIdentityData() set
_blockchainIdentity = null and hasCheckedIdentityForUpgrade = false; apply the
same clearing logic where storage-absence is handled (the similar block around
lines 208-213) so the previous wallet's identity and the
hasCheckedIdentityForUpgrade latch are both cleared.
- Around line 366-408: The upgrade flow latches hasCheckedIdentityForUpgrade too
early and treats a null/getIdentityBalance miss as success, causing retries to
stop even when keys are still missing; fix by: in upgradeIdentity() don't set
hasCheckedIdentityForUpgrade before calling addMissingKeys — only set it when
addMissingKeys returns true; in addMissingKeys(), treat null balance or missing
wallet/identity initialization as a retry-needed case and call
dashPayConfig.set(UPGRADE_IDENTITY_REQUIRED, true) (rather than leaving it
unset), and ensure you check the actual in-memory identity object
(_blockchainIdentity) initialization/state instead of hasIdentity() so you only
proceed when the object is fully initialized; keep existing success path to set
UPGRADE_IDENTITY_REQUIRED=false when keys were added.
- Around line 889-899: getIdentityFromPublicKeyId currently collapses "lookup
unavailable" into a null identity; instead, when
platformRepo.getWalletEncryptionKey() returns null (unencrypted wallet) you must
surface that as a lookup-unavailable condition rather than a definitive miss
(e.g. throw or return a specific error/exception), and do not swallow retryable
DAPI failures — do not catch MaxRetriesReachedException or
NoAvailableAddressesForRetryException and return null; let them propagate (or
wrap and rethrow) so callers (preBlockDownload) can distinguish
transient/unavailable lookups from "identity not found". Update
getIdentityFromPublicKeyId to check platformRepo.getWalletEncryptionKey(), throw
a LookupUnavailableException (or rethrow the caught exceptions) before calling
platformRepo.getBlockchainIdentityKey and
platform.stateRepository.fetchIdentityFromPubKeyHash, and remove the existing
catch blocks that return null for those exceptions.

In
`@wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt`:
- Line 91: Replace unsafe non-null assertions on
identityRepository.blockchainIdentity in PlatformBroadcastService with explicit
null checks and descriptive failures: in each public method that currently does
val blockchainIdentity = identityRepository.blockchainIdentity!! (the three call
sites), use a safe read like val blockchainIdentity =
identityRepository.blockchainIdentity ?: throw IllegalStateException("blockchain
identity not available; ensure identity is loaded before calling
PlatformBroadcastService.<methodName>") or return a failure result/error
response appropriate to the method API. Update the three occurrences (the
statements creating blockchainIdentity) so they surface a clear, descriptive
error instead of allowing an NPE.

In `@wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt`:
- Around line 182-186: The init() currently launches identityRepository.init()
and initializeStateRepository() asynchronously and returns immediately, causing
a race with preBlockDownload(), initSync(), and updateContactRequests() which
read hasBlockchainIdentity/blockchainIdentity and stateRepository; change
PlatformSyncService.init() so it waits for those initializations to complete
before returning (e.g., start a coroutine for identityRepository.init() and
initializeStateRepository() and call job.join(), or run them inside runBlocking)
so identityRepository.init() and initializeStateRepository() finish before any
calls to preBlockDownload(), initSync(), or updateContactRequests() occur.
- Around line 1726-1737: The initializeStateRepository() seeding misses
identities from sent/accepted contact requests, causing sent contacts without
profile rows to be unmarked; update the method to also iterate
dashPayContactRequestDao.loadFromMe(identityId) (or the DAO method that returns
sent requests) and call
platform.stateRepository.addValidIdentity(it.userIdentifier) for each entry, in
addition to the existing dashPayContactRequestDao.loadFromOthers(...) and
dashPayProfileDao.loadAll() calls, before calling
platform.stateRepository.storeIdentity(blockchainIdentity.identity!!).

In `@wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt`:
- Around line 539-541: Replace the bare IllegalStateException() in the else
branch inside CreateIdentityService (the else shown in the diff) with an
exception that includes a descriptive message and relevant state context (e.g.
the unexpected branch name or the variable representing the current state); for
example, throw IllegalStateException("Unexpected identity creation state:
$state") or otherwise interpolate the appropriate local variables so the log
shows why this branch was reached. Ensure you reference the same local
identifier(s) used in the surrounding when/if branches so the message is
informative for debugging.

In `@wallet/src/de/schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt`:
- Line 84: The worker currently dereferences blockchainIdentity (from
identityRepository.blockchainIdentity) with !! outside the try, which can throw
if the identity is missing; update the UpdateProfileWorker logic to check for a
null blockchainIdentity before building the profile payload (or wrap the
dereference inside the existing try), and if it's null return
Result.failure(...) (or a specific retry/failure as appropriate) instead of
letting an exception escape; reference the blockchainIdentity variable and
identityRepository.blockchainIdentity and ensure the profile-building path
(where blockchainIdentity was used) only runs when the identity is non-null.

In `@wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt`:
- Around line 194-195: The code uses identityRepository.blockchainIdentity!!
which can throw an NPE; update the DashPayUserActivityViewModel code to
null-check blockchainIdentity (or use a safe-call + let) before calling
getContactTransactions: if blockchainIdentity is null return early or handle the
error path, otherwise call
blockchainIdentity.getContactTransactions(Identifier.from(userId),
accountReference); ensure any downstream code expects the possibly-absent
result.

In `@wallet/src/de/schildbach/wallet/ui/invite/InviteHandler.kt`:
- Around line 48-49: InviteHandler currently declares `@Inject` lateinit vars
platformRepo and identityRepository but is manually instantiated, so Hilt won't
inject them; replace the `@Inject` properties by adding PlatformRepo and
IdentityRepository parameters to InviteHandler's constructor (remove `@Inject` and
lateinit), update all call sites that create InviteHandler to pass these
dependencies, and then use the injected constructor fields when calling
identityRepository.clearBlockchainIdentityData() (and any platformRepo usage);
alternatively, if you prefer Hilt-managed creation, convert InviteHandler to use
an `@Inject` constructor and obtain instances from Hilt-managed components instead
of manual instantiation.

---

Outside diff comments:
In
`@wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt`:
- Around line 91-112: The code skips calling addPaymentKeyChainFromContact(...)
when hasReceivingKeyChain(contact) is true, which prevents existing contacts
from receiving transfer key upgrades; update the logic so that after creating
the contact request (platform.contactRequests.create) you always verify and
apply any missing transfer/transfer-account keys for existing contacts (not only
on missing receiving chain) by invoking an explicit upgrade path—either call a
new helper (e.g., upgradeContactKeyChainIfNeeded) or call
addPaymentKeyChainFromContact(...) conditionally to add missing transfer keys
for EvolutionContact instances even when
walletDataProvider.wallet!!.hasReceivingKeyChain(contact) is true, and ensure
you still propagate context (Context.propagate(...)), persist changes via
identityRepository.updateDashPayContactRequest(...) and trigger
platformSyncService.postUpdateBloomFilters() as appropriate; also consider
leveraging upgradeIdentity(...) semantics to detect and merge missing keys
rather than relying solely on in-place mutation.

In `@wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt`:
- Around line 382-389: The addTopUp function uses
identityRepository.blockchainIdentity!! which can throw an NPE; update addTopUp
to null-check identityRepository.blockchainIdentity before using it (or retrieve
it into a local val and check), and if null either throw a descriptive
IllegalStateException (e.g., "blockchain identity not initialized") or return a
recoverable error; ensure the TopUp constructor still receives a non-null
uniqueIdString only after the guard and keep the topUpsDao.insert(topUp) call
after the check.
- Around line 539-554: The null-check logic can still NPE because it
force-unwraps identityRepository.blockchainIdentity and
platformRepo.getWalletEncryptionKey(); modify the branch handling when topUp ==
null to safely obtain the identity and wallet key before using them: first
retrieve val identityObj = identityRepository.blockchainIdentity and if it's
null skip or log and continue, then compute val identity = topUp?.toUserId ?:
identityObj.uniqueIdentifier.toString() and only then call
topUpsDao.insert(TopUp(...)) and topUpIdentity(...); also avoid force-unwrapping
platformRepo.getWalletEncryptionKey() by obtaining the key into a val and
skipping/handling the operation if it is null so topUpIdentity is never called
with a null key.
- Around line 419-426: The code uses identityRepository.blockchainIdentity!!
when calling topUp (with topUpTx and aesKeyParameter), which can NPE if
blockchainIdentity is null; change this to an explicit null check before calling
identityRepository.blockchainIdentity.topUp (or use a safe-call with elvis): if
identityRepository.blockchainIdentity == null then log an error (including
context like topUpTx.txId) and abort/return an error instead of invoking topUp,
otherwise call blockchainIdentity.topUp(topUpTx, aesKeyParameter, useISLock =
true, waitForChainlock = true); apply the same pattern you used for addTopUp to
ensure no NPE.
- Around line 674-686: The code calls identityRepository.getLocalUserProfile()
and then force-unwraps dashPayProfile with dashPayProfile!! before passing it to
createAppsFlyerLink, which can NPE; change this to safely handle a null profile
by checking dashPayProfile for null (e.g., if (dashPayProfile != null) { ... }
or return/skip when null) before calling createAppsFlyerLink and
updateInvitation so createAppsFlyerLink and updateInvitation are only invoked
with a non-null profile; reference dashPayProfile, getLocalUserProfile(),
createAppsFlyerLink(...), updateInvitation(...) and invitation.copy(...) when
applying the fix.

In `@wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt`:
- Around line 309-312: The IllegalStateException thrown in the else branches of
createIdentity and createIdentityFromInvitation lacks context; update those
throw sites to include a descriptive message explaining why this state is
illegal (e.g., include the unexpected enum/value or operation name) mirroring
the pattern used elsewhere in the file so logs show meaningful diagnostics;
locate the throw in methods createIdentity and createIdentityFromInvitation and
replace IllegalStateException() with IllegalStateException("descriptive message
about the unexpected state or input").

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 699-708: The crash occurs because observeContacts()'s coroutine
dereferences identityRepository.blockchainIdentity!! before
IdentityRepository.init() may have set it; update the mapping inside
MainViewModel's viewModelWorkerScope.launch so it first checks
identityRepository.blockchainIdentity for null and only resolves contacts when
non-null (e.g., skip or return empty map if blockchainIdentity is null),
replacing the forced !! with a safe check or safe-call against
identityRepository.blockchainIdentity.getContactForTransaction(tx) to avoid NPEs
in getContactForTransaction.

In `@wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt`:
- Around line 555-565: The code uses firstTimestamp = min(...) and then selects
mostRecentContactRequest = map[firstTimestamp], which actually picks the oldest
request; change the logic to select the latest contact request (use max of
contactRequest.timestamp or compute lastTimestamp) and then pass that request's
accountReference into
identityRepository.getNextContactAddress(dashPayProfile.userId, ...). Update
references to firstTimestamp/mostRecentContactRequest to reflect the latest
timestamp so the derived contact address uses the active/latest accountReference
from dashPayContactRequests.

---

Nitpick comments:
In `@wallet/src/de/schildbach/wallet/ui/dashpay/NotificationCountLiveData.kt`:
- Line 28: Remove the unused constructor parameter platformRepo from the
NotificationCountLiveData class definition (symbol: NotificationCountLiveData)
and update its constructor call sites to no longer pass platformRepo —
specifically remove the argument in the instantiation inside MainViewModel
(symbol: MainViewModel, around the previous call at the old line 294). Ensure
any imports or references to PlatformRepo in NotificationCountLiveData are
deleted if unused, and run a build to confirm no other call sites pass
platformRepo.

In `@wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsLiveData.kt`:
- Line 31: Remove the unused constructor parameter platformRepo from the
NotificationsLiveData class and update all call sites (notably the
NotificationsViewModel instantiation) to stop passing platformRepo; since
shouldShowAlert() and searchContacts() are now on identityRepository, delete the
platformRepo parameter declaration in NotificationsLiveData and remove it from
the NotificationsViewModel constructor call where NotificationsLiveData(...) is
created, ensuring the class compiles and no other references to platformRepo
remain in NotificationsLiveData.

In `@wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt`:
- Line 63: The injected IdentityRepository property in
DashPayUserActivityViewModel is currently public (val identityRepository) while
other injected dependencies are private; make it private to match consistency by
changing the declaration to a private val identityRepository so external access
is prevented unless required—update any internal usages within
DashPayUserActivityViewModel to reference the now-private identityRepository and
ensure no external callers rely on it before making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc34c452-374e-4673-96fd-5852efd846cb

📥 Commits

Reviewing files that changed from the base of the PR and between c1e56b5 and 09becb3.

📒 Files selected for processing (29)
  • build.gradle
  • wallet/src/de/schildbach/wallet/WalletApplication.java
  • wallet/src/de/schildbach/wallet/WalletApplicationExt.kt
  • wallet/src/de/schildbach/wallet/di/AppModule.kt
  • wallet/src/de/schildbach/wallet/di/DashPayModule.kt
  • wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt
  • wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt
  • wallet/src/de/schildbach/wallet/service/CoinJoinService.kt
  • wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt
  • wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt
  • wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt
  • wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt
  • wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityWorker.kt
  • wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityWorker.kt
  • wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/TransactionResultViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/DashPayViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/EditProfileViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/NotificationCountLiveData.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsLiveData.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/notification/NotificationsViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/utils/DashPayConfig.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteWorker.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt
  • wallet/src/de/schildbach/wallet/ui/invite/InviteHandler.kt
  • wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt

Comment on lines +889 to +899
override fun getIdentityFromPublicKeyId(): Identity? {
return try {
platformRepo.getWalletEncryptionKey()?.let {
val firstIdentityKey = platformRepo.getBlockchainIdentityKey(0, it) ?: return null
platform.stateRepository.fetchIdentityFromPubKeyHash(firstIdentityKey.pubKeyHash)
}
} catch (e: MaxRetriesReachedException) {
null
} catch (e: NoAvailableAddressesForRetryException) {
null
}
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 9, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don't collapse 'lookup unavailable' into 'identity not found'.

On unencrypted wallets getWalletEncryptionKey() returns null, so the safe-call skips the pubkey lookup entirely. The catch blocks also fold retryable DAPI failures into the same null result. preBlockDownload() treats that as a definitive miss and will skip recovery for identities that actually exist.

Suggested fix
     override fun getIdentityFromPublicKeyId(): Identity? {
         return try {
-            platformRepo.getWalletEncryptionKey()?.let {
-                val firstIdentityKey = platformRepo.getBlockchainIdentityKey(0, it) ?: return null
-                platform.stateRepository.fetchIdentityFromPubKeyHash(firstIdentityKey.pubKeyHash)
-            }
+            val keyParameter = platformRepo.getWalletEncryptionKey()
+            if (walletApplication.wallet!!.isEncrypted && keyParameter == null) {
+                throw IllegalStateException("Wallet encryption key unavailable")
+            }
+            val firstIdentityKey = platformRepo.getBlockchainIdentityKey(0, keyParameter) ?: return null
+            platform.stateRepository.fetchIdentityFromPubKeyHash(firstIdentityKey.pubKeyHash)
         } catch (e: MaxRetriesReachedException) {
-            null
+            throw e
         } catch (e: NoAvailableAddressesForRetryException) {
-            null
+            throw e
         }
     }
🧰 Tools
🪛 detekt (1.23.8)

[warning] 895-895: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 897-897: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

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

In `@wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt`
around lines 889 - 899, getIdentityFromPublicKeyId currently collapses "lookup
unavailable" into a null identity; instead, when
platformRepo.getWalletEncryptionKey() returns null (unencrypted wallet) you must
surface that as a lookup-unavailable condition rather than a definitive miss
(e.g. throw or return a specific error/exception), and do not swallow retryable
DAPI failures — do not catch MaxRetriesReachedException or
NoAvailableAddressesForRetryException and return null; let them propagate (or
wrap and rethrow) so callers (preBlockDownload) can distinguish
transient/unavailable lookups from "identity not found". Update
getIdentityFromPublicKeyId to check platformRepo.getWalletEncryptionKey(), throw
a LookupUnavailableException (or rethrow the caught exceptions) before calling
platformRepo.getBlockchainIdentityKey and
platform.stateRepository.fetchIdentityFromPubKeyHash, and remove the existing
catch blocks that return null for those exceptions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we don't have a good way to handle this failure. We would need to stop sync process otherwise a rescan blockchain is required.

Copy link
Contributor

Choose a reason for hiding this comment

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

@HashEngineering, understood — if the caller can't distinguish "transient failure" from "no identity" without stopping the entire sync, propagating the exceptions becomes a much larger change.

As a minimal improvement, you could at least log the swallowed exceptions at warn/error level before returning null, so operators can see in the logs when a DAPI retry failure is masquerading as a clean "not found." This also satisfies the detekt SwallowedException warnings without changing any control flow:

override fun getIdentityFromPublicKeyId(): Identity? {
    return try {
        platformRepo.getWalletEncryptionKey()?.let {
            val firstIdentityKey = platformRepo.getBlockchainIdentityKey(0, it) ?: return null
            platform.stateRepository.fetchIdentityFromPubKeyHash(firstIdentityKey.pubKeyHash)
        }
    } catch (e: MaxRetriesReachedException) {
        log.warn("getIdentityFromPublicKeyId: DAPI max retries reached, returning null (identity lookup may be incomplete)", e)
        null
    } catch (e: NoAvailableAddressesForRetryException) {
        log.warn("getIdentityFromPublicKeyId: No available DAPI addresses, returning null (identity lookup may be incomplete)", e)
        null
    }
}

The unencrypted wallet case (key = null → safe call skips) is arguably acceptable as-is since there's no key to look up with, though a comment noting the behaviour would help future readers. The larger issue of callers being unable to distinguish transient failures from definitive misses could be tracked as a follow-up.

Would you like me to open a GitHub issue to track the sync-stopping / rescan-required logic as a future improvement?


✏️ Learnings added
Learnt from: HashEngineering
Repo: dashpay/dash-wallet PR: 1470
File: wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt:889-899
Timestamp: 2026-03-09T03:31:06.650Z
Learning: In `wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt`, `getIdentityFromPublicKeyId()` intentionally swallows `MaxRetriesReachedException` and `NoAvailableAddressesForRetryException` by returning null. The developer (HashEngineering) confirmed there is no good way to handle these failures without stopping the entire sync process, which would require a rescan blockchain. This is an accepted architectural tradeoff; do not flag these swallowed exceptions as critical issues in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines 182 to +186
override fun init() {
syncScope.launch { platformRepo.init() }
syncScope.launch {
identityRepository.init()
initializeStateRepository()
}
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 9, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await IdentityRepository initialization before starting sync work.

init() now fires identityRepository.init()/initializeStateRepository() on a background coroutine and returns immediately. preBlockDownload(), initSync(), and updateContactRequests() all rely on hasBlockchainIdentity or blockchainIdentity!!, so a cold start can skip the first sync cycle or run with an empty stateRepository if those entrypoints win the race.

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

In `@wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt`
around lines 182 - 186, The init() currently launches identityRepository.init()
and initializeStateRepository() asynchronously and returns immediately, causing
a race with preBlockDownload(), initSync(), and updateContactRequests() which
read hasBlockchainIdentity/blockchainIdentity and stateRepository; change
PlatformSyncService.init() so it waits for those initializations to complete
before returning (e.g., start a coroutine for identityRepository.init() and
initializeStateRepository() and call job.join(), or run them inside runBlocking)
so identityRepository.init() and initializeStateRepository() finish before any
calls to preBlockDownload(), initSync(), or updateContactRequests() occur.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we will handle this later

Copy link
Contributor

Choose a reason for hiding this comment

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

@HashEngineering, understood — I'll note this for later. Would you like me to open a GitHub issue to track this race condition so it doesn't get lost?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@HashEngineering, I've created the GitHub issue to track this race condition. You can find it in the repository's issue tracker. The issue captures the root cause, the affected code location, and a proposed fix for future reference.


🏁 Script executed:

gh issue create \
  --repo dashpay/dash-wallet \
  --title "Race condition: PlatformSyncService.init() doesn't await identity repository initialization" \
  --assignee HashEngineering \
  --body "## Summary

\`PlatformSynchronizationService.init()\` launches \`identityRepository.init()\` and \`initializeStateRepository()\` on a background coroutine and returns immediately. This creates a race condition where \`preBlockDownload()\`, \`initSync()\`, and \`updateContactRequests()\` may be called before initialization completes, reading \`hasBlockchainIdentity\`/\`blockchainIdentity\` or using an empty \`stateRepository\`.

## Affected File

\`wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt\` (around lines 182–186)

## Root Cause

\`\`\`kotlin
override fun init() {
    syncScope.launch {
        identityRepository.init()
        initializeStateRepository()
    }
    // Returns immediately — callers may race with the above coroutine
}
\`\`\`

## Proposed Fix

Make \`init()\` wait for \`identityRepository.init()\` and \`initializeStateRepository()\` to complete before any call to \`preBlockDownload()\`, \`initSync()\`, or \`updateContactRequests()\` occurs. Options include:

- Storing the launched job and calling \`job.join()\` in \`preBlockDownload()\`/\`initSync()\` before proceeding.
- Converting \`init()\` to a \`suspend fun\` and awaiting the initializations directly.

## References

- PR: https://github.com/dashpay/dash-wallet/pull/1470
- Review comment: https://github.com/dashpay/dash-wallet/pull/1470#discussion_r2902690539
- Requested by: `@HashEngineering`"

Length of output: 50


@HashEngineering, the issue has been created and is ready for tracking:

#1471 — Race condition: PlatformSyncService.init() doesn't await identity repository initialization

It's assigned to you and includes the root cause, affected file location, and proposed fix options for when you're ready to address it.

Copy link
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

🤖 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/platform/IdentityRepository.kt`:
- Around line 621-625: The flow currently filters only on hasUsername then
force-unwraps identityData.userId in the flatMapLatest lambda, which can NPE
during onboarding; change the upstream predicate to ensure userId is non-null
(e.g. add a check like identityData.userId != null or replace
filterNotNull()/filter { it.hasUsername } with a single filter { it.hasUsername
&& it.userId != null }) or safely unwrap userId with let before calling
contacts-related logic so the code in the flatMapLatest (where
identityData.userId!! is referenced) never force-unwraps a null userId.
- Around line 890-892: getLocalUserProfile() currently force-unwraps
loadBlockchainIdentityBaseData() and its userId which will throw after
clearBlockchainIdentityData(); update getLocalUserProfile() to safely handle
nulls by calling loadBlockchainIdentityBaseData()?, check if the returned base
data or baseData.userId is null, and return null when either is missing,
otherwise call dashPayProfileDao.loadByUserId(userId) as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c16bb954-8f67-4bf3-b3c9-59573d48b072

📥 Commits

Reviewing files that changed from the base of the PR and between 09becb3 and f3ae37b.

📒 Files selected for processing (4)
  • wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt
  • wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt
  • wallet/src/de/schildbach/wallet/ui/invite/InviteHandler.kt
  • wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt

Copy link
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: 1

🤖 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/ui/DashPayUserActivityViewModel.kt`:
- Around line 194-197: The current code early-returns emptyList() when
identityRepository.blockchainIdentity is null, which discards already-built
results; instead, change the logic in DashPayUserActivityViewModel (around
identityRepository.blockchainIdentity check) to log the null identity but do NOT
return from the withContext block—fall back to an empty transaction list (e.g.,
set a local transactionResults = emptyList()) and continue merging with the
existing results variable so previously collected contact/invitation items are
preserved before the function returns.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99aaf35b-be28-4ff3-a8e0-7545f829c3dd

📥 Commits

Reviewing files that changed from the base of the PR and between f3ae37b and b27dad6.

📒 Files selected for processing (2)
  • wallet/src/de/schildbach/wallet/ui/DashPayUserActivityViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • wallet/src/de/schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt

Copy link
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 (1)
wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt (1)

456-469: Consider using firstOrNull for cleaner null handling.

Using firstOrNull { } instead of wrapping first { } in a try-catch is more idiomatic Kotlin and eliminates the swallowed exception warning.

Proposed refactor
             val userIds = if (onlyExactUsername) {
                 val result = mutableListOf<Identifier>()
-                val exactNameDoc = try {
-                    DomainDocument(nameDocuments.first { text == it.data["normalizedLabel"] })
-                } catch (e: NoSuchElementException) {
-                    null
-                }
+                val exactNameDoc = nameDocuments
+                    .firstOrNull { text == it.data["normalizedLabel"] }
+                    ?.let { DomainDocument(it) }
                 if (exactNameDoc != null) {
                     result.add(platformRepo.getIdentityForName(exactNameDoc))
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt`
around lines 456 - 469, Replace the try/catch around nameDocuments.first { ... }
with idiomatic firstOrNull to avoid swallowing exceptions: in the
onlyExactUsername branch, use nameDocuments.firstOrNull { text ==
it.data["normalizedLabel"] } to get a nullable element, wrap it in
DomainDocument only when non-null (e.g., via let or conditional), then call
platformRepo.getIdentityForName as before; this keeps the same behavior for
onlyExactUsername and the use of identifierDocumentMap.keys else branch
unchanged.
🤖 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/platform/IdentityRepository.kt`:
- Around line 802-807: The KDoc for override suspend fun
clearDatabase(includeInvitations: Boolean) in IdentityRepository.kt is stale and
contradicts the implementation; update it by either removing the lines stating
"This should not be a suspended method" or by revising the comment to accurately
describe why clearDatabase is suspendable (e.g., because it performs
coroutine-based IO) so the doc matches the function signature; modify the KDoc
above the clearDatabase method (the comment block starting "This is used by java
code, outside of coroutines") to reflect the current design or remove the
contradictory sentence.
- Around line 232-240: The two log statements in IdentityRepository.kt are
referencing the class field _blockchainIdentity?.uniqueIdString instead of the
newly constructed local blockchainIdentity, which can be null/stale; update both
log calls to use blockchainIdentity.uniqueIdString (or
blockchainIdentity?.uniqueIdString where needed) and ensure the log message
shows the local object's uniqueIdString and userId (e.g.,
blockchainIdentity.userId / blockchainIdentity?.uniqueIdString) so the logs
reflect the identity being loaded rather than the repository field.

---

Nitpick comments:
In `@wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt`:
- Around line 456-469: Replace the try/catch around nameDocuments.first { ... }
with idiomatic firstOrNull to avoid swallowing exceptions: in the
onlyExactUsername branch, use nameDocuments.firstOrNull { text ==
it.data["normalizedLabel"] } to get a nullable element, wrap it in
DomainDocument only when non-null (e.g., via let or conditional), then call
platformRepo.getIdentityForName as before; this keeps the same behavior for
onlyExactUsername and the use of identifierDocumentMap.keys else branch
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9fbdb0ea-9a8c-4849-bd0b-991991583374

📥 Commits

Reviewing files that changed from the base of the PR and between b27dad6 and f2eb6b0.

📒 Files selected for processing (1)
  • wallet/src/de/schildbach/wallet/service/platform/IdentityRepository.kt

Copy link

@abaranouski abaranouski left a comment

Choose a reason for hiding this comment

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

looks good

@HashEngineering HashEngineering merged commit 1a438df into master Mar 10, 2026
3 of 5 checks passed
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.

2 participants