Skip to content

fix: skip unrecognized BIP329 label entries instead of failing the w…#670

Open
Sandipmandal25 wants to merge 8 commits into
bitcoinppl:masterfrom
Sandipmandal25:fix/nunchuck-label-vout-parse
Open

fix: skip unrecognized BIP329 label entries instead of failing the w…#670
Sandipmandal25 wants to merge 8 commits into
bitcoinppl:masterfrom
Sandipmandal25:fix/nunchuck-label-vout-parse

Conversation

@Sandipmandal25

@Sandipmandal25 Sandipmandal25 commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #277

When importing BIP329 labels from Nunchuck, a single entry with an unrecognized ref format (e.g. non-numeric vout) caused the entire import to fail with "Unable to parse file: error parsing vout", resulting in all labels being lost.

parse_labels now processes input line-by-line: valid entries are imported, invalid ones are logged and skipped. The import only fails if every line is unrecognized.

Testing

5 unit tests added covering:

  • all valid
  • mixed valid/invalid
  • all invalid
  • empty input
  • blank lines

Checklist

Summary by CodeRabbit

  • New Features

    • Label import now returns a parsed report (imported vs skipped) and UIs surface partial-import details.
    • Backup import reports now include skipped label counts.
  • Bug Fixes

    • Import parsing tolerates blank lines and skips malformed/unsupported entries instead of failing the whole import.
    • Transaction refresh after imports remains intact.
  • Tests

    • Added tests for partial success, full failure, empty input, and blank-line handling.

Review Change Stack

@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown

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

This PR changes label import to return a LabelParseReport (imported, skipped), switches Rust JSONL parsing to tolerant line-by-line Label deserialization, and surfaces skipped-count results through updated UniFFI, Android, and iOS UI flows so partial-import outcomes are reported.

Changes

Cohort / File(s) Summary
Rust Label Manager & Parsing
rust/src/label_manager.rs, rust/src/multi_format.rs
Added public LabelParseReport { imported, skipped }; changed import, import_labels, import_without_cloud_backup_dirty, and UniFFI export to return LabelParseReport; replaced single-block JSONL parsing with per-line Label deserialization that counts skipped entries and errors when no supported labels parsed; redesigned Bip329Labels to include parse_skipped and added conversions/Deref/AsRef.
Rust Backup Integration
rust/src/backup/import.rs
Adjusted calls to new import API result handling and standardized error mapping using ResultExt::map_err_str, mapping success to unit where needed.
Android FFI & Generated Bindings
android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
UniFFI JNI signatures updated to return a Rust-buffer-backed LabelParseReport; added LabelParseReport data class and FfiConverterTypeLabelParseReport; LabelManager interface/impl methods now return LabelParseReport.
Android UI — Import Flows
android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt, android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt, android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt, android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt
Capture import result and surface partial-success path: added onPartialSuccess(skipped: UInt) callback, conditionally show partial-success snackbars/alerts with singular/plural wording when skipped > 0, and refresh transactions in those paths.
iOS FFI/Core & UI
ios/CoveCore/Sources/CoveCore/CoveCore.swift, ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift, ios/Cove/ScanManager.swift
LabelManager.import now returns LabelParseReport; callers capture result and conditionally show partial-success alerts (reporting skipped count) and trigger transaction refreshes where appropriate.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as Client/UI
    participant Manager as LabelManager
    participant Parser as Line Parser
    participant Storage as Storage/Persistence

    rect rgba(100,149,237,0.5)
    Note over User,Storage: Line-by-line import with report
    User->>UI: provide JSONL / scanned payload
    UI->>Manager: import(jsonl) / importLabels(labels)
    Manager->>Parser: parse each non-empty line into Label
    loop per line
        Parser->>Parser: attempt deserialize
        alt deserializes to supported Label
            Parser->>Manager: collect label
        else unparsable or unsupported
            Parser->>Manager: increment skipped (debug log)
        end
    end
    Parser-->>Manager: (Labels, report {imported, skipped})
    Manager->>Storage: save_imported_labels(parsed supported Labels)
    Storage-->>Manager: ok (returns report)
    Manager-->>UI: LabelParseReport {imported, skipped}
    alt skipped > 0
        UI->>UI: show partial-success alert/snackbar
    else
        UI->>UI: show full-success alert
    end
    UI->>Manager: refresh transactions
    UI-->>User: display feedback
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibble lines one by one,
Bad bits skipped, the good ones run.
Counted hops — imported, skipped — hooray!
Across iOS, Android, Rust we play.
🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main change: skipping unrecognized BIP329 label entries instead of failing, which is the core fix for issue #277.
Description check ✅ Passed The PR description adequately covers the summary of changes, the specific bug being fixed, testing coverage, and completes all checklist items from the template.
Linked Issues check ✅ Passed The code changes comprehensively address issue #277: implement line-by-line BIP329 parsing [rust/src/label_manager.rs, rust/src/multi_format.rs], wire LabelParseReport across Rust/iOS/Android for partial-import reporting [all platforms], ensure all import paths (file/QR/NFC) use tolerant parsing [all platforms], surface partial-import warnings to users [NfcLabelImportSheet.kt, MoreInfoPopover.kt, SelectedWalletScreen.swift, ScanManager.kt, WalletSheets.kt], and validate only supported label variants are counted as imported [label_manager.rs].
Out of Scope Changes check ✅ Passed All changes directly support the core objective of tolerant BIP329 parsing and partial-import reporting. No unrelated refactoring, cleanup, or feature additions are present.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

greptile-apps Bot commented Apr 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a brittle BIP329 label import that failed entirely when a single line couldn't be parsed (e.g. Nunchuck's non-numeric vout format). parse_labels and MultiFormat now process input line-by-line, collecting valid entries and counting skipped ones, and return a new LabelParseReport struct so every UI surface (iOS file picker, iOS/Android QR scan, Android NFC, Android file picker) can surface partial-success feedback to the user.

  • Rust core: parse_labels iterates lines with serde_json, skips malformed/unsupported entries, and only fails when every line is unrecognised; five new unit tests cover all key paths.
  • LabelParseReport (imported: u32, skipped: u32) is returned by import, importLabels, and import_without_cloud_backup_dirty, threaded through UniFFI to both platforms.
  • UI paths: all import entry-points consume the report and show "Could not import N labels" when skipped > 0; a previously missing getTransactions refresh was also added to the iOS scanned-labels path.

Confidence Score: 5/5

Safe to merge — the fix is well-scoped, all import entry-points are updated consistently, and the new unit tests validate the key edge cases.

The Rust core change is straightforward (per-line parsing with a LabelParseReport result), the UniFFI bindings are regenerated correctly, and every UI path that calls the import methods has been updated. Previously flagged issues with partial-success routing (NFC sheet) and silent discard (iOS file picker) are both resolved. The only remaining items are a minor variable-shadowing nit in one Swift file and a missing early-return guard in import_without_cloud_backup_dirty, neither of which affects correctness.

No files require special attention.

Important Files Changed

