diff --git a/android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt b/android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt index 235360cde..02b5066f0 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/ScanManager.kt @@ -68,8 +68,18 @@ class ScanManager private constructor() { } try { - LabelManager(id = selectedWallet).use { it.importLabels(multiFormat.v1) } - app.alertState = TaggedItem(AppAlertState.ImportedLabelsSuccessfully) + val result = LabelManager(id = selectedWallet).use { it.importLabels(multiFormat.v1) } + if (result.skipped > 0u) { + val noun = if (result.skipped == 1u) "label" else "labels" + app.alertState = TaggedItem( + AppAlertState.General( + title = "Labels Imported", + message = "Could not import ${result.skipped} $noun" + ) + ) + } else { + app.alertState = TaggedItem(AppAlertState.ImportedLabelsSuccessfully) + } app.walletManager?.let { wm -> mainScope.launch { diff --git a/android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt b/android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt index a82f31138..8e068fdf9 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/nfc/NfcLabelImportSheet.kt @@ -45,6 +45,7 @@ fun NfcLabelImportSheet( labelManager: LabelManager, onDismiss: () -> Unit, onSuccess: () -> Unit, + onPartialSuccess: (skipped: UInt) -> Unit, onError: (String) -> Unit, ) { val context = LocalContext.current @@ -86,11 +87,13 @@ fun NfcLabelImportSheet( // try to parse the NFC data as BIP329 labels try { result.text?.let { text -> - // try to import the labels - labelManager.import(text.trim()) - // success! dismiss and notify + val importResult = labelManager.import(text.trim()) nfcReader.reset() - onSuccess() + if (importResult.skipped > 0u) { + onPartialSuccess(importResult.skipped) + } else { + onSuccess() + } return@collect } diff --git a/android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt b/android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt index b68b47fe4..6ae06512d 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/wallet/MoreInfoPopover.kt @@ -60,21 +60,37 @@ fun rememberWalletExportLaunchers( } } ?: throw Exception("Unable to read file") - // validate import was successful before showing success message - currentManager.rust.labelManager().use { labelManager -> + val result = currentManager.rust.labelManager().use { labelManager -> labelManager.import(fileContents.trim()) } // refresh transactions with updated labels - try { + val refreshFailed = try { currentManager.rust.getTransactions() + false } 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 + true + } + + val skippedMsg = if (result.skipped > 0u) { + val noun = if (result.skipped == 1u) "label" else "labels" + "Could not import ${result.skipped} $noun" + } else { + null } - snackbarHostState.showSnackbar("Labels imported successfully") + val msg = when { + skippedMsg != null && refreshFailed -> + "Labels imported. $skippedMsg. Transaction list may need manual refresh" + skippedMsg != null -> + "Labels imported. $skippedMsg" + refreshFailed -> + "Labels imported successfully, but transaction list may need manual refresh" + else -> + "Labels imported successfully" + } + snackbarHostState.showSnackbar(msg) } catch (e: Exception) { android.util.Log.e(tag, "error importing labels", e) snackbarHostState.showSnackbar("Unable to import labels: ${e.localizedMessage ?: e.message}") diff --git a/android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt b/android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt index a0022db1b..2e987df48 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/wallet/WalletSheets.kt @@ -295,6 +295,20 @@ internal fun WalletSheetsHost( } } }, + onPartialSuccess = { skipped -> + onDismissNfcScanner() + scope.launch { + val noun = if (skipped == 1u) "label" else "labels" + val skippedMsg = "Labels imported. Could not import $skipped $noun" + try { + manager.rust.getTransactions() + snackbarHostState.showSnackbar(skippedMsg) + } catch (e: Exception) { + android.util.Log.e(tag, "Failed to refresh transactions after partial NFC label import", e) + snackbarHostState.showSnackbar("$skippedMsg. Transaction list may need manual refresh") + } + } + }, onError = { errorMsg -> onDismissNfcScanner() scope.launch { diff --git a/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt b/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt index d194fa010..50907eef5 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt @@ -2388,9 +2388,9 @@ internal object UniffiLib { external fun uniffi_cove_fn_method_labelmanager_has_labels(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus, ): Byte external fun uniffi_cove_fn_method_labelmanager_import(`ptr`: Long,`jsonl`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, - ): Unit + ): RustBuffer.ByValue external fun uniffi_cove_fn_method_labelmanager_importlabels(`ptr`: Long,`labels`: Long,uniffi_out_err: UniffiRustCallStatus, - ): Unit + ): RustBuffer.ByValue external fun uniffi_cove_fn_method_labelmanager_insert_or_update_labels_for_txn(`ptr`: Long,`details`: Long,`label`: RustBuffer.ByValue,`origin`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Unit external fun uniffi_cove_fn_method_labelmanager_transaction_label(`ptr`: Long,`txId`: Long,uniffi_out_err: UniffiRustCallStatus, @@ -4004,10 +4004,10 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { if (lib.uniffi_cove_checksum_method_labelmanager_has_labels() != 29517.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } - if (lib.uniffi_cove_checksum_method_labelmanager_import() != 37462.toShort()) { + if (lib.uniffi_cove_checksum_method_labelmanager_import() != 57006.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } - if (lib.uniffi_cove_checksum_method_labelmanager_importlabels() != 52503.toShort()) { + if (lib.uniffi_cove_checksum_method_labelmanager_importlabels() != 8041.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } if (lib.uniffi_cove_checksum_method_labelmanager_insert_or_update_labels_for_txn() != 51703.toShort()) { @@ -14001,9 +14001,9 @@ public interface LabelManagerInterface { fun `hasLabels`(): kotlin.Boolean - fun `import`(`jsonl`: kotlin.String) + fun `import`(`jsonl`: kotlin.String): LabelParseReport - fun `importLabels`(`labels`: Bip329Labels) + fun `importLabels`(`labels`: Bip329Labels): LabelParseReport fun `insertOrUpdateLabelsForTxn`(`details`: TransactionDetails, `label`: kotlin.String, `origin`: kotlin.String?) @@ -14210,8 +14210,8 @@ open class LabelManager: Disposable, AutoCloseable, LabelManagerInterface - @Throws(LabelManagerException::class)override fun `import`(`jsonl`: kotlin.String) - = + @Throws(LabelManagerException::class)override fun `import`(`jsonl`: kotlin.String): LabelParseReport { + return FfiConverterTypeLabelParseReport.lift( callWithHandle { uniffiRustCallWithError(LabelManagerException) { _status -> UniffiLib.uniffi_cove_fn_method_labelmanager_import( @@ -14220,12 +14220,13 @@ open class LabelManager: Disposable, AutoCloseable, LabelManagerInterface FfiConverterString.lower(`jsonl`),_status) } } - + ) + } - @Throws(LabelManagerException::class)override fun `importLabels`(`labels`: Bip329Labels) - = + @Throws(LabelManagerException::class)override fun `importLabels`(`labels`: Bip329Labels): LabelParseReport { + return FfiConverterTypeLabelParseReport.lift( callWithHandle { uniffiRustCallWithError(LabelManagerException) { _status -> UniffiLib.uniffi_cove_fn_method_labelmanager_importlabels( @@ -14234,7 +14235,8 @@ open class LabelManager: Disposable, AutoCloseable, LabelManagerInterface FfiConverterTypeBip329Labels.lower(`labels`),_status) } } - + ) + } @@ -27736,6 +27738,8 @@ data class BackupImportReport ( , var `walletsWithLabelsImported`: kotlin.UInt , + var `labelsSkipped`: kotlin.UInt + , var `labelsFailedWalletNames`: List , var `labelsFailedErrors`: List @@ -27777,6 +27781,7 @@ public object FfiConverterTypeBackupImportReport: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): LabelParseReport { + return LabelParseReport( + FfiConverterUInt.read(buf), + FfiConverterUInt.read(buf), + ) + } + + override fun allocationSize(value: LabelParseReport) = ( + FfiConverterUInt.allocationSize(value.`imported`) + + FfiConverterUInt.allocationSize(value.`skipped`) + ) + + override fun write(value: LabelParseReport, buf: ByteBuffer) { + FfiConverterUInt.write(value.`imported`, buf) + FfiConverterUInt.write(value.`skipped`, buf) + } +} + + + data class LoadedCloudBackupDetail ( var `detail`: CloudBackupDetail , diff --git a/ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift b/ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift index 3adadd475..dcd5b5f11 100644 --- a/ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift +++ b/ios/Cove/Flows/SelectedWalletFlow/SelectedWalletScreen.swift @@ -368,14 +368,24 @@ struct SelectedWalletScreen: View { do { 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 result = try labelManager.import(jsonl: fileContents) + + if result.skipped > 0 { + let noun = result.skipped == 1 ? "label" : "labels" + app.alertState = .init( + .general( + title: "Labels Imported", + message: "Could not import \(result.skipped) \(noun)" + ) ) - ) + } else { + app.alertState = .init( + .general( + title: "Success!", + message: "Labels have been imported successfully." + ) + ) + } // when labels are imported, we need to get the transactions again with the updated labels Task { await manager.rust.getTransactions() } @@ -448,13 +458,25 @@ struct SelectedWalletScreen: View { } do { - try labelManager.importLabels(labels: labels) - app.alertState = .init( - .general( - title: "Success!", - message: "Labels have been imported successfully." + let result = try labelManager.importLabels(labels: labels) + if result.skipped > 0 { + let noun = result.skipped == 1 ? "label" : "labels" + app.alertState = .init( + .general( + title: "Labels Imported", + message: "Could not import \(result.skipped) \(noun)" + ) ) - ) + } else { + app.alertState = .init( + .general( + title: "Success!", + message: "Labels have been imported successfully." + ) + ) + } + + Task { await manager.rust.getTransactions() } } catch { app.alertState = .init( diff --git a/ios/Cove/ScanManager.swift b/ios/Cove/ScanManager.swift index 3910cdf68..12790f2e6 100644 --- a/ios/Cove/ScanManager.swift +++ b/ios/Cove/ScanManager.swift @@ -35,8 +35,16 @@ import SwiftUI return setInvalidLabels() } - try LabelManager(id: selectedWallet).import(labels: labels) - app.alertState = .init(.importedLabelsSuccessfully) + let report = try LabelManager(id: selectedWallet).import(labels: labels) + if report.skipped > 0 { + let noun = report.skipped == 1 ? "label" : "labels" + app.alertState = .init(.general( + title: "Labels Imported", + message: "Could not import \(report.skipped) \(noun)" + )) + } else { + app.alertState = .init(.importedLabelsSuccessfully) + } Task { await manager.rust.getTransactions() } } } catch { @@ -97,7 +105,18 @@ import SwiftUI Log.error(panic) case let .bip329Labels(labels): if let selectedWallet = Database().globalConfig().selectedWallet() { - return try LabelManager(id: selectedWallet).import(labels: labels) + let report = try LabelManager(id: selectedWallet).import(labels: labels) + if report.skipped > 0 { + let noun = report.skipped == 1 ? "label" : "labels" + app.alertState = TaggedItem(.general( + title: "Labels Imported", + message: "Could not import \(report.skipped) \(noun)" + )) + } else { + app.alertState = TaggedItem(.importedLabelsSuccessfully) + } + Task { await app.walletManager?.rust.getTransactions() } + return } app.alertState = TaggedItem( diff --git a/ios/CoveCore/Sources/CoveCore/CoveCore.swift b/ios/CoveCore/Sources/CoveCore/CoveCore.swift index 47e00ca19..bfbd50b60 100644 --- a/ios/CoveCore/Sources/CoveCore/CoveCore.swift +++ b/ios/CoveCore/Sources/CoveCore/CoveCore.swift @@ -83,7 +83,7 @@ public extension FiatOrBtc { } public extension LabelManager { - func `import`(labels: Bip329Labels) throws { + func `import`(labels: Bip329Labels) throws -> LabelParseReport { try importLabels(labels: labels) } } diff --git a/ios/CoveCore/Sources/CoveCore/generated/cove.swift b/ios/CoveCore/Sources/CoveCore/generated/cove.swift index 3b8edba44..cc920ad15 100644 --- a/ios/CoveCore/Sources/CoveCore/generated/cove.swift +++ b/ios/CoveCore/Sources/CoveCore/generated/cove.swift @@ -5093,9 +5093,9 @@ public protocol LabelManagerProtocol: AnyObject, Sendable { func hasLabels() -> Bool - func `import`(jsonl: String) throws + func `import`(jsonl: String) throws -> LabelParseReport - func importLabels(labels: Bip329Labels) throws + func importLabels(labels: Bip329Labels) throws -> LabelParseReport func insertOrUpdateLabelsForTxn(details: TransactionDetails, label: String, origin: String?) throws @@ -5229,22 +5229,24 @@ open func hasLabels() -> Bool { }) } -open func `import`(jsonl: String)throws {try rustCallWithError(FfiConverterTypeLabelManagerError_lift) { +open func `import`(jsonl: String)throws -> LabelParseReport { + return try FfiConverterTypeLabelParseReport_lift(try rustCallWithError(FfiConverterTypeLabelManagerError_lift) { uniffiCallStatus in uniffi_cove_fn_method_labelmanager_import( self.uniffiCloneHandle(), FfiConverterString.lower(jsonl),uniffiCallStatus ) -} +}) } -open func importLabels(labels: Bip329Labels)throws {try rustCallWithError(FfiConverterTypeLabelManagerError_lift) { +open func importLabels(labels: Bip329Labels)throws -> LabelParseReport { + return try FfiConverterTypeLabelParseReport_lift(try rustCallWithError(FfiConverterTypeLabelManagerError_lift) { uniffiCallStatus in uniffi_cove_fn_method_labelmanager_importlabels( self.uniffiCloneHandle(), FfiConverterTypeBip329Labels_lower(labels),uniffiCallStatus ) -} +}) } open func insertOrUpdateLabelsForTxn(details: TransactionDetails, label: String, origin: String?)throws {try rustCallWithError(FfiConverterTypeLabelManagerError_lift) { @@ -13102,6 +13104,7 @@ public struct BackupImportReport: Equatable, Hashable { public var failedWalletNames: [String] public var failedWalletErrors: [String] public var walletsWithLabelsImported: UInt32 + public var labelsSkipped: UInt32 public var labelsFailedWalletNames: [String] public var labelsFailedErrors: [String] public var settingsRestored: Bool @@ -13117,7 +13120,7 @@ public struct BackupImportReport: Equatable, Hashable { // Default memberwise initializers are never public by default, so we // declare one manually. - public init(walletsImported: UInt32, importedWalletNames: [String], walletsSkipped: UInt32, skippedWalletNames: [String], walletsFailed: UInt32, failedWalletNames: [String], failedWalletErrors: [String], walletsWithLabelsImported: UInt32, labelsFailedWalletNames: [String], labelsFailedErrors: [String], settingsRestored: Bool, settingsError: String?, + public init(walletsImported: UInt32, importedWalletNames: [String], walletsSkipped: UInt32, skippedWalletNames: [String], walletsFailed: UInt32, failedWalletNames: [String], failedWalletErrors: [String], walletsWithLabelsImported: UInt32, labelsSkipped: UInt32, labelsFailedWalletNames: [String], labelsFailedErrors: [String], settingsRestored: Bool, settingsError: String?, /** * Wallets imported with degraded functionality (e.g. unknown secret type) */degradedWalletNames: [String], @@ -13132,6 +13135,7 @@ public struct BackupImportReport: Equatable, Hashable { self.failedWalletNames = failedWalletNames self.failedWalletErrors = failedWalletErrors self.walletsWithLabelsImported = walletsWithLabelsImported + self.labelsSkipped = labelsSkipped self.labelsFailedWalletNames = labelsFailedWalletNames self.labelsFailedErrors = labelsFailedErrors self.settingsRestored = settingsRestored @@ -13164,6 +13168,7 @@ public struct FfiConverterTypeBackupImportReport: FfiConverterRustBuffer { failedWalletNames: FfiConverterSequenceString.read(from: &buf), failedWalletErrors: FfiConverterSequenceString.read(from: &buf), walletsWithLabelsImported: FfiConverterUInt32.read(from: &buf), + labelsSkipped: FfiConverterUInt32.read(from: &buf), labelsFailedWalletNames: FfiConverterSequenceString.read(from: &buf), labelsFailedErrors: FfiConverterSequenceString.read(from: &buf), settingsRestored: FfiConverterBool.read(from: &buf), @@ -13182,6 +13187,7 @@ public struct FfiConverterTypeBackupImportReport: FfiConverterRustBuffer { FfiConverterSequenceString.write(value.failedWalletNames, into: &buf) FfiConverterSequenceString.write(value.failedWalletErrors, into: &buf) FfiConverterUInt32.write(value.walletsWithLabelsImported, into: &buf) + FfiConverterUInt32.write(value.labelsSkipped, into: &buf) FfiConverterSequenceString.write(value.labelsFailedWalletNames, into: &buf) FfiConverterSequenceString.write(value.labelsFailedErrors, into: &buf) FfiConverterBool.write(value.settingsRestored, into: &buf) @@ -15277,6 +15283,60 @@ public func FfiConverterTypeLabelExportResult_lower(_ value: LabelExportResult) } +public struct LabelParseReport: Equatable, Hashable { + public var imported: UInt32 + public var skipped: UInt32 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(imported: UInt32, skipped: UInt32) { + self.imported = imported + self.skipped = skipped + } + + + + +} + +#if compiler(>=6) +extension LabelParseReport: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeLabelParseReport: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LabelParseReport { + return + try LabelParseReport( + imported: FfiConverterUInt32.read(from: &buf), + skipped: FfiConverterUInt32.read(from: &buf) + ) + } + + public static func write(_ value: LabelParseReport, into buf: inout [UInt8]) { + FfiConverterUInt32.write(value.imported, into: &buf) + FfiConverterUInt32.write(value.skipped, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeLabelParseReport_lift(_ buf: RustBuffer) throws -> LabelParseReport { + return try FfiConverterTypeLabelParseReport.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeLabelParseReport_lower(_ value: LabelParseReport) -> RustBuffer { + return FfiConverterTypeLabelParseReport.lower(value) +} + + public struct LoadedCloudBackupDetail: Equatable, Hashable { public var detail: CloudBackupDetail public var cloudOnly: CloudOnlyState @@ -38585,10 +38645,10 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_labelmanager_has_labels() != 29517) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_labelmanager_import() != 37462) { + if (uniffi_cove_checksum_method_labelmanager_import() != 57006) { return InitializationResult.apiChecksumMismatch } - if (uniffi_cove_checksum_method_labelmanager_importlabels() != 52503) { + if (uniffi_cove_checksum_method_labelmanager_importlabels() != 8041) { return InitializationResult.apiChecksumMismatch } if (uniffi_cove_checksum_method_labelmanager_insert_or_update_labels_for_txn() != 51703) { diff --git a/rust/src/backup/import.rs b/rust/src/backup/import.rs index 4967c0d9d..8d29dfb13 100644 --- a/rust/src/backup/import.rs +++ b/rust/src/backup/import.rs @@ -1,6 +1,7 @@ use std::str::FromStr as _; use bip39::Mnemonic; +use cove_util::result_ext::ResultExt as _; use strum::IntoEnumIterator as _; use tracing::{error, info, warn}; use zeroize::Zeroizing; @@ -10,7 +11,7 @@ use cove_types::network::Network; use crate::database::Database; use crate::database::global_config::GlobalConfigKey; -use crate::label_manager::LabelManager; +use crate::label_manager::{LabelManager, LabelParseReport}; use crate::mnemonic::MnemonicExt as _; use crate::wallet::fingerprint::Fingerprint; use crate::wallet::metadata::{WalletId, WalletMetadata, WalletMode, WalletType}; @@ -42,6 +43,7 @@ pub async fn import_all( Ok(RestoreResult::Imported { name, labels_imported, + labels_skipped, labels_failure, fingerprint, degraded, @@ -50,6 +52,7 @@ pub async fn import_all( if labels_imported { report.wallets_with_labels_imported += 1; } + report.labels_skipped += labels_skipped; if let Some((name, error)) = labels_failure { report.labels_failed_wallet_names.push(name); report.labels_failed_errors.push(error); @@ -120,6 +123,7 @@ enum RestoreResult { Imported { name: String, labels_imported: bool, + labels_skipped: u32, labels_failure: Option<(String, String)>, fingerprint: Option<(Fingerprint, Network, WalletMode)>, degraded: bool, @@ -270,6 +274,7 @@ pub(crate) struct LabelRestoreWarning { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub(crate) struct LabelRestoreOutcome { pub imported: bool, + pub skipped: u32, pub warning: Option, } @@ -330,12 +335,20 @@ async fn restore_wallet( LabelRestoreBehavior::MarkCloudBackupDirty, ); let labels_imported = labels_outcome.imported; + let labels_skipped = labels_outcome.skipped; if let Some(warning) = labels_outcome.warning { warn!("Failed to import labels for wallet {name}: {}", warning.error); labels_failure = Some((warning.wallet_name, warning.error)); } - Ok(RestoreResult::Imported { name, labels_imported, labels_failure, fingerprint, degraded }) + Ok(RestoreResult::Imported { + name, + labels_imported, + labels_skipped, + labels_failure, + fingerprint, + degraded, + }) } /// Run a restore operation, cleaning up on failure @@ -531,9 +544,17 @@ pub(crate) fn cleanup_failed_wallet(metadata: &WalletMetadata) -> Vec { failures } -fn import_labels(id: &WalletId, jsonl: &str) -> Result<(), BackupError> { +fn import_labels(id: &WalletId, jsonl: &str) -> Result { + let manager = LabelManager::new(id.clone()); + manager.import(jsonl).map_err_str(BackupError::Restore) +} + +fn import_labels_preserve_backup( + id: &WalletId, + jsonl: &str, +) -> Result { let manager = LabelManager::new(id.clone()); - manager.import(jsonl).map_err(|e| BackupError::Restore(e.to_string())) + manager.import_without_cloud_backup_dirty(jsonl).map_err_str(BackupError::Restore) } pub(crate) fn restore_wallet_labels( @@ -546,18 +567,22 @@ pub(crate) fn restore_wallet_labels( return LabelRestoreOutcome::default(); }; - let manager = LabelManager::new(wallet_id.clone()); let import_result = match behavior { LabelRestoreBehavior::MarkCloudBackupDirty => import_labels(wallet_id, jsonl), - LabelRestoreBehavior::PreserveCloudBackupClean => manager - .import_without_cloud_backup_dirty(jsonl) - .map_err(|error| BackupError::Restore(error.to_string())), + LabelRestoreBehavior::PreserveCloudBackupClean => { + import_labels_preserve_backup(wallet_id, jsonl) + } }; match import_result { - Ok(()) => LabelRestoreOutcome { imported: true, warning: None }, + Ok(report) => LabelRestoreOutcome { + imported: report.imported > 0, + skipped: report.skipped, + warning: None, + }, Err(error) => LabelRestoreOutcome { imported: false, + skipped: 0, warning: Some(LabelRestoreWarning { wallet_name: wallet_name.to_string(), error: error.to_string(), diff --git a/rust/src/backup/model.rs b/rust/src/backup/model.rs index 418cd7580..42d9f957e 100644 --- a/rust/src/backup/model.rs +++ b/rust/src/backup/model.rs @@ -144,6 +144,7 @@ pub struct BackupImportReport { pub failed_wallet_names: Vec, pub failed_wallet_errors: Vec, pub wallets_with_labels_imported: u32, + pub labels_skipped: u32, pub labels_failed_wallet_names: Vec, pub labels_failed_errors: Vec, pub settings_restored: bool, diff --git a/rust/src/label_manager.rs b/rust/src/label_manager.rs index 189235785..13e4bd95c 100644 --- a/rust/src/label_manager.rs +++ b/rust/src/label_manager.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use cove_util::result_ext::ResultExt as _; +use tracing::{debug, warn}; use crate::{ database::{InsertOrUpdate, Record, record::Timestamps, wallet_data::WalletDataDb}, @@ -56,6 +57,12 @@ pub enum LabelManagerError { pub type Error = LabelManagerError; type Result = std::result::Result; +#[derive(Debug, Clone, uniffi::Record)] +pub struct LabelParseReport { + pub imported: u32, + pub skipped: u32, +} + #[derive(Debug, Clone, uniffi::Object)] pub struct AddressArgs { pub address: Option
, @@ -231,13 +238,23 @@ impl LabelManager { } #[uniffi::method(name = "importLabels")] - pub fn _import_labels(&self, labels: Arc) -> Result<(), LabelManagerError> { + pub fn ffi_import_labels( + &self, + labels: Arc, + ) -> Result { let labels = Arc::unwrap_or_clone(labels); - self.import_labels(labels.0) + let parse_skipped = labels.parse_skipped; + let mut report = self.import_labels(labels.labels)?; + report.skipped += parse_skipped; + Ok(report) } - pub fn import(&self, jsonl: &str) -> Result<(), LabelManagerError> { - self.save_imported_labels(parse_labels(jsonl)?, true) + pub fn import(&self, jsonl: &str) -> Result { + let (labels, report) = parse_labels(jsonl)?; + if labels.is_empty() { + return Ok(report); + } + self.save_imported_labels(labels, report, true) } pub async fn export(&self) -> Result { @@ -299,28 +316,48 @@ impl LabelManager { Ok(Self { db }) } - pub fn import_labels(&self, labels: impl Into) -> Result<(), LabelManagerError> { - self.save_imported_labels(labels.into(), true) + pub fn import_labels( + &self, + labels: impl Into, + ) -> Result { + let labels = labels.into(); + // count only supported variants — insert_label_with_write_txn silently drops + // PublicKey and ExtendedPublicKey, so labels.len() would overstate the import count. + let is_supported = |l: &&Label| { + matches!( + l, + Label::Transaction(_) | Label::Address(_) | Label::Input(_) | Label::Output(_) + ) + }; + let supported = labels.iter().filter(is_supported).count() as u32; + if supported == 0 { + return Err(LabelManagerError::Parse("no supported labels found".to_string())); + } + let skipped = labels.len() as u32 - supported; + let report = LabelParseReport { imported: supported, skipped }; + self.save_imported_labels(labels, report, true) } pub(crate) fn import_without_cloud_backup_dirty( &self, jsonl: &str, - ) -> Result<(), LabelManagerError> { - self.save_imported_labels(parse_labels(jsonl)?, false) + ) -> Result { + let (labels, report) = parse_labels(jsonl)?; + self.save_imported_labels(labels, report, false) } fn save_imported_labels( &self, labels: Labels, + report: LabelParseReport, mark_cloud_backup_dirty: bool, - ) -> Result<(), LabelManagerError> { + ) -> Result { self.db.labels.insert_labels(labels).map_err_str(LabelManagerError::Save)?; if mark_cloud_backup_dirty { self.mark_cloud_backup_dirty(); } - Ok(()) + Ok(report) } fn mark_cloud_backup_dirty(&self) { @@ -524,6 +561,107 @@ impl LabelManager { } } -fn parse_labels(jsonl: &str) -> Result { - Labels::try_from_str(jsonl).map_err_str(LabelManagerError::Parse) +fn parse_labels(jsonl: &str) -> Result<(Labels, LabelParseReport), LabelManagerError> { + let lines: Vec<&str> = jsonl.trim().lines().filter(|line| !line.trim().is_empty()).collect(); + + if lines.is_empty() { + return Ok((Labels::new(vec![]), LabelParseReport { imported: 0, skipped: 0 })); + } + + let mut parsed = Vec::with_capacity(lines.len()); + let mut first_error: Option = None; + let mut skipped = 0u32; + + for line in &lines { + match serde_json::from_str::