Skip to content

Implement CSPP phase 1 items#594

Merged
praveenperera merged 25 commits intomasterfrom
cspp-phase-1
Feb 18, 2026
Merged

Implement CSPP phase 1 items#594
praveenperera merged 25 commits intomasterfrom
cspp-phase-1

Conversation

@praveenperera
Copy link
Contributor

@praveenperera praveenperera commented Feb 13, 2026

Closes: #554
Closes: #555
Closes: #556

Summary by CodeRabbit

  • New Features

    • Watch‑only wallet flows: import via hardware (QR/NFC/clipboard) or mnemonic (12/24), and explicit set‑wallet‑type action.
    • Duplicate‑wallet reconciliation and upgrade paths to recover existing wallets.
  • Bug Fixes

    • Hot‑wallet missing key now surfaces with clear recovery, import, or downgrade options.
    • Hot wallets auto‑downgrade to watch‑only when keys are unavailable; improved import error handling and messages.
  • Chores

    • Secure preferences excluded from backups for better data protection.

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Detects missing hot-wallet private keys and adds watch-only import/restore flows across Rust, Android, and iOS: new AppAlertState variants, downgrade/reconcile logic emitting HotWalletKeyMissing, FFI enum/serialization expansions, and a new FFI call to set wallet type.

Changes

Cohort / File(s) Summary
Alert state & FFI enums
rust/src/app/alert_state.rs, android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
Added AppAlertState variants (HotWalletKeyMissing, ConfirmWatchOnly, CantSendOnWatchOnlyWallet, WatchOnlyImportHardware, WatchOnlyImportWords) and extended FFI serialization/read/write to cover new variants.
Rust: wallet manager & import/reconcile
rust/src/manager/wallet_manager.rs, rust/src/manager/import_wallet_manager.rs, rust/src/wallet.rs, rust/src/wallet/metadata.rs
Detect missing private keys, downgrade Hot→WatchOnly when absent (deferred HotWalletKeyMissing notification), add set_wallet_type API, reconcile/upgrade watch-only wallets on import, and add wallet_matches_fingerprint helper.
Rust: API & DB errors
rust/src/app.rs, rust/src/database/error.rs
Added FFI-exposed set_wallet_type(id, wallet_type) and new DatabaseError::WalletNotFound.
Android UI & wiring
MainActivity / WalletManager / Flows
android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt, android/app/src/main/java/org/bitcoinppl/cove/WalletManager.kt, android/app/src/main/java/org/bitcoinppl/cove/flows/.../WalletSettingsScreen.kt
Global dialog branches and navigation for new alert variants and watch-only import flows; WalletManager surfaces HotWalletKeyMissing; debug-only "Simulate Missing Key" added to settings.
iOS UI & wiring
ios/Cove/CoveApp.swift, ios/Cove/WalletManager.swift, ios/Cove/Flows/.../HotWalletImportScreen.swift, ios/Cove/Flows/.../WalletSettingsView.swift, ios/Cove/TaggedItem.swift
Added hot-wallet-key-missing and watch-only import alerts/actions, missing-metadata handling in import flow, changed TaggedItem equality to compare only id, and debug simulate-missing-key UI (DEBUG only).
FFI/Serialization expansion (many files)
android/.../cove_core/..., rust/src/...
Broad expansion of enum variants (WalletType, WalletColor, WalletAddressType, WordCheckState, SecurityAlertState, UrType, etc.) with corresponding allocation/read/write updates across FFI bindings.
Build/config updates
android/app/build.gradle.kts, android/build.gradle.kts, android/gradle/wrapper/gradle-wrapper.properties, android/gradlew*
Gradle and dependency version bumps, removed Kotlin plugin from top-level plugins block, updated wrapper to Gradle 9.3.1 and plugin versions; small script tweaks.
Backups & security
android/app/src/main/res/xml/backup_rules.xml, android/app/src/main/res/xml/data_extraction_rules.xml, ios/Cove/Security.swift
Exclude cove_secure_storage.xml from backups/data-transfer; Keychain save uses .accessibleWhenUnlockedThisDeviceOnly.
Misc Android edits
android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt, android/app/src/main/java/org/bitcoinppl/cove/flows/.../HotWalletImportScreen.kt
Added AppManager.clearWalletManager(), call to clear wallet manager after import, and warning logs when importing words for an existing hot wallet.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Mobile UI
    participant App as App Manager
    participant Keychain
    participant DB as Database
    participant Rust as FFI/Rust

    User->>UI: Open wallet
    UI->>App: Load wallet
    App->>DB: Fetch wallet metadata
    DB-->>App: Metadata (type=Hot)
    App->>Keychain: Retrieve private key
    alt Key present
        Keychain-->>App: Key found
        App-->>UI: Wallet usable
    else Key missing
        Keychain-->>App: Key not found
        App->>Rust: downgrade_and_notify_if_needed
        Rust->>DB: Update metadata -> WatchOnly (persist)
        DB-->>Rust: Persisted
        Rust-->>App: Queue HotWalletKeyMissing
        App-->>UI: Emit HotWalletKeyMissing alert
        UI->>User: Show import/watch-only options
        User->>UI: Choose import method (Words/Hardware/QR/NFC/Paste)
        UI->>Rust: Trigger import via FFI
        Rust->>Keychain: Save private key / descriptors
        Rust->>DB: Update metadata (Hot/Cold) & persist
        DB-->>Rust: Persisted
        Rust-->>App: Notify wallet restored
        App-->>UI: Update UI (wallet restored)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

android