Filename Overview
rust/src/label_manager.rs Core fix: parse_labels now skips malformed/unsupported lines instead of aborting; all public methods return LabelParseReport; new unit tests cover valid, mixed, all-invalid, empty, and blank-line cases.
rust/src/multi_format.rs Tolerant BIP329 parsing added to MultiFormat::try_from_string; Bip329Labels extended with parse_skipped counter; manual trait impls replace the removed derive macros.
rust/src/backup/import.rs Extracts import_labels_preserve_backup helper; both helpers discard the LabelParseReport (intentional — backup restore has no UI to surface partial-success counts).
ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift File-picker and scanned-labels paths both consume LabelParseReport and surface skip counts; getTransactions task added to the scanned-labels path (was previously missing); minor variable-shadowing issue with result.
ios/Cove/ScanManager.swift Both QR scan paths updated to consume the report and show partial-success alerts; getTransactions task added to the bip329Labels case where it was previously absent.
android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt Adds onPartialSuccess callback so partial imports no longer route through onError; nfcReader.reset() is correctly called before either success branch.
android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt Implements onPartialSuccess lambda: dismisses NFC scanner, refreshes transactions, and shows a snackbar that includes both the skip count and an optional refresh-failure note.
android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt File-picker import path refactored: transaction refresh no longer short-circuits on error, skip count and refresh failure are composed into a single snackbar message.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Raw JSONL input] --> B[parse_labels / MultiFormat tolerant parse]
    B --> C{Any valid lines?}
    C -- No --> D[Err: LabelManagerError::Parse\nfirst error message]
    C -- Yes --> E[LabelParseReport\nimported=N, skipped=M]
    E --> F[save_imported_labels]
    F --> G{skipped > 0?}
    G -- Yes --> H[Show partial-success alert\nCould not import M labels]
    G -- No --> I[Show success alert\nLabels imported successfully]
    H --> J[getTransactions refresh]
    I --> J
Loading

Reviews (4): Last reviewed commit: "fix label noun and preserve skipped coun..." | Re-trigger Greptile

Comment thread rust/src/label_manager.rs Outdated
Comment thread rust/src/label_manager.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
rust/src/label_manager.rs (1)

536-544: Optional: avoid allocating Vec<&str> for lines.

You only need the input count for the summary log. You can stream through lines() with counters instead of collecting, at the cost of slightly more verbose code. Not a blocker.