"🐰 I hopped through keys and dialogs bright,
Downgraded a hot one into watch-only light,
QR, NFC, words — import on the run,
Xpubs and descriptors, reconciliation done,
Tiny rabbit cheers — wallets hum in delight!"

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes several out-of-scope changes: Gradle/build configuration updates (AGP 8.13.1→9.0.1, Gradle 8.13→9.3.1, dependency version bumps) and backup/data extraction rules modifications unrelated to missing key detection and re-import flows. Move build configuration and backup rule changes to separate PRs focused on dependency upgrades and configuration hardening, keeping this PR focused on the missing key detection feature.
Docstring Coverage ⚠️ Warning Docstring coverage is 29.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Implement CSPP phase 1 items' is vague and generic, using non-descriptive terms that don't convey the specific changes. Consider a more specific title describing the main feature, such as 'Detect missing hot wallet keys and enable re-import' or 'Add missing private key detection and watch-only downgrade flow'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All coding requirements from issues #554, #555, and #556 are addressed: hot wallet key detection with downgrade logic, platform-specific alerts with re-import options, and seed word validation with keychain restoration.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cspp-phase-1

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.

@greptile-apps
Copy link

greptile-apps bot commented Feb 13, 2026

Greptile Summary

This PR implements CSPP (Cove Security & Privacy Protocol) phase 1, adding wallet type upgrade/downgrade paths, duplicate wallet reconciliation, and backup exclusion for sensitive data.

  • Hot wallet key recovery: When a hot wallet's private key is missing from the keychain (e.g., after restoring from backup), the wallet is automatically downgraded to watch-only with a user-facing alert offering options to re-import keys, use as hardware wallet, or keep as watch-only.
  • Wallet upgrade paths: Watch-only wallets can now be upgraded to Cold (via hardware export) or Hot (via mnemonic import) in-place, reusing the existing wallet ID and preserving transaction history.
  • Watch-only send guard: The send button is visually dimmed and shows an actionable alert with import options instead of a dead-end error message.
  • Backup security: iOS keychain entries now use .accessibleWhenUnlockedThisDeviceOnly, and Android excludes cove_secure_storage.xml from cloud and device-transfer backups.
  • Android build toolchain: Upgrades to AGP 9.0.1, Gradle 9.3.1, Kotlin Compose 2.3.10, and various AndroidX/library bumps.
  • ClearCachedWalletManager: New reconcile message invalidates stale wallet managers after in-place wallet type changes; both Android and iOS now consistently check the wallet ID before clearing.

Confidence Score: 4/5

  • This PR is safe to merge with low risk; the core wallet upgrade/downgrade logic is well-structured with proper error propagation and the security improvements are sound.
  • The wallet type upgrade/downgrade paths are logically correct with proper guards (e.g., upgrade_to_cold checks for WatchOnly before upgrading, Hot wallet import checks for existing key before overwriting). The set_wallet_type method now follows DB-first pattern preventing stale in-memory state. Security improvements (keychain access level, backup exclusions) are appropriate. Minor concerns include non-atomic keychain writes during upgrade and redundant setWalletType calls on both platforms after upgrade_to_cold, but these are harmless in practice.
  • rust/src/manager/import_wallet_manager.rs (multi-step keychain writes without rollback), rust/src/manager/wallet_manager.rs (new downgrade logic and DeferredSender integration)

Important Files Changed

Filename Overview
rust/src/manager/wallet_manager.rs Adds set_wallet_type method (DB-first, then in-memory update), downgrade_and_notify_if_needed function for hot wallets missing keys, DeferredSender usage, and HotWalletKeyMissing reconcile message. Logic is sound with proper error propagation.
rust/src/manager/import_wallet_manager.rs Rewrites mnemonic import to handle existing wallets: Hot wallets with missing keys get restored, Hot wallets with keys show duplicate alert, other types (Cold/WatchOnly/XpubOnly) upgrade to Hot. Adds MissingMetadata error variant (defined but never constructed).
rust/src/wallet.rs Adds upgrade_to_cold helper to convert WatchOnly wallets to Cold during pubport import. Checks wallet type before upgrade. Sends ClearCachedWalletManager to invalidate stale wallet managers.
rust/src/app/alert_state.rs Adds WatchOnlyImportHardware and WatchOnlyImportWords alert states, expands HotWalletKeyMissing message with platform-specific backup explanation, and updates CantSendOnWatchOnlyWallet message to be more actionable.
ios/Cove/CoveApp.swift Adds alert button handlers for HotWalletKeyMissing, ConfirmWatchOnly, CantSendOnWatchOnlyWallet, WatchOnlyImportHardware, and WatchOnlyImportWords. Updates importColdWallet with in-place upgrade logic and importHotWallet for wallet manager cache invalidation.
ios/Cove/Security.swift Adds .accessibleWhenUnlockedThisDeviceOnly keychain access level to prevent private keys from being included in device backups or transfers.
android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt Adds alert dialog handlers for HotWalletKeyMissing, ConfirmWatchOnly, CantSendOnWatchOnlyWallet, WatchOnlyImportHardware, and WatchOnlyImportWords. Provides wallet import/upgrade/downgrade options matching iOS parity.
android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt Adds clearWalletManager(), ClearCachedWalletManager reconcile handler (now matching iOS ID check), and cold wallet import upgrade logic with error handling for setWalletType.
android/app/src/main/res/xml/backup_rules.xml Excludes cove_secure_storage.xml from Android full backups to prevent private key material from leaking through device backups.
android/app/src/main/res/xml/data_extraction_rules.xml Excludes cove_secure_storage.xml from both cloud backup and device-to-device transfer, preventing private key leakage through Android backup mechanisms.

Flowchart

flowchart TD
    A[Wallet Load / Import] --> B{Wallet exists<br/>with same fingerprint?}
    B -->|No| C[Create new wallet]
    B -->|Yes| D{Current wallet type?}

    D -->|Hot + key present| E[Show Duplicate Alert]
    D -->|Hot + key missing| F[Restore key from mnemonic<br/>Keep as Hot]
    D -->|WatchOnly| G{Import source?}
    D -->|Cold / XpubOnly| H{Import source?}

    G -->|Mnemonic| I[Save key + descriptors<br/>Upgrade to Hot]
    G -->|Pubport/Xpub| J[Save xpub + descriptors<br/>Upgrade to Cold]

    H -->|Mnemonic| I
    H -->|Pubport/Xpub| K{Type is WatchOnly?}
    K -->|Yes| J
    K -->|No| E

    subgraph "On Wallet Manager Init"
        L[Load wallet] --> M{Hot wallet?}
        M -->|No| N[Continue normally]
        M -->|Yes| O{Private key in keychain?}
        O -->|Yes| N
        O -->|No| P[Downgrade to WatchOnly<br/>Send HotWalletKeyMissing alert]
    end

    P --> Q[User sees recovery options]
    Q --> R[Import 12/24 words]
    Q --> S[Use with Hardware Wallet]
    Q --> T[Keep as Watch Only]
Loading

Last reviewed commit: e34647b

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

14 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Copy link

@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: 7

Caution

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

⚠️ Outside diff range comments (3)
rust/src/wallet.rs (1)

266-270: ⚠️ Potential issue | 🟠 Major

Orphaned SQLite database files created on watch-only → cold upgrade path.

BdkStore::try_new(&id, network) on line 269 creates a persistent SQLite database file (bdk_wallet_sqlite_{id}.db). When the upgrade path is taken (lines 312-333), this function returns early without using the store, leaving the SQLite file and its auxiliary WAL/SHM files orphaned on disk. These files accumulate on repeated imports of hardware wallets matching existing watch-only wallets.

Move the BdkStore creation after the fingerprint/upgrade check, or explicitly delete the orphaned store files before returning from the upgrade path using BdkStore::delete_sqlite_store().

rust/src/manager/import_wallet_manager.rs (1)

112-124: ⚠️ Potential issue | 🟠 Major

Fingerprint lookup mechanisms differ: inconsistent sources could cause duplicate wallets.

Fingerprint::try_new() in import_wallet_manager.rs (line 119) reads from the keychain via Keychain::global().get_wallet_xpub(), while wallet.rs (line 305) reads directly from wallet_metadata.master_fingerprint. If a wallet's xpub is removed or unavailable in the keychain but the metadata fingerprint persists (e.g., due to corruption or migration), the import flow will not find the existing wallet and create a duplicate instead of upgrading it. The lookup inconsistency compounds this risk—Fingerprint::try_new() silently fails with .ok()?, filtering out wallets that metadata-based checks would find.

ios/Cove/CoveApp.swift (1)

226-247: ⚠️ Potential issue | 🟡 Minor

Dead cases: .hotWalletKeyMissing and .confirmWatchOnly are already handled above.

Lines 242–243 list .hotWalletKeyMissing and .confirmWatchOnly in the default "OK" group, but they are already matched by the specific case let .hotWalletKeyMissing(walletId:) at line 76 and case .confirmWatchOnly at line 95. These entries in the default group are unreachable dead code. SwiftLint also flags this as duplicate_conditions.

Remove them from the grouped default to silence the warning and avoid confusion:

Proposed fix
         case .invalidWordGroup,
              .errorImportingHotWallet,
              .importedSuccessfully,
              .unableToSelectWallet,
              .errorImportingHardwareWallet,
              .invalidFileFormat,
              .importedLabelsSuccessfully,
              .unableToGetAddress,
              .failedToScanQr,
              .noUnsignedTransactionFound,
              .tapSignerSetupFailed,
              .tapSignerInvalidAuth,
              .tapSignerDeriveFailed,
              .general,
              .invalidFormat,
-             .loading,
-             .hotWalletKeyMissing,
-             .confirmWatchOnly:
+             .loading:
             Button("OK") {
                 app.alertState = .none
             }
🤖 Fix all issues with AI agents
In `@android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt`:
- Around line 420-452: In the AppAlertState.HotWalletKeyMissing handler, don’t
silently swallow exceptions from app.rust.setWalletType(walletId,
WalletType.COLD); instead catch the exception and either log it (using the same
logging approach used elsewhere in this file) and/or set app.alertState to an
error alert (e.g., TaggedItem(AppAlertState.SomeErrorState) or a toast) so the
user is informed that setWalletType failed; update the try/catch around
app.rust.setWalletType to capture the exception, log the error details, and
surface a user-facing alert via app.alertState rather than leaving the dialog
closed with no feedback.

In `@ios/Cove/CoveApp.swift`:
- Around line 87-90: The Button's action uses try? with
app.rust.setWalletType(id: walletId, walletType: .cold) which silently ignores
failures; replace the silent try? with a do-catch (or use Result) to catch
errors from setWalletType and surface them via app.alertState (e.g., set an
error alert message) and only clear app.alertState on success; locate the Button
action where app.alertState is set and update it to handle the thrown error from
app.rust.setWalletType and present a user-facing error alert.
- Around line 190-202: The Paste button handler currently creates a Wallet via
Wallet.newFromXpub and calls app.rust.selectWallet(id: wallet.id()) but does not
navigate to the newly selected wallet; update the Button action so that after a
successful selectWallet call it also calls
app.resetRoute(.SelectedWallet(wallet.id())) (or the
app.resetRoute(Route.SelectedWallet(id)) equivalent used elsewhere) so the UI
navigates to the imported wallet; keep the existing error handling around
Wallet.newFromXpub and selectWallet.

In `@rust/src/manager/import_wallet_manager.rs`:
- Around line 137-140: The current lookup uses
Database::global().wallets.get(&id, network, mode)? and maps a None to
ImportWalletError::WalletAlreadyExists(id.clone()), which is misleading when the
fingerprint exists but metadata is missing; change this to return a more
accurate error (e.g. add/import ImportWalletError::MissingMetadata(id.clone())
or reuse an appropriate existing variant) and replace the
None-to-WalletAlreadyExists mapping in the import flow (the get call handling in
import_wallet_manager.rs) so callers receive the correct MissingMetadata error;
update any matching/handling sites that expect WalletAlreadyExists accordingly.

In `@rust/src/manager/wallet_manager.rs`:
- Around line 261-278: The current sanity check comparing wallet.metadata with
Database::global().wallets.get(...) can fail if downgrade_and_notify_if_needed
mutates wallet.metadata in-memory then fails to persist; update the flow so the
DB is authoritative: either change downgrade_and_notify_if_needed to return a
Result and propagate persistence errors up from Wallet::try_load_persisted (so
the in-memory mutation only happens when the DB write succeeded), or move the
metadata sanity check (the comparison after Database::global().wallets.get) to
run before calling downgrade_and_notify_if_needed, or re-read the wallet
metadata from Database::global().wallets.get after
downgrade_and_notify_if_needed completes and use that value for the equality
check; reference the functions Wallet::try_load_persisted,
downgrade_and_notify_if_needed, and Database::global().wallets.get when making
the change.
- Around line 1363-1370: The current match on
Keychain::global().get_wallet_key(...) treats any Err as "no key" and sets
has_private_key = false, which can permanently downgrade wallets on transient
keychain failures; update the logic around
Keychain::global().get_wallet_key(&metadata.id) (the match that sets
has_private_key) to distinguish transient keychain errors from a definitive
Ok(None): inspect the returned Err (by error kind, variant, or an
is_locked/is_unavailable helper on the keychain error type), and for transient
errors either propagate the error (return Err from the surrounding function),
schedule/retry the lookup, or leave the wallet in its prior state instead of
setting has_private_key = false; keep the warn log but include the error and use
a different handling path than the Ok(None) branch so only Ok(None) triggers
permanent downgrade to watch-only.

In `@rust/src/wallet.rs`:
- Around line 298-310: The code is silently discarding database errors by
calling .unwrap_or(None) on the Result from database.wallets.get_all; replace
that swallow with proper error propagation so real DB errors are returned
instead of treated as "no wallets". Locate the block that builds existing using
database.wallets.get_all(network, mode) and remove the .unwrap_or(None), instead
propagate the Result (e.g., use ? on the get_all call or map the Err out) and
then perform the filter_map over the successful wallets to set existing; ensure
the containing function's signature permits returning the propagated error so
database failures are not masked.
🧹 Nitpick comments (4)
rust/src/wallet.rs (1)

312-333: No cleanup on partial failure during watch-only → cold upgrade.

The upgrade path performs multiple fallible operations (save xpub, set wallet_type, save descriptors, update metadata, select wallet, load wallet). If any step after save_wallet_xpub fails, previously persisted changes are not rolled back. This mirrors the existing pattern in try_new_persisted_and_selected which does have cleanup logic (lines 177-198).

Consider wrapping this in a similar cleanup-on-failure pattern, or at least documenting that partial state is acceptable here.

rust/src/manager/import_wallet_manager.rs (1)

126-157: No rollback on partial failure during wallet reconciliation.

This multi-step reconciliation (save key → save xpub → save descriptors → update metadata → select wallet) has no cleanup if an intermediate step fails. For example, if save_public_descriptor (line 144) fails after the mnemonic was already saved (line 131), the keychain will contain the private key but the wallet won't be updated to Hot type, leaving inconsistent state.

Consider adding cleanup logic similar to Wallet::try_new_persisted_and_selected in wallet.rs.

rust/src/app.rs (1)

422-441: No validation on wallet type transitions.

set_wallet_type allows setting any WalletType (including Hot) without verifying that the prerequisite state exists (e.g., private key in the keychain). A frontend bug or misuse could set a wallet to Hot when no mnemonic is available, causing send-flow failures that are harder to diagnose than an upfront error.

Consider adding a guard, at minimum for the Hot transition:

Suggested validation
+        if wallet_type == WalletType::Hot {
+            let has_key = Keychain::global().get_wallet_key(&id).ok().flatten().is_some();
+            if !has_key {
+                return Err(DatabaseError::WalletNotFound); // or a more specific error
+            }
+        }
+
         metadata.wallet_type = wallet_type;
rust/src/app/alert_state.rs (1)

109-122: OS-specific backup messaging looks good.

The cfg(target_os) approach for tailoring the backup-type wording is clean. Minor nit: on Android the backup system is typically called "Google backup" rather than just "Android" — consider whether "Google" or "Android Auto Backup" would be clearer to end users.

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
ios/Cove/CoveApp.swift (1)

228-248: ⚠️ Potential issue | 🟡 Minor

Dead patterns: .hotWalletKeyMissing and .confirmWatchOnly are already handled above.

These two variants are matched by dedicated case arms at lines 76 and 95, so they are unreachable here. SwiftLint also flags this as duplicate_conditions. Remove them from this group.

Proposed fix
         case .invalidWordGroup,
              .errorImportingHotWallet,
              .importedSuccessfully,
              .unableToSelectWallet,
              .errorImportingHardwareWallet,
              .invalidFileFormat,
              .importedLabelsSuccessfully,
              .unableToGetAddress,
              .failedToScanQr,
              .noUnsignedTransactionFound,
              .tapSignerSetupFailed,
              .tapSignerInvalidAuth,
              .tapSignerDeriveFailed,
              .general,
              .invalidFormat,
-             .loading,
-             .hotWalletKeyMissing,
-             .confirmWatchOnly:
+             .loading:
             Button("OK") {
                 app.alertState = .none
             }
🤖 Fix all issues with AI agents
In `@android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt`:
- Around line 741-762: The Wallet returned by Wallet.newFromXpub is
AutoCloseable and must be closed after use to avoid UniFFI resource leaks; wrap
creation and use in a Kotlin use block (or ensure wallet.close() is called) so
you obtain the id via the Wallet instance inside the block and then call
app.rust.selectWallet(id) and app.resetRoute(Route.SelectedWallet(id)) before
the wallet is closed; update the Paste button handler to use
Wallet.newFromXpub(...) with wallet.use { ... } (or explicit close) and preserve
the existing error handling via the catch block.

In `@ios/Cove/CoveApp.swift`:
- Around line 95-98: The confirmWatchOnly alert's "I Understand" button only
clears app.alertState and never persists the wallet type change; update the
Button handler in the case .confirmWatchOnly (the Button("I Understand")
closure) to call app.setWalletType(id: walletId, walletType: .watchOnly) (or the
app-equivalent API used for persisting wallet type) before setting
app.alertState = .none so the wallet is actually converted to watch-only and
then the alert dismissed.

@praveenperera
Copy link
Contributor Author

@greptile-apps re-review

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

14 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@praveenperera
Copy link
Contributor Author

@greptile-apps re-review

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link

@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 (3)
rust/src/wallet.rs (2)

266-269: ⚠️ Potential issue | 🟠 Major

Orphaned BDK store created when taking the WatchOnly → Cold upgrade path.

When the upgrade branch (line 308–328) is taken, the BdkStore created on line 269 for the new id is never used and its SQLite file is left on disk. Each re-import of a matching pubport export leaks an orphaned file.

Move BdkStore::try_new (and the new WalletId) below the upgrade check so they are only allocated when a new wallet is actually needed, or clean up the unused store in the early-return path.

Suggested restructure (move store creation after upgrade check)
         let pubport_descriptors = match pubport {
             // ... (match arms unchanged)
         };
 
         let fingerprint = pubport_descriptors.fingerprint();
 
         // compute xpub and descriptors early
         let xpub = pubport_descriptors.xpub()...;
         let descriptors: Descriptors = pubport_descriptors.into();
 
-        let id = WalletId::new();
-        let mut metadata = WalletMetadata::new_for_hardware(id.clone(), "", None);
-        let mut store = BdkStore::try_new(&id, network).map_err_str(WalletError::LoadError)?;
-
         // check for existing wallet with same fingerprint, upgrade watch-only → cold
         if let Some(fingerprint) = fingerprint.as_ref() {
             // ... upgrade path (unchanged) ...
         }
 
+        let id = WalletId::new();
+        let mut metadata = WalletMetadata::new_for_hardware(id.clone(), "", None);
+        let mut store = BdkStore::try_new(&id, network).map_err_str(WalletError::LoadError)?;
+
         // ... rest of new-wallet creation ...

Note: metadata is also mutated inside the fingerprint block (line 296), so you would need to split the metadata initialization or pass the fingerprint into the block differently. But the store allocation is the main concern.

#!/bin/bash
# Verify that `metadata` fields set inside the fingerprint block are only used in the upgrade path
# or also needed for the new-wallet path, to assess how to restructure.
rg -n 'metadata\.' rust/src/wallet.rs | head -60

Also applies to: 308-329


663-687: ⚠️ Potential issue | 🟠 Major

check_for_duplicate_wallet still silently swallows database errors.

The same .unwrap_or_default() pattern that was just fixed in try_new_persisted_from_pubport (line 300) persists here on line 680. A DB failure is treated as "no wallets found," allowing a duplicate wallet to be created silently.

Proposed fix: propagate the error
 fn check_for_duplicate_wallet(
     network: Network,
     mode: metadata::WalletMode,
     fingerprint: Fingerprint,
 ) -> Result<(), WalletError> {
     let all_fingerprints: Vec<(WalletId, Arc<Fingerprint>)> = Database::global()
         .wallets
-        .get_all(network, mode)
-        .map(|wallets| {
-            wallets
-                .into_iter()
-                .filter_map(|wallet_metadata| {
-                    let fingerprint = wallet_metadata.master_fingerprint?;
-                    Some((wallet_metadata.id, fingerprint))
-                })
-                .collect()
-        })
-        .unwrap_or_default();
+        .get_all(network, mode)?
+        .into_iter()
+        .filter_map(|wallet_metadata| {
+            let fingerprint = wallet_metadata.master_fingerprint?;
+            Some((wallet_metadata.id, fingerprint))
+        })
+        .collect();
 
     if let Some((id, _)) = all_fingerprints.into_iter().find(|(_, f)| f.as_ref() == &fingerprint) {
         return Err(WalletError::WalletAlreadyExists(id));
     }
 
     Ok(())
 }
ios/Cove/CoveApp.swift (1)

239-259: ⚠️ Potential issue | 🟡 Minor

Duplicate switch cases: .hotWalletKeyMissing and .confirmWatchOnly are unreachable here.

Lines 255–256 duplicate cases already handled at Lines 76 and 105 respectively. Swift matches cases top-to-bottom, so these entries are dead code and the generic "OK" button will never be shown for these states. This is also what SwiftLint flagged (duplicate_conditions).

Remove both from the catch-all group:

Proposed fix
         case .invalidWordGroup,
              .errorImportingHotWallet,
              .importedSuccessfully,
              .unableToSelectWallet,
              .errorImportingHardwareWallet,
              .invalidFileFormat,
              .importedLabelsSuccessfully,
              .unableToGetAddress,
              .failedToScanQr,
              .noUnsignedTransactionFound,
              .tapSignerSetupFailed,
              .tapSignerInvalidAuth,
              .tapSignerDeriveFailed,
              .general,
              .invalidFormat,
-             .loading,
-             .hotWalletKeyMissing,
-             .confirmWatchOnly:
+             .loading:
             Button("OK") {
                 app.alertState = .none
             }
🧹 Nitpick comments (2)
ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportScreen.swift (1)

228-229: MissingMetadata error is silently swallowed — user sees no feedback.

Like several other branches here (.Keychain, .Database, .BdkError), this only logs the error. For a "wallet metadata missing" scenario — which indicates data corruption or loss — the user should arguably be informed so they can take action (e.g., re-import). Consider surfacing this via an alert, at least for this specific case.