♻️ Sketch
-    let lines: Vec<&str> = jsonl.trim().lines().filter(|line| !line.trim().is_empty()).collect();
-
-    if lines.is_empty() {
-        return Ok(Labels::new(vec![]));
-    }
-
-    let mut parsed = Vec::with_capacity(lines.len());
-    let mut first_error: Option<String> = None;
-    let mut skipped = 0usize;
-
-    for line in &lines {
+    let mut parsed = Vec::new();
+    let mut first_error: Option<String> = None;
+    let mut skipped = 0usize;
+    let mut total = 0usize;
+
+    for line in jsonl.trim().lines().filter(|l| !l.trim().is_empty()) {
+        total += 1;
         match serde_json::from_str::<Label>(line) {
             Ok(label) => parsed.push(label),
             Err(e) => {
                 if first_error.is_none() {
                     first_error = Some(e.to_string());
                 }
                 warn!("skipping unrecognized label entry: {e}");
                 skipped += 1;
             }
         }
     }
+
+    if total == 0 {
+        return Ok(Labels::new(vec![]));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/label_manager.rs` around lines 536 - 544, The current code allocates
a Vec<&str> from jsonl.trim().lines() only to count and iterate; change to
streaming over jsonl.trim().lines() instead: remove the lines: Vec<&str>
allocation and instead iterate the Lines iterator once, keeping counters
(total_lines, skipped) and filling parsed (Vec) as you go while capturing
first_error; preserve behavior that returns Ok(Labels::new(vec![])) when
total_lines == 0 and keep using parsed, first_error, and skipped variable names
so the summary log can use total_lines for the count. Ensure you still trim/skip
empty lines (line.trim().is_empty()) during the single pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust/src/label_manager.rs`:
- Around line 584-598: The comment and test description misspell the product
name "Nunchuk" as "Nunchuck"; update all occurrences in the comment blocks and
any related string literals or docstrings around the parse_labels tests (e.g.,
the comment above BAD_VOUT_OUTPUT and the test
test_parse_labels_skips_bad_vout_line) to use "Nunchuk" instead, and ensure the
PR description uses the corrected spelling as well so names are consistent.

---

Nitpick comments:
In `@rust/src/label_manager.rs`:
- Around line 536-544: The current code allocates a Vec<&str> from
jsonl.trim().lines() only to count and iterate; change to streaming over
jsonl.trim().lines() instead: remove the lines: Vec<&str> allocation and instead
iterate the Lines iterator once, keeping counters (total_lines, skipped) and
filling parsed (Vec) as you go while capturing first_error; preserve behavior
that returns Ok(Labels::new(vec![])) when total_lines == 0 and keep using
parsed, first_error, and skipped variable names so the summary log can use
total_lines for the count. Ensure you still trim/skip empty lines
(line.trim().is_empty()) during the single pass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0ccbcdaf-b407-4ef7-95db-e5628e13e97c

📥 Commits

Reviewing files that changed from the base of the PR and between 803fc79 and 2eee6e0.

📒 Files selected for processing (1)
  • rust/src/label_manager.rs

Comment thread rust/src/label_manager.rs Outdated
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from 8e73a59 to ff26cc3 Compare April 16, 2026 22:27
@praveenperera

Copy link
Copy Markdown
Contributor

@Sandipmandal25 this looks good thanks, 2 questions before approving

  1. do we know why nunchuk uses this format? is it valid? should we just make ours less strict?
  2. should we report back to the user how many rows could not be imported? maybe even give reasons why?

@Sandipmandal25

Copy link
Copy Markdown
Collaborator Author

@Sandipmandal25 this looks good thanks, 2 questions before approving

  1. do we know why nunchuk uses this format? is it valid? should we just make ours less strict?

we dont have a real nunchuk export to verify the exact format the issue mentioned coin collection labels with custom hashtag types but didnt shared a sanitized file. what we do know is bip329 spec explicitly says "importing wallets complying to this specification should ignore types not defined here", so making Cove less strict isnt a workaround its what the spec requires. the old Labels::try_from_str was the non compliant behavior.

  1. should we report back to the user how many rows could not be imported? maybe even give reasons why?

agreed right now skips only go to the debug log. surfacing a count + reasons to the user means changing import to return something richer than Result which touches the Uniffi boundary and needs ui changes to show the warning. can to add it here or track it as a follow up issue whichever you prefer.

@praveenperera

Copy link
Copy Markdown
Contributor

@Sandipmandal25 lets do the full change including sending all the info back, but for now lets keep the UI simple and just say could not import xx labels but lets make a new issue so we don't forget to hook up the new report to the UI to show the reasons

@Sandipmandal25

Copy link
Copy Markdown
Collaborator Author

@Sandipmandal25 lets do the full change including sending all the info back, but for now lets keep the UI simple and just say could not import xx labels but lets make a new issue so we don't forget to hook up the new report to the UI to show the reasons

sounds good updating the pr shortly! thanks

Comment thread rust/src/label_manager.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt (1)

63-81: ⚠️ Potential issue | 🟡 Minor

On transaction-refresh failure, the "labels skipped" feedback is suppressed.

If getTransactions() throws at Line 69, the function shows the "transaction list may need manual refresh" snackbar and returns early, so the result.skipped > 0 branch at Line 76 never runs. The user never learns that some labels were skipped. Consider either showing the skipped-count snackbar before refreshing, or combining both pieces of information in the fallback snackbar.

♻️ Suggested reorder
-                        val result = currentManager.rust.labelManager().use { labelManager ->
-                            labelManager.import(fileContents.trim())
-                        }
-
-                        // refresh transactions with updated labels
-                        try {
-                            currentManager.rust.getTransactions()
-                        } catch (refreshError: Exception) {
-                            android.util.Log.e(tag, "failed to refresh transactions after label import", refreshError)
-                            snackbarHostState.showSnackbar("Labels imported successfully, but transaction list may need manual refresh")
-                            return@launch
-                        }
-
-                        if (result.skipped > 0u) {
-                            val noun = if (result.skipped == 1u) "label" else "labels"
-                            snackbarHostState.showSnackbar("Labels imported. Could not import ${result.skipped} $noun")
-                        } else {
-                            snackbarHostState.showSnackbar("Labels imported successfully")
-                        }
+                        val result = currentManager.rust.labelManager().use { labelManager ->
+                            labelManager.import(fileContents.trim())
+                        }
+
+                        val baseMessage = if (result.skipped > 0u) {
+                            val noun = if (result.skipped == 1u) "label" else "labels"
+                            "Labels imported. Could not import ${result.skipped} $noun"
+                        } else {
+                            "Labels imported successfully"
+                        }
+
+                        try {
+                            currentManager.rust.getTransactions()
+                            snackbarHostState.showSnackbar(baseMessage)
+                        } catch (refreshError: Exception) {
+                            android.util.Log.e(tag, "failed to refresh transactions after label import", refreshError)
+                            snackbarHostState.showSnackbar("$baseMessage — transaction list may need manual refresh")
+                        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt`
around lines 63 - 81, The snackbar about skipped labels can be suppressed by
early return when currentManager.rust.getTransactions() throws; before calling
getTransactions() (in the block using currentManager.rust.labelManager()), check
result.skipped and show the skipped-count snackbar via
snackbarHostState.showSnackbar("... ${result.skipped} ...") (or build a combined
message) so the user is always informed; keep the existing refresh attempt with
currentManager.rust.getTransactions() and its fallback snackbar, but do not
return before emitting the skipped-labels notification.
ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift (1)

349-371: ⚠️ Potential issue | 🟡 Minor

File-import flow doesn't surface skipped count, unlike other entry points.

try labelManager.import(jsonl: fileContents) discards the returned LabelImportResult and unconditionally shows "Labels have been imported successfully." This is inconsistent with the QR flow below (Line 432-447) and with CoveMainView.swift / Android counterparts, all of which now show a "Labels Imported / Could not import X labels" message when skipped > 0. Users importing a non-compliant BIP329 file here will silently lose entries without notice — the exact scenario this PR is meant to improve.

✏️ Proposed fix
-                let file = try result.get()
-                let fileContents = try FileReader(for: file).read()
-                try labelManager.import(jsonl: fileContents)
-
-                app.alertState = .init(
-                    .general(
-                        title: "Success!",
-                        message: "Labels have been imported successfully."
-                    )
-                )
+                let file = try result.get()
+                let fileContents = try FileReader(for: file).read()
+                let importResult = try labelManager.import(jsonl: fileContents)
+
+                if importResult.skipped > 0 {
+                    app.alertState = .init(
+                        .general(
+                            title: "Labels Imported",
+                            message: "Could not import \(importResult.skipped) labels"
+                        )
+                    )
+                } else {
+                    app.alertState = .init(
+                        .general(
+                            title: "Success!",
+                            message: "Labels have been imported successfully."
+                        )
+                    )
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift` around lines
349 - 371, The import branch in SelectedWalletScreen currently ignores the
return of labelManager.import(jsonl:) and always shows a success alert; change
it to capture the returned LabelImportResult, check its skipped count, and set
app.alertState to either a full success message or a partial-success message
("Labels Imported / Could not import X labels") when result.skipped > 0
(mirroring the QR flow handling). Keep the Task { await
manager.rust.getTransactions() } call after a successful/partial import so
transactions refresh, and reference LabelImportResult and the skipped property
when constructing the alert text.
🧹 Nitpick comments (4)
rust/src/backup/import.rs (1)

525-547: Prefer map_err_str per coding guidelines.

Both of these touched lines match the exact anti-pattern called out in the guidelines. Replacing the closures with map_err_str (from cove_util::ResultExt) keeps behavior identical and is cleaner.

♻️ Proposed refactor
+use cove_util::result_ext::ResultExt as _;
@@
 fn import_labels(id: &WalletId, jsonl: &str) -> Result<(), BackupError> {
     let manager = LabelManager::new(id.clone());
-    manager.import(jsonl).map(drop).map_err(|e| BackupError::Restore(e.to_string()))
+    manager.import(jsonl).map(drop).map_err_str(BackupError::Restore)
 }
@@
         LabelRestoreBehavior::PreserveCloudBackupClean => manager
             .import_without_cloud_backup_dirty(jsonl)
             .map(drop)
-            .map_err(|error| BackupError::Restore(error.to_string())),
+            .map_err_str(BackupError::Restore),
     };

(Adjust the import to the correct ResultExt path if it differs here — e.g., it's imported as use cove_util::result_ext::ResultExt as _; in rust/src/label_manager.rs.)

As per coding guidelines: "Use cove_util::ResultExt::map_err_str instead of .map_err(|e| Error::Variant(e.to_string())) — it's cleaner and equivalent".

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

In `@rust/src/backup/import.rs` around lines 525 - 547, Replace the manual
.map_err(|e| BackupError::Restore(e.to_string())) calls with the
ResultExt::map_err_str helper: in import_labels (which calls
manager.import(...)) and in restore_wallet_labels where
manager.import_without_cloud_backup_dirty(...) is used, import the trait (e.g.,
use cove_util::result_ext::ResultExt as _;) and call map_err_str to convert
errors to BackupError::Restore uniformly; this removes the closure anti-pattern
and keeps behavior identical while relying on the ResultExt helper.
ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift (1)

432-449: Success alert diverges from other iOS success paths.

Other iOS import sites use .importedLabelsSuccessfully (e.g., CoveMainView.swift Lines 420, 506) for the zero-skipped case, while this uses a generic .general(title: "Success!", ...). Minor inconsistency — consider aligning to .importedLabelsSuccessfully for a uniform UX.

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

In `@ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift` around lines
432 - 449, The success branch in SelectedWalletScreen.swift uses a generic
.general alert instead of the app's standard .importedLabelsSuccessfully case;
update the zero-skipped branch where you handle result from
labelManager.importLabels(labels:) to set app.alertState =
.init(.importedLabelsSuccessfully) (instead of the current .general(...)
message) so behavior matches other import flows, leaving the skipped>0 branch
unchanged and keeping the subsequent Task { await manager.rust.getTransactions()
} call.
rust/src/label_manager.rs (1)

575-577: Avoid .unwrap() even when the invariant holds.

The invariant (parsed.is_empty() ⇒ at least one parse error) means first_error is Some here, but an explicit fallback makes this robust against future refactors and avoids a potential panic if the invariant is ever broken.

🛡️ Proposed defensive tweak
-    if parsed.is_empty() {
-        return Err(LabelManagerError::Parse(first_error.unwrap()));
-    }
+    if parsed.is_empty() {
+        return Err(LabelManagerError::Parse(
+            first_error.unwrap_or_else(|| "no parseable label entries".to_string()),
+        ));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/label_manager.rs` around lines 575 - 577, The code returns
Err(LabelManagerError::Parse(first_error.unwrap())) assuming first_error is Some
when parsed.is_empty(); make this defensive by replacing the unwrap with a safe
extraction (e.g., match or unwrap_or/unwrap_or_else) that supplies a sensible
fallback error when first_error is None, so in the parsed.is_empty() branch you
always construct LabelManagerError::Parse with either the actual parse error or
a default error value/message; update the use sites around parsed and
first_error in label_manager.rs to avoid .unwrap().
android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt (1)

356-362: Pluralization inconsistency with MoreInfoPopover.kt.

Here the message is always "Could not import ${result.skipped} labels", but MoreInfoPopover.kt correctly picks "label" vs "labels" based on count. Consider aligning for consistency across entry points.

✏️ Proposed fix
                         if (result.skipped > 0u) {
+                            val noun = if (result.skipped == 1u) "label" else "labels"
                             alertState = TaggedItem(
                                 AppAlertState.General(
                                     title = "Labels Imported",
-                                    message = "Could not import ${result.skipped} labels"
+                                    message = "Could not import ${result.skipped} $noun"
                                 )
                             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt` around lines 356
- 362, The alert message in AppManager.kt always uses "labels" regardless of
result.skipped; update the TaggedItem(AppAlertState.General(...)) assignment
(where alertState is set) to pluralize correctly by checking result.skipped and
using "label" when result.skipped == 1 and "labels" otherwise (or call the
existing pluralization helper used by MoreInfoPopover.kt if available), so the
message reads "Could not import 1 label" vs "Could not import N labels".
🤖 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/nfc/NfcLabelImportSheet.kt`:
- Around line 89-95: The current logic treats a partial import (when
importResult.skipped > 0u) as a full error by calling onError, which
misrepresents success — change the flow in the block after val importResult =
labelManager.import(text.trim()) so that successful imports call onSuccess() and
skipped counts are reported separately; either add a new callback
onPartialSuccess(skipped: UInt) and invoke it when importResult.skipped > 0u
(while still calling onSuccess for the successful work) or keep calling
onSuccess() and surface the skipped count via an additional UI/state method;
also format the message to handle singular vs plural for the skipped count
(e.g., "Could not import 1 label" vs "labels") and ensure nfcReader.reset() is
still called regardless.

In `@rust/src/backup/import.rs`:
- Line 527: The restore path currently ignores the skipped-label count from
parse_labels, so change the import flow (starting at manager.import(jsonl)) and
the restore_wallet_labels return types to propagate skipped counts into
LabelRestoreOutcome (or augment LabelRestoreWarning) and ensure
BackupImportReport includes that skipped count; specifically, have parse_labels
return/sketch its skipped number to be carried through restore_wallet_labels and
consumed by manager.import(jsonl) result mapping so the resulting
LabelRestoreOutcome/LabelRestoreWarning records imported vs skipped counts and
BackupImportReport surfaces partial-label imports to the user.

In `@rust/src/label_manager.rs`:
- Around line 335-348: The code in save_imported_labels computes imported from
labels.len() before calling self.db.labels.insert_labels, which can overstate
actual inserts if insert_labels deduplicates/updates; change the implementation
so insert_labels returns an outcome (e.g., counts of inserted vs updated) and
compute imported from that result instead of labels.len(), update the call site
in save_imported_labels to use the returned inserted count when building
LabelImportResult, and preserve the mark_cloud_backup_dirty() behavior and error
mapping via LabelManagerError::Save.

---

Outside diff comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt`:
- Around line 63-81: The snackbar about skipped labels can be suppressed by
early return when currentManager.rust.getTransactions() throws; before calling
getTransactions() (in the block using currentManager.rust.labelManager()), check
result.skipped and show the skipped-count snackbar via
snackbarHostState.showSnackbar("... ${result.skipped} ...") (or build a combined
message) so the user is always informed; keep the existing refresh attempt with
currentManager.rust.getTransactions() and its fallback snackbar, but do not
return before emitting the skipped-labels notification.

In `@ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift`:
- Around line 349-371: The import branch in SelectedWalletScreen currently
ignores the return of labelManager.import(jsonl:) and always shows a success
alert; change it to capture the returned LabelImportResult, check its skipped
count, and set app.alertState to either a full success message or a
partial-success message ("Labels Imported / Could not import X labels") when
result.skipped > 0 (mirroring the QR flow handling). Keep the Task { await
manager.rust.getTransactions() } call after a successful/partial import so
transactions refresh, and reference LabelImportResult and the skipped property
when constructing the alert text.

---

Nitpick comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt`:
- Around line 356-362: The alert message in AppManager.kt always uses "labels"
regardless of result.skipped; update the TaggedItem(AppAlertState.General(...))
assignment (where alertState is set) to pluralize correctly by checking
result.skipped and using "label" when result.skipped == 1 and "labels" otherwise
(or call the existing pluralization helper used by MoreInfoPopover.kt if
available), so the message reads "Could not import 1 label" vs "Could not import
N labels".

In `@ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift`:
- Around line 432-449: The success branch in SelectedWalletScreen.swift uses a
generic .general alert instead of the app's standard .importedLabelsSuccessfully
case; update the zero-skipped branch where you handle result from
labelManager.importLabels(labels:) to set app.alertState =
.init(.importedLabelsSuccessfully) (instead of the current .general(...)
message) so behavior matches other import flows, leaving the skipped>0 branch
unchanged and keeping the subsequent Task { await manager.rust.getTransactions()
} call.

In `@rust/src/backup/import.rs`:
- Around line 525-547: Replace the manual .map_err(|e|
BackupError::Restore(e.to_string())) calls with the ResultExt::map_err_str
helper: in import_labels (which calls manager.import(...)) and in
restore_wallet_labels where manager.import_without_cloud_backup_dirty(...) is
used, import the trait (e.g., use cove_util::result_ext::ResultExt as _;) and
call map_err_str to convert errors to BackupError::Restore uniformly; this
removes the closure anti-pattern and keeps behavior identical while relying on
the ResultExt helper.

In `@rust/src/label_manager.rs`:
- Around line 575-577: The code returns
Err(LabelManagerError::Parse(first_error.unwrap())) assuming first_error is Some
when parsed.is_empty(); make this defensive by replacing the unwrap with a safe
extraction (e.g., match or unwrap_or/unwrap_or_else) that supplies a sensible
fallback error when first_error is None, so in the parsed.is_empty() branch you
always construct LabelManagerError::Parse with either the actual parse error or
a default error value/message; update the use sites around parsed and
first_error in label_manager.rs to avoid .unwrap().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1734a067-830e-4926-9e9f-fd366a3e5b25

📥 Commits

Reviewing files that changed from the base of the PR and between 2eee6e0 and c1eee31.

📒 Files selected for processing (8)
  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt
  • android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt
  • ios/Cove/CoveMainView.swift
  • ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift
  • ios/CoveCore/Sources/CoveCore/CoveCore.swift
  • rust/src/backup/import.rs
  • rust/src/label_manager.rs

Comment thread rust/src/backup/import.rs Outdated
Comment thread rust/src/label_manager.rs
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from c1eee31 to 4bded16 Compare April 17, 2026 19:00
@Sandipmandal25 Sandipmandal25 marked this pull request as draft April 17, 2026 19:01
@Sandipmandal25 Sandipmandal25 marked this pull request as ready for review April 17, 2026 19:02
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from 4bded16 to 1f8193a Compare April 17, 2026 19:04
Comment thread android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt Outdated
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from 1f8193a to d1e5124 Compare April 17, 2026 19:11

@praveenperera praveenperera left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found three correctness issues that still need to be addressed before this is ready:

  • rust/src/label_manager.rs:318-320
    importLabels() reports all parsed labels as imported because it uses labels.len(), but LabelsTable::insert_label_with_write_txn() still drops unsupported BIP329 variants like pubkey and xpub in its _ arm. That means scanner imports can show full success even when some records were never persisted. The import report should be computed after filtering to the label types the wallet can actually store.

  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt:355-356
    The skipped-label handling still does not apply to QR/camera imports with a malformed JSONL line. MultiFormat::try_from_string() builds Bip329Labels with the strict bip329::Labels::try_from_str parser, so those scans fail before importLabels(multiFormat.v1) is ever reached. Scanner imports should also go through the tolerant parser so valid labels are still imported.

  • android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt:91-94
    Partial NFC imports are still reported as failures. This code calls onSuccess() and then immediately calls onError() when some labels are skipped. WalletSheetsHost treats onError as a hard failure, so the user gets a Failed to import labels... snackbar even though labels were imported and transactions were refreshed. Partial imports need a success-with-warning path instead of reusing the failure callback.

@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from d1e5124 to cb36212 Compare April 21, 2026 07:12
@Sandipmandal25 Sandipmandal25 marked this pull request as draft April 21, 2026 07:14
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from cb36212 to 695b5db Compare April 21, 2026 07:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (2)
android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt (1)

67-78: ⚠️ Potential issue | 🟡 Minor

Don’t lose partial-import details when refresh fails.

If getTransactions() throws after a partial import, the early return@launch skips the result.skipped branch and shows a full-success message.

💬 Proposed restructuring
 // refresh transactions with updated labels
+val importMessage =
+    if (result.skipped > 0u) {
+        val noun = if (result.skipped == 1u) "label" else "labels"
+        "Labels imported. Could not import ${result.skipped} $noun"
+    } else {
+        "Labels imported successfully"
+    }
+
 try {
     currentManager.rust.getTransactions()
 } catch (refreshError: Exception) {
     android.util.Log.e(tag, "failed to refresh transactions after label import", refreshError)
-    snackbarHostState.showSnackbar("Labels imported successfully, but transaction list may need manual refresh")
+    snackbarHostState.showSnackbar("$importMessage, but transaction list may need manual refresh")
     return@launch
 }
 
-if (result.skipped > 0u) {
-    val noun = if (result.skipped == 1u) "label" else "labels"
-    snackbarHostState.showSnackbar("Labels imported. Could not import ${result.skipped} $noun")
-} else {
-    snackbarHostState.showSnackbar("Labels imported successfully")
-}
+snackbarHostState.showSnackbar(importMessage)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt`
around lines 67 - 78, The catch block for currentManager.rust.getTransactions()
returns early and thus suppresses the subsequent result.skipped handling; change
the flow so partial-import details are always shown: in MoreInfoPopover.kt, when
catching the refreshError (currentManager.rust.getTransactions()), log the error
with android.util.Log.e(tag,...), show the "Labels imported successfully, but
transaction list may need manual refresh" snackbar, but do NOT return@launch;
instead continue to evaluate result.skipped and call
snackbarHostState.showSnackbar("Labels imported. Could not import
${result.skipped} $noun") when result.skipped > 0u so partial-import info is
preserved. Ensure the error path still communicates both the refresh failure and
any skipped count to the user.
rust/src/backup/import.rs (1)

525-546: ⚠️ Potential issue | 🟡 Minor

Use map_err_str for these error conversions.

Both import() and import_without_cloud_backup_dirty() return Result<LabelParseReport, LabelManagerError>, which converts to BackupError::Restore(String). The map_err_str helper is cleaner and aligns with repository patterns.

♻️ Proposed cleanup
+use cove_util::ResultExt as _;
+
 fn import_labels(id: &WalletId, jsonl: &str) -> Result<(), BackupError> {
     let manager = LabelManager::new(id.clone());
-    manager.import(jsonl).map(drop).map_err(|e| BackupError::Restore(e.to_string()))
+    manager.import(jsonl).map(drop).map_err_str(BackupError::Restore)
 }
 ...
         LabelRestoreBehavior::PreserveCloudBackupClean => manager
             .import_without_cloud_backup_dirty(jsonl)
             .map(drop)
-            .map_err(|error| BackupError::Restore(error.to_string())),
+            .map_err_str(BackupError::Restore),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/backup/import.rs` around lines 525 - 546, Replace the manual error
string conversions with the map_err_str helper: in import_labels use
manager.import(jsonl).map(drop).map_err(map_err_str) (so it returns
BackupError::Restore via the helper) and in restore_wallet_labels replace the
.map_err(|error| BackupError::Restore(error.to_string())) on
manager.import_without_cloud_backup_dirty(jsonl).map(drop) with
.map_err(map_err_str); target the import_labels function and the call site
inside restore_wallet_labels that calls import_without_cloud_backup_dirty.
🧹 Nitpick comments (1)
android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt (1)

80-96: Key the NFC collector to its real dependencies.

onPartialSuccess, onSuccess, and onError are captured inside a long-lived LaunchedEffect(Unit), so recomposition can leave this collector calling stale callbacks. Use rememberUpdatedState for callbacks and key the effect to stable dependencies such as nfcReader and labelManager.

♻️ Suggested shape
+val currentOnSuccess by rememberUpdatedState(onSuccess)
+val currentOnPartialSuccess by rememberUpdatedState(onPartialSuccess)
+val currentOnError by rememberUpdatedState(onError)
+
- LaunchedEffect(Unit) {
+ LaunchedEffect(nfcReader, labelManager) {
    nfcReader.startScanning()
    nfcReader.scanResults.collect { result ->
      ...
-         onPartialSuccess(importResult.skipped)
+         currentOnPartialSuccess(importResult.skipped)
      ...
-         onSuccess()
+         currentOnSuccess()
      ...
-     onError("No text data found on NFC tag")
+     currentOnError("No text data found on NFC tag")
      ...
-     onError(result.message)
+     currentOnError(result.message)

Per coding guidelines: "LaunchedEffect should be keyed to actual dependencies, not Unit".

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

In `@android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt`
around lines 80 - 96, The LaunchedEffect in NfcLabelImportSheet is keyed to Unit
and captures potentially-stale callbacks (onPartialSuccess, onSuccess, onError);
change it to key the effect to the actual stable dependencies (nfcReader and
labelManager) and wrap each callback with rememberUpdatedState so the collector
always calls the latest lambda. Specifically, create val currentOnPartialSuccess
= rememberUpdatedState(onPartialSuccess) (and equivalents for
onSuccess/onError), then use LaunchedEffect(nfcReader, labelManager) { ...
collect { ... use currentOnPartialSuccess.value(), currentOnSuccess.value(),
currentOnError.value() ... } } to avoid stale captures.
🤖 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/AppManager.kt`:
- Around line 303-546: Remove the dead AppManager.handleMultiFormat() method
(the entire function body and its signature) since scanning now funnels through
Scanner.handleMultiFormat() via ScanManager (used by MainActivity, NfcScanSheet,
SendFlowHardwareScreen); after deletion, remove or tidy any imports/fields that
become unused because of the method removal and run a build/check to ensure no
references remain to AppManager.handleMultiFormat.

In `@android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt`:
- Around line 72-77: The alert message for skipped imports in ScanManager.kt
currently always uses the plural "labels"; update the message construction where
app.alertState is set (TaggedItem -> AppAlertState.General) to use singular
"label" when result.skipped == 1u and plural "labels" otherwise (e.g., choose
the word based on result.skipped or build the message with a
conditional/ternary-like expression so "Could not import 1 label" is shown for
one skipped item and "Could not import N labels" for all other counts).

In `@android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt`:
- Around line 298-308: In the onPartialSuccess lambda, preserve and display the
actual skipped count in both success and failure paths: when calling
snackbarHostState.showSnackbar inside the catch block (after onDismissNfcScanner
and manager.rust.getTransactions()), include the skipped variable in the message
(e.g., "Labels imported (X skipped)") instead of the generic "some entries
skipped"; also log the caught exception details using android.util.Log.e along
with the tag to aid debugging while keeping the skipped count in the UI message.

In `@ios/Cove/ScanManager.swift`:
- Around line 39-43: The alert message uses a hardcoded plural ("labels") when
displaying report.skipped, causing "Could not import 1 labels"; update both
places that set app.alertState (the .general alert initialization where
report.skipped is interpolated) to choose singular "label" when report.skipped
== 1 and plural "labels" otherwise, e.g., compute a small pluralized message
string (based on report.skipped) and pass that into the .general(title:message:)
initializer so the alert reads correctly for 0/1/>1 skipped entries.

In `@rust/src/label_manager.rs`:
- Around line 240-247: _import_labels currently receives an Arc<Bip329Labels>
that was created after silently dropping invalid BIP329 lines, so the returned
LabelParseReport shows skipped = 0; fix by either (A) routing the raw BIP329
JSONL through the existing import(&str) path so parsing/skip-counting happens
there before calling import_labels, or (B) augmenting Bip329Labels to carry a
skipped_count field (set where filter_map(...ok()) currently drops items in
multi_format.rs) and update MultiFormat/import_labels and _import_labels to
propagate that skipped_count into LabelParseReport; update LabelParseReport
population to include that skipped value so mobile clients see correct skipped
counts.
- Around line 313-331: parse_labels currently treats all deserialized Label
variants as imported even though insert_label_with_write_txn silently drops
PublicKey and ExtendedPublicKey; update parse_labels to compute supported vs
skipped the same way import_labels does: iterate the parsed Labels and count
supported variants using the same matches check (Label::Transaction,
Label::Address, Label::Input, Label::Output), set imported = supported and
skipped = total - supported in the LabelParseReport, then pass the original
labels and that report into save_imported_labels so raw JSONL imports report
skipped BIP329 variants consistently with import_labels.

In `@rust/src/multi_format.rs`:
- Around line 145-154: The parsing code for bip329 labels currently drops parse
failures via bip329::Label::try_from_str(...).ok() so the LabelParseReport never
sees how many lines failed; change the flow to count parse failures and
propagate that count into import logic (either by extending Bip329Labels to
carry a skipped: u32 alongside labels or by returning both parsed labels and
skipped count to the existing importLabels call) so LabelManager.importLabels()
can aggregate parse-time and import-time skips into LabelParseReport; while
here, remove the redundant .trim() on each line (string was already trimmed),
and log parse failures with debug!/warn! including the offending line or error
for diagnosability.

---

Outside diff comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt`:
- Around line 67-78: The catch block for currentManager.rust.getTransactions()
returns early and thus suppresses the subsequent result.skipped handling; change
the flow so partial-import details are always shown: in MoreInfoPopover.kt, when
catching the refreshError (currentManager.rust.getTransactions()), log the error
with android.util.Log.e(tag,...), show the "Labels imported successfully, but
transaction list may need manual refresh" snackbar, but do NOT return@launch;
instead continue to evaluate result.skipped and call
snackbarHostState.showSnackbar("Labels imported. Could not import
${result.skipped} $noun") when result.skipped > 0u so partial-import info is
preserved. Ensure the error path still communicates both the refresh failure and
any skipped count to the user.

In `@rust/src/backup/import.rs`:
- Around line 525-546: Replace the manual error string conversions with the
map_err_str helper: in import_labels use
manager.import(jsonl).map(drop).map_err(map_err_str) (so it returns
BackupError::Restore via the helper) and in restore_wallet_labels replace the
.map_err(|error| BackupError::Restore(error.to_string())) on
manager.import_without_cloud_backup_dirty(jsonl).map(drop) with
.map_err(map_err_str); target the import_labels function and the call site
inside restore_wallet_labels that calls import_without_cloud_backup_dirty.

---

Nitpick comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt`:
- Around line 80-96: The LaunchedEffect in NfcLabelImportSheet is keyed to Unit
and captures potentially-stale callbacks (onPartialSuccess, onSuccess, onError);
change it to key the effect to the actual stable dependencies (nfcReader and
labelManager) and wrap each callback with rememberUpdatedState so the collector
always calls the latest lambda. Specifically, create val currentOnPartialSuccess
= rememberUpdatedState(onPartialSuccess) (and equivalents for
onSuccess/onError), then use LaunchedEffect(nfcReader, labelManager) { ...
collect { ... use currentOnPartialSuccess.value(), currentOnSuccess.value(),
currentOnError.value() ... } } to avoid stale captures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 18a6dbf8-df1d-4fc4-8735-f06f37684527

📥 Commits

Reviewing files that changed from the base of the PR and between c1eee31 and cb36212.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (13)
  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt
  • android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt
  • android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/CoveMainView.swift
  • ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift
  • ios/Cove/ScanManager.swift
  • ios/CoveCore/Sources/CoveCore/CoveCore.swift
  • rust/src/backup/import.rs
  • rust/src/label_manager.rs
  • rust/src/multi_format.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • ios/CoveCore/Sources/CoveCore/CoveCore.swift
  • ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift

Comment thread android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt Outdated
Comment thread android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt
Comment thread ios/Cove/ScanManager.swift
Comment thread rust/src/label_manager.rs
Comment thread rust/src/label_manager.rs
Comment thread rust/src/multi_format.rs
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch 3 times, most recently from 8732227 to 1b47d11 Compare April 21, 2026 07:43
@Sandipmandal25 Sandipmandal25 marked this pull request as ready for review April 21, 2026 07:44
@Sandipmandal25

Copy link
Copy Markdown
Collaborator Author

I found three correctness issues that still need to be addressed before this is ready:

  • rust/src/label_manager.rs:318-320
    importLabels() reports all parsed labels as imported because it uses labels.len(), but LabelsTable::insert_label_with_write_txn() still drops unsupported BIP329 variants like pubkey and xpub in its _ arm. That means scanner imports can show full success even when some records were never persisted. The import report should be computed after filtering to the label types the wallet can actually store.
  • android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt:355-356
    The skipped-label handling still does not apply to QR/camera imports with a malformed JSONL line. MultiFormat::try_from_string() builds Bip329Labels with the strict bip329::Labels::try_from_str parser, so those scans fail before importLabels(multiFormat.v1) is ever reached. Scanner imports should also go through the tolerant parser so valid labels are still imported.
  • android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt:91-94
    Partial NFC imports are still reported as failures. This code calls onSuccess() and then immediately calls onError() when some labels are skipped. WalletSheetsHost treats onError as a hard failure, so the user gets a Failed to import labels... snackbar even though labels were imported and transactions were refreshed. Partial imports need a success-with-warning path instead of reusing the failure callback.

Hey @praveenperera addressed all LabelParseReport with imported/skipped counts is wired up across Rust iOS and Android. All import paths (QR, NFC, file) now go through the tolerant line by line parser and surface partial import warnings to the user. Also hit a pretty large merge conflict during rebase since this overlapped with #685 took a bit to ort out cleanly. Should be good for another look when you get a chance!

@Sandipmandal25

Copy link
Copy Markdown
Collaborator Author

also squashed the commits into 1 to keep the repo clean.

@praveenperera

Copy link
Copy Markdown
Contributor

thanks but don't squash anymore @Sandipmandal25, makes it harder to review just the changes made since last review, thanks

@praveenperera praveenperera force-pushed the fix/nunchuck-label-vout-parse branch from 1b47d11 to 819a2d5 Compare April 21, 2026 18:04

@praveenperera praveenperera left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks, good almost ready, just some small changes and some questions.

Comment thread rust/src/backup/import.rs Outdated
Comment thread rust/src/backup/import.rs Outdated
Comment thread rust/src/label_manager.rs Outdated
Comment thread rust/src/label_manager.rs Outdated
Comment thread rust/src/label_manager.rs Outdated
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from 819a2d5 to 25248f2 Compare April 21, 2026 21:06
@Sandipmandal25

Copy link
Copy Markdown
Collaborator Author

looks, good almost ready, just some small changes and some questions.

done the chnages maybe you can took a final look.

@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from 25248f2 to f381fab Compare April 21, 2026 21:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
rust/src/backup/import.rs (1)

527-527: Prefer map_err_str per repo guidelines.

-    manager.import(jsonl).map_err(|e| BackupError::Restore(e.to_string()))?;
+    manager.import(jsonl).map_err_str(BackupError::Restore)?;

Requires importing cove_util::ResultExt. Same applies to line 546 (.map_err(|error| BackupError::Restore(error.to_string()))).

As per coding guidelines: "Use cove_util::ResultExt::map_err_str instead of .map_err(|e| Error::Variant(e.to_string())) — it's cleaner and equivalent".

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

In `@rust/src/backup/import.rs` at line 527, Replace the explicit closures that
convert errors to BackupError::Restore with the repo helper: import the trait
via use cove_util::ResultExt; and change manager.import(jsonl).map_err(|e|
BackupError::Restore(e.to_string()))? and the similar .map_err(|error|
BackupError::Restore(error.to_string())) occurrences to use
.map_err_str(BackupError::Restore)? so the ResultExt helper handles the
to_string conversion cleanly.
rust/src/multi_format.rs (1)

145-154: Tolerant parse loses diagnosability and skipped-count telemetry.

Two residual issues in this block (previously flagged and marked addressed, but still present in the shown code):

  1. Line 148: .trim() is redundant — string was already trimmed at line 105.
  2. Parse failures are silently dropped via filter_map(... .ok()) with no debug!/warn! logging, so malformed JSONL lines are undiagnosable from logs. Even though LabelManager::import later counts import-time skips, parse-time drops at this layer won't appear in logs for support/debug.
♻️ Proposed fix
-        let parsed_labels: Vec<bip329::Label> = string
-            .trim()
-            .lines()
-            .filter_map(|line| bip329::Label::try_from_str(line.trim()).ok())
-            .collect();
+        let parsed_labels: Vec<bip329::Label> = string
+            .lines()
+            .filter_map(|line| {
+                let line = line.trim();
+                if line.is_empty() {
+                    return None;
+                }
+                match bip329::Label::try_from_str(line) {
+                    Ok(label) => Some(label),
+                    Err(e) => {
+                        debug!("skipping unparseable BIP329 line: {e}");
+                        None
+                    }
+                }
+            })
+            .collect();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/multi_format.rs` around lines 145 - 154, The parsing block currently
re-trims an already-trimmed `string` and silently discards parse errors via
`filter_map(... .ok())`; change the loop that builds `parsed_labels` to iterate
over `string.lines()` (no extra `.trim()`), attempt
`bip329::Label::try_from_str` for each line, collect successes into
`parsed_labels` and for failures log a `debug!` or `warn!` (including the
offending line and the parse error) so malformed JSONL is diagnosable; keep the
existing return of
`Self::Bip329Labels(Arc::new(bip329::Labels::new(parsed_labels).into()))` when
non-empty and ensure any skipped-count telemetry continues to be tracked by
`LabelManager::import` or by incrementing a local counter reported via logs.
rust/src/label_manager.rs (1)

602-652: Test coverage is solid; consider one more case.

The five cases cover the core matrix well. One small gap: a test where all lines are valid JSON Labels but unsupported variants (e.g., pubkey/xpub) would lock in the behavior of the all-skipped-as-unsupported branch (see comment on lines 586–588) and guard against regressions if parse_labels's semantics change.

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

In `@rust/src/label_manager.rs` around lines 602 - 652, Add a test that feeds
parse_labels valid JSONL lines whose "type" fields are
supported-by-JSON-but-unsupported-by-parse_labels (e.g., "pubkey" and "xpub") to
exercise the "all-skipped-as-unsupported" branch: construct a JSONL string of
two valid Label objects with types "pubkey" and "xpub", call
parse_labels(&jsonl).unwrap(), then assert labels.is_empty(), report.imported ==
0, and report.skipped == 2; reference parse_labels and the Label variant names
("pubkey", "xpub") when adding the new test in the tests module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust/src/label_manager.rs`:
- Around line 318-322: The is_supported closure definition in label_manager.rs
is exceeding rustfmt width and caused CI to fail; run cargo fmt (or rustfmt) to
reformat the closure so its body is wrapped onto multiple lines, ensuring the
matches! arm for Label::Transaction/Address/Input/Output is line-wrapped; locate
the is_supported closure near the comment and insert_label_with_write_txn usage
to verify formatting after running cargo fmt.
- Around line 249-252: After parsing in import(), guard against empty input:
call parse_labels(jsonl) as before, then if labels.is_empty() return Ok(report)
(i.e., do an early return) or pass mark_cloud_backup_dirty = !labels.is_empty()
to save_imported_labels so no cloud-dirty flag is set for a no-op. Update the
function import to use the labels variable from parse_labels and only mark cloud
backup dirty / call save_imported_labels when at least one label will be
inserted.
- Around line 586-588: When parsed.is_empty() currently returns
Err(LabelManagerError::Parse(first_error.unwrap_or_default())), change the logic
in the function that builds `parsed`/`first_error` so you distinguish two cases:
(1) parsed.is_empty() && first_error.is_some() => return
LabelManagerError::Parse(the original error), and (2) parsed.is_empty() &&
first_error.is_none() => return a clearer error indicating "all labels were
skipped as unsupported" (either add a new enum variant like
LabelManagerError::AllUnsupported or return LabelManagerError::Parse with an
explicit message such as "all labels were skipped as unsupported
(pubkey/xpub)"); update any call sites/tests expecting the previous empty-string
parse error to use the new message/variant. Ensure references to `parsed`,
`first_error`, and `LabelManagerError::Parse` are adjusted accordingly.

---

Nitpick comments:
In `@rust/src/backup/import.rs`:
- Line 527: Replace the explicit closures that convert errors to
BackupError::Restore with the repo helper: import the trait via use
cove_util::ResultExt; and change manager.import(jsonl).map_err(|e|
BackupError::Restore(e.to_string()))? and the similar .map_err(|error|
BackupError::Restore(error.to_string())) occurrences to use
.map_err_str(BackupError::Restore)? so the ResultExt helper handles the
to_string conversion cleanly.

In `@rust/src/label_manager.rs`:
- Around line 602-652: Add a test that feeds parse_labels valid JSONL lines
whose "type" fields are supported-by-JSON-but-unsupported-by-parse_labels (e.g.,
"pubkey" and "xpub") to exercise the "all-skipped-as-unsupported" branch:
construct a JSONL string of two valid Label objects with types "pubkey" and
"xpub", call parse_labels(&jsonl).unwrap(), then assert labels.is_empty(),
report.imported == 0, and report.skipped == 2; reference parse_labels and the
Label variant names ("pubkey", "xpub") when adding the new test in the tests
module.

In `@rust/src/multi_format.rs`:
- Around line 145-154: The parsing block currently re-trims an already-trimmed
`string` and silently discards parse errors via `filter_map(... .ok())`; change
the loop that builds `parsed_labels` to iterate over `string.lines()` (no extra
`.trim()`), attempt `bip329::Label::try_from_str` for each line, collect
successes into `parsed_labels` and for failures log a `debug!` or `warn!`
(including the offending line and the parse error) so malformed JSONL is
diagnosable; keep the existing return of
`Self::Bip329Labels(Arc::new(bip329::Labels::new(parsed_labels).into()))` when
non-empty and ensure any skipped-count telemetry continues to be tracked by
`LabelManager::import` or by incrementing a local counter reported via logs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9c636f0b-ab08-48ef-ba3c-17216a778090

📥 Commits

Reviewing files that changed from the base of the PR and between cb36212 and 25248f2.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (11)
  • android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt
  • android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt
  • android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt
  • android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift
  • ios/Cove/ScanManager.swift
  • ios/CoveCore/Sources/CoveCore/CoveCore.swift
  • rust/src/backup/import.rs
  • rust/src/label_manager.rs
  • rust/src/multi_format.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt
  • ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift
  • ios/Cove/ScanManager.swift

Comment thread rust/src/label_manager.rs
Comment thread rust/src/label_manager.rs
Comment thread rust/src/label_manager.rs
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from f381fab to d66cb28 Compare April 21, 2026 21:16

@praveenperera praveenperera left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found three actionable issues in the diff. The main one is that MultiFormat still drops malformed BIP329 rows before LabelManager sees them, so the scan/file-open paths that import MultiFormat::Bip329Labels can partially import labels while still reporting full success. There is also an iOS file-import path that still ignores the new LabelParseReport, and import_labels() currently treats unsupported-only payloads as a successful import. I could not verify prior PR discussion in this environment because prc was unavailable and both requested gh calls failed to reach GitHub, so this review is based on the checked-out diff.

Comment thread rust/src/multi_format.rs Outdated
Comment thread ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift
Comment thread rust/src/label_manager.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
rust/src/multi_format.rs (1)

378-389: Consider removing Deref implementation in favor of AsRef only (optional).

Implementing Deref on a non-smart-pointer wrapper like Bip329Labels is flagged by the Rust API Guidelines (C-DEREF: "Deref and DerefMut are implemented only for smart pointers"), because deref coercion can make method resolution surprising. Verification confirms that no call sites rely on deref coercion; all uses explicitly access .labels field directly. The AsRef<bip329::Labels> implementation (already present) provides an explicit alternative when needed, and the public field ensures direct field access is available. Low priority — flagging since the type is pub and exposed via uniffi::Object.

♻️ Proposed refactor
-impl std::ops::Deref for Bip329Labels {
-    type Target = bip329::Labels;
-    fn deref(&self) -> &Self::Target {
-        &self.labels
-    }
-}
-
 impl AsRef<bip329::Labels> for Bip329Labels {
     fn as_ref(&self) -> &bip329::Labels {
         &self.labels
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/multi_format.rs` around lines 378 - 389, Remove the std::ops::Deref
implementation for Bip329Labels and rely solely on the existing
AsRef<bip329::Labels> impl and the public labels field; specifically delete the
impl block named "impl std::ops::Deref for Bip329Labels" (the deref method
returning &self.labels) so the type no longer provides deref coercion, leaving
the "impl AsRef<bip329::Labels> for Bip329Labels" and the pub labels field
intact; after removal, run cargo check/CI to ensure no call sites relied on
deref coercion and update any callers to use .as_ref() or .labels explicitly if
needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift`:
- Around line 352-368: The import alert logic incorrectly pluralizes skipped
labels and shows a positive "Labels Imported" title even when nothing was
imported; extract the duplicated alert construction into a helper like
labelImportAlertState(for result: LabelParseReport) that returns an
AppAlertState, implement singular/plural for "label"/"labels" and choose title
"Import Failed" when result.imported == 0, "Labels Imported" when
result.imported > 0 && result.skipped > 0, else the existing "Success!" message,
then replace the inline alert blocks in the import path (the try
labelManager.import(...) handling) and onChangeOfScannedLabels with calls to
this helper.

---

Nitpick comments:
In `@rust/src/multi_format.rs`:
- Around line 378-389: Remove the std::ops::Deref implementation for
Bip329Labels and rely solely on the existing AsRef<bip329::Labels> impl and the
public labels field; specifically delete the impl block named "impl
std::ops::Deref for Bip329Labels" (the deref method returning &self.labels) so
the type no longer provides deref coercion, leaving the "impl
AsRef<bip329::Labels> for Bip329Labels" and the pub labels field intact; after
removal, run cargo check/CI to ensure no call sites relied on deref coercion and
update any callers to use .as_ref() or .labels explicitly if needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c8649379-0a27-4ebf-ae58-62677259b0d7

📥 Commits

Reviewing files that changed from the base of the PR and between 25248f2 and 6ae7eaf.

📒 Files selected for processing (4)
  • ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift
  • rust/src/backup/import.rs
  • rust/src/label_manager.rs
  • rust/src/multi_format.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rust/src/label_manager.rs

Comment thread ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch 2 times, most recently from e3904d3 to 8c86ab5 Compare April 24, 2026 18:12
Comment thread rust/src/backup/import.rs Outdated
Comment thread rust/src/backup/import.rs Outdated
@praveenperera praveenperera force-pushed the fix/nunchuck-label-vout-parse branch from 70d6ea5 to 2541f27 Compare May 5, 2026 15:33
Comment thread android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt Outdated

@praveenperera praveenperera left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

backup restore still discards the new LabelParseReport, so partial label restores can look fully clean while rows were skipped.

Comment thread rust/src/backup/import.rs Outdated
@Sandipmandal25 Sandipmandal25 force-pushed the fix/nunchuck-label-vout-parse branch from 38bc30e to 77a91f3 Compare May 12, 2026 20:50
@praveenperera praveenperera force-pushed the fix/nunchuck-label-vout-parse branch from f0eac96 to 4369e89 Compare May 13, 2026 14:40
@coderabbitai coderabbitai Bot mentioned this pull request Jun 19, 2026
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.

[BUG] Error Importing Labels from Nunchuck

2 participants