Proposed improvement
             case let .MissingMetadata(walletId):
-                Log.error("Wallet metadata missing for \(walletId)")
+                Log.error("Wallet metadata missing for \(walletId)")
+                app.alertState = TaggedItem(.general(
+                    title: "Import Error",
+                    message: "Wallet metadata is missing. Please try importing again."
+                ))
ios/Cove/CoveApp.swift (1)

200-215: Nit: wallet.id() called twice — store in a local.

wallet.id() is called on both Line 206 and Line 207. While likely inexpensive, extracting it to a local let is cleaner and consistent with how importColdWallet (Line 329) handles this.

Proposed fix
             Button("Paste") {
                 app.alertState = .none
                 let text = UIPasteboard.general.string ?? ""
                 if text.isEmpty { return }
                 do {
                     let wallet = try Wallet.newFromXpub(xpub: text)
-                    try app.rust.selectWallet(id: wallet.id())
-                    app.resetRoute(to: .selectedWallet(wallet.id()))
+                    let id = wallet.id()
+                    try app.rust.selectWallet(id: id)
+                    app.resetRoute(to: .selectedWallet(id))
                 } catch {

Copy link

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
ios/Cove/CoveApp.swift (1)

239-259: ⚠️ Potential issue | 🟡 Minor

Duplicate switch cases: hotWalletKeyMissing and confirmWatchOnly are handled twice.

Lines 76–108 already handle .hotWalletKeyMissing and .confirmWatchOnly with specific buttons. Including them again in the catch-all at lines 255–256 creates dead code branches that SwiftLint flags as duplicate_conditions. Remove them from the catch-all group.

Proposed fix
         case .invalidWordGroup,
              .errorImportingHotWallet,
              .importedSuccessfully,
              .unableToSelectWallet,
              .errorImportingHardwareWallet,
              .invalidFileFormat,
              .importedLabelsSuccessfully,
              .unableToGetAddress,
              .failedToScanQr,
              .noUnsignedTransactionFound,
              .tapSignerSetupFailed,
              .tapSignerInvalidAuth,
              .tapSignerDeriveFailed,
              .general,
              .invalidFormat,
-             .loading,
-             .hotWalletKeyMissing,
-             .confirmWatchOnly:
+             .loading:
             Button("OK") {
                 app.alertState = .none
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/CoveApp.swift` around lines 239 - 259, The switch contains duplicate
cases: remove .hotWalletKeyMissing and .confirmWatchOnly from the catch-all case
group so the earlier specific handlers remain effective; locate the switch
handling app.alertState in CoveApp.swift and delete those two enum cases from
the grouped list (leaving the Button("OK") fallback for the remaining cases) to
eliminate the duplicate_conditions lint warning.
🧹 Nitpick comments (3)
ios/Cove/TaggedItem.swift (1)

18-25: Identity-only equality — reasonable trade-off, but document the intent.

Removing the T: Equatable constraint is a pragmatic choice to support non-Equatable payloads (e.g., FFI-generated alert state types). The custom == comparing only id means two independently created TaggedItems wrapping the same value will never be equal, since each gets a fresh UUID.

This is fine for the primary use case (SwiftUI optional-binding for sheets/alerts), but could surprise anyone who later uses TaggedItem in collections (Set, Dictionary key) or value-based equality checks. A brief doc comment on the == override would help future readers understand the deliberate semantics.

📝 Suggested doc comment
 struct TaggedItem<T>: Identifiable, Equatable {
     let id = UUID()
     let item: T
 
+    /// Equality is based solely on `id` (instance identity) rather than the
+    /// wrapped `item`, because `T` is not constrained to `Equatable`.
     static func == (lhs: TaggedItem, rhs: TaggedItem) -> Bool {
         lhs.id == rhs.id
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/TaggedItem.swift` around lines 18 - 25, The equality implementation
for TaggedItem intentionally compares only the generated id (UUID) so two
instances wrapping the same payload are not equal; add a short doc comment on
the TaggedItem type or the static func == (lhs:rhs:) explaining that T is not
constrained to Equatable, equality is identity-only (uses id), and this is
deliberate for SwiftUI sheet/alert usage to avoid value-based equality surprises
when used in Sets/Dictionaries or value comparisons; reference TaggedItem, id,
item, and the custom == implementation in your comment.
android/app/build.gradle.kts (1)

84-84: Duplicate lifecycle-runtime-ktx dependency.

androidx.lifecycle:lifecycle-runtime-ktx:2.10.0 is declared on both Line 84 and Line 107. Remove one to avoid confusion.

Proposed fix
-    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
     implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
+    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")

i.e., remove Line 84 and keep the one at Line 107 grouped with other lifecycle dependencies.

Also applies to: 107-107

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

In `@android/app/build.gradle.kts` at line 84, The Gradle file declares a
duplicate dependency
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0"); remove the
redundant declaration so the artifact is only declared once (keep the entry that
is grouped with the other lifecycle dependencies), i.e., delete the extra
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") occurrence and
ensure only the grouped lifecycle declarations (including
lifecycle-runtime-ktx:2.10.0) remain.
rust/src/manager/wallet_manager.rs (1)

914-927: set_wallet_type silently swallows DB persistence failures.

The method logs the error but does not return it. If the DB write fails, the in-memory cache and reconciler reflect the new type, but the database retains the old type. On next wallet load, the wallet reverts to the old type. This is consistent with the pattern used in dispatch (line 1284) and validate_metadata (line 950), so it's an existing design choice — just noting it for awareness.

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

In `@rust/src/manager/wallet_manager.rs` around lines 914 - 927, set_wallet_type
currently applies the new WalletType to the in-memory metadata and sends
Message::WalletMetadataChanged before attempting
Database::global().wallets.update_wallet_metadata, which causes DB failures to
be silently inconsistent; change set_wallet_type to return a Result (e.g.,
Result<(), WalletManagerError>), attempt the DB update first (call
update_wallet_metadata with the new metadata), and only on Ok update the
in-memory metadata and call
self.reconciler.send(Message::WalletMetadataChanged(...)); on DB error return
Err(error) (or map it to your manager error type) so callers can handle
persistence failures—refer to set_wallet_type,
Database::global().wallets.update_wallet_metadata,
self.reconciler.send(Message::WalletMetadataChanged) and follow the error-return
pattern used by dispatch and validate_metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt`:
- Around line 420-458: The HotWalletKeyMissing AlertDialog
(AppAlertState.HotWalletKeyMissing) lacks an explicit dismiss/cancel button; add
a dismissButton parameter to the AlertDialog (alongside the existing
confirmButton) that renders a TextButton (e.g., labeled "Cancel" or "Dismiss")
which calls onDismiss() when clicked, mirroring the pattern used by
CantSendOnWatchOnlyWallet and improving discoverability/accessibility.

In `@android/build.gradle.kts`:
- Around line 3-4: The AGP version string for the Android Gradle plugin is
incorrect—replace the version value in the id("com.android.application")
declaration from "9.0.1" to a valid release (e.g., "9.0.0") or change it to the
intended pre-release like "9.1.0-alpha" if that was desired; update only the
version literal in the id("com.android.application") declaration and then
re-sync/verify the build to ensure compatibility with the Kotlin Compose plugin
already set to "2.3.10".

In `@ios/Cove/Flows/NewWalletFlow/HotWallet/HotWalletImportScreen.swift`:
- Around line 229-230: The .MissingMetadata(walletId) branch currently only logs
the error; update it to also surface a user-facing alert like the other error
branches (Keychain, Database) so the user knows the import failed — keep the
Log.error call but also call the existing UI alert helper used elsewhere in
HotWalletImportScreen (e.g., presentErrorAlert / showImportFailureAlert or the
same method used in the Keychain/Database cases) with a brief message such as
"Import failed: wallet metadata missing", and ensure the alert is presented on
the main thread.

In `@rust/src/app.rs`:
- Around line 422-441: FfiApp::set_wallet_type currently updates DB but leaves
RustWalletManager's in-memory cache stale; update this method to either delegate
to the existing RustWalletManager::set_wallet_type (or equivalent manager
instance) so the manager updates its cached WalletMetadata and triggers the
reconciler/WalletMetadataChanged event, or after persisting call into
RustWalletManager to refresh its metadata and emit the change; ensure you
reference and update the manager instance used by the app (RustWalletManager)
rather than only mutating the DB so the UI sees the new WalletType immediately.

In `@rust/src/manager/import_wallet_manager.rs`:
- Around line 117-123: The code is swallowing DB errors by calling
unwrap_or_default() on Database::global().wallets.get_all, which treats failures
as "no wallets" and can create duplicate wallets; change the call to propagate
the error instead (remove unwrap_or_default and use the Result from get_all,
e.g. return Err or use the ? operator) so import flow can fail fast and
reconcile existing entries; update the surrounding function signature to return
a Result if needed and ensure wallet matching still uses
wallet_metadata.wallet_matches_fingerprint(fingerprint) on the successful
get_all result.
- Around line 67-68: Remove the dead error variant MissingMetadata(WalletId)
from the error enum in import_wallet_manager.rs: delete the enum variant
declaration and any associated use/import of WalletId that only existed for this
variant, and update any pattern matches or references to this variant (if
present) so the enum remains exhaustive; ensure Cargo builds and run tests to
confirm no other code constructs this error.

---

Outside diff comments:
In `@ios/Cove/CoveApp.swift`:
- Around line 239-259: The switch contains duplicate cases: remove
.hotWalletKeyMissing and .confirmWatchOnly from the catch-all case group so the
earlier specific handlers remain effective; locate the switch handling
app.alertState in CoveApp.swift and delete those two enum cases from the grouped
list (leaving the Button("OK") fallback for the remaining cases) to eliminate
the duplicate_conditions lint warning.

---

Duplicate comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt`:
- Around line 756-767: The Wallet returned by Wallet.newFromXpub(...) is an
AutoCloseable UniFFI resource that's not closed; wrap its usage to ensure
deterministic cleanup (either use Kotlin's use { ... } or a try/finally that
calls wallet.close()). Specifically, obtain the Wallet from
Wallet.newFromXpub(text.trim()), call wallet.id() inside the scoped block, then
call app.rust.selectWallet(id) and app.resetRoute(Route.SelectedWallet(id))
before the block exits so wallet.close() runs even if exceptions occur; keep the
existing catch handling for AppAlertState.ErrorImportingHardwareWallet.

---

Nitpick comments:
In `@android/app/build.gradle.kts`:
- Line 84: The Gradle file declares a duplicate dependency
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0"); remove the
redundant declaration so the artifact is only declared once (keep the entry that
is grouped with the other lifecycle dependencies), i.e., delete the extra
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") occurrence and
ensure only the grouped lifecycle declarations (including
lifecycle-runtime-ktx:2.10.0) remain.

In `@ios/Cove/TaggedItem.swift`:
- Around line 18-25: The equality implementation for TaggedItem intentionally
compares only the generated id (UUID) so two instances wrapping the same payload
are not equal; add a short doc comment on the TaggedItem type or the static func
== (lhs:rhs:) explaining that T is not constrained to Equatable, equality is
identity-only (uses id), and this is deliberate for SwiftUI sheet/alert usage to
avoid value-based equality surprises when used in Sets/Dictionaries or value
comparisons; reference TaggedItem, id, item, and the custom == implementation in
your comment.

In `@rust/src/manager/wallet_manager.rs`:
- Around line 914-927: set_wallet_type currently applies the new WalletType to
the in-memory metadata and sends Message::WalletMetadataChanged before
attempting Database::global().wallets.update_wallet_metadata, which causes DB
failures to be silently inconsistent; change set_wallet_type to return a Result
(e.g., Result<(), WalletManagerError>), attempt the DB update first (call
update_wallet_metadata with the new metadata), and only on Ok update the
in-memory metadata and call
self.reconciler.send(Message::WalletMetadataChanged(...)); on DB error return
Err(error) (or map it to your manager error type) so callers can handle
persistence failures—refer to set_wallet_type,
Database::global().wallets.update_wallet_metadata,
self.reconciler.send(Message::WalletMetadataChanged) and follow the error-return
pattern used by dispatch and validate_metadata.

Upgrade Gradle 8.13 → 9.3.1, AGP 8.13.1 → 9.0.1, Kotlin 2.2.10 → 2.3.10.
Remove kotlin-android plugin (AGP 9 has built-in Kotlin support) and stale
composeOptions block. Update dependencies: compose-bom, lifecycle, activity-compose,
jna, accompanist, mlkit, zxing, coil, security-crypto, runtime-livedata.
When a hot wallet's private key is missing from the keychain, automatically
downgrade it to a cold wallet on load. When the user re-imports the same
mnemonic, restore the private key and upgrade the wallet back to hot instead
of returning a "wallet already exists" error.
Move the hot-wallet-downgrade notification into the Rust layer so both
iOS and Android get it automatically via the reconcile message channel,
instead of each frontend querying the DB before/after load.
When importing an xpub for a wallet that already exists as watch-only,
upgrade it to a cold wallet instead of returning WalletAlreadyExists.
Saves xpub, public descriptors, and updates wallet type/origin metadata,
mirroring the existing hot wallet upgrade path for seed word imports.
Watch-only wallets can be upgraded back via xpub or seed word import,
while cold wallets cannot. This makes recovery possible when a hot
wallet loses its private key from the keychain.
When a watch-only wallet tries to send, present options to upgrade it
by importing hardware wallet keys (QR, NFC, paste) or seed words
(QR, NFC, 12/24 words). Adds set_wallet_type FFI method, new
WatchOnlyImportHardware/WatchOnlyImportWords alert states, and
WalletNotFound database error.
Wrap alert-to-alert state changes in DispatchQueue.main.async on iOS to
prevent SwiftUI's dismiss binding from overwriting the new alert state.
Use pushRoute instead of loadAndReset so users can swipe back to the
wallet after starting an import flow, on both iOS and Android.
Surface setWalletType errors to the user on both Android and iOS instead
of silently swallowing them. Add navigation after xpub paste import on
iOS. In Rust, propagate keychain and DB errors from downgrade check
instead of swallowing them, add MissingMetadata error variant for the
correct semantic, and propagate get_all errors in duplicate detection.
If the fingerprint matches a wallet that is already hot, select it and
return WalletAlreadyExists instead of silently overwriting its private
key material with the newly imported mnemonic.
Route "Use with Hardware Wallet" through WalletManager.setWalletType()
instead of FfiApp.setWalletType() so the cache and reconcile channel
are updated. Delete the now-unused FfiApp.setWalletType() method.

Add ClearCachedWalletManager app reconcile message so the frontend
drops its stale WalletManager when a watch-only wallet is upgraded
to cold via xpub import.
When importing a hardware export that matches an existing watch-only
wallet, upgrade the wallet type to cold without navigating away from
the current screen. Also adds a db field comment on Wallet struct.
Screen-level @State alert was racing with the route change from
selectWallet — the UIAlertController dismissal animation overlapped
with the LoadAndResetContainer delay, causing the "Import Successful"
alert to hang on xpub and QR hardware imports. App-level alertState
persists across route changes so the alert shows on the destination
wallet screen instead.
@praveenperera
Copy link
Contributor Author

@greptile-apps re-review

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

39 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 18, 2026

Additional Comments (1)

rust/src/wallet.rs
Orphaned SQLite database file on upgrade path

When the existing-wallet upgrade path is taken (early return at line 333), the BdkStore created here with a new WalletId is dropped but its SQLite file on disk is never cleaned up. BdkStore::try_new calls Connection::open which creates a file at sqlite_data_path(&id). The connection is closed on drop, but the file persists.

Each time a user re-imports a hardware wallet that matches an existing watch-only wallet, a new orphaned .sqlite file accumulates on disk. Consider deferring the BdkStore creation until after the duplicate check, or cleaning up the store in the upgrade branch:

        let id = WalletId::new();
        let mut metadata = WalletMetadata::new_for_hardware(id.clone(), "", None);

Then defer the BdkStore::try_new call to after the fingerprint-matching/upgrade early-return block, just before it's actually needed to create a new wallet.

Only check the wallet to see if fingerprint matches
…nditional cache clear

- Defer BdkStore creation past fingerprint duplicate check so the
  upgrade early-return path no longer orphans a SQLite file on disk
- Make set_wallet_type return Result and write DB before updating
  in-memory state, so failures are surfaced instead of silently reverted
- Check walletId before clearing cached WalletManager on Android,
  matching existing iOS behavior
@praveenperera
Copy link
Contributor Author

@greptile-apps re-review

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

38 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

The hot upgrade path (importing mnemonic into WatchOnly wallet) was
missing the cache invalidation that upgrade_to_cold already had,
leaving a stale WatchOnly manager in memory until the user navigated
away and back.
Add explanatory comments in AppManager on Android and iOS to document that multiple screens within the same wallet (send, coin control, tx details, settings) should call getWalletManager to reuse the cached walletManager and avoid recreating the actor and reconciler. This makes the intended caching behavior clearer in both platforms.
@praveenperera praveenperera merged commit bec9988 into master Feb 18, 2026
9 checks passed
@praveenperera praveenperera deleted the cspp-phase-1 branch February 18, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant