diff --git a/CLAUDE.md b/CLAUDE.md index fbfdae662..8b18004d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,7 +134,7 @@ Screen::ui() → AppAction::BackendTask(task) **Backend task enums**: `BackendTask` has variants like `IdentityTask(IdentityTask)`, `WalletTask(WalletTask)`, `TokenTask(Box)`, etc. Each sub-enum has its own variants and corresponding `run_*_task()` method. Results are `BackendTaskSuccessResult` with 50+ typed variants. -**Error handling**: Backend tasks return `Result` (`src/backend_task/error.rs`). `TaskError` is a typed error envelope — `Display` produces user-friendly text for `MessageBanner`, `Debug` provides technical details for logs. `From` ensures backwards compatibility: existing `Result` code works unchanged. Domain errors (`DashPayError`, `SpvError`, etc.) are wired as `#[from]` variants for automatic conversion via `?`. When adding new backend error types, add a `#[from]` variant to `TaskError` rather than converting to `String`. +**Error handling**: Backend tasks return `Result` (`src/backend_task/error.rs`). `TaskError` is a typed error envelope — `Display` produces user-friendly text for `MessageBanner`, `Debug` provides technical details for logs. Domain errors (`DashPayError`, `SpvError`, etc.) are wired as `#[from]` variants for automatic conversion via `?`. When adding new backend error types, add a dedicated `TaskError` variant rather than converting to `String`. ## Screen Pattern diff --git a/src/backend_task/contested_names/query_dpns_contested_resources.rs b/src/backend_task/contested_names/query_dpns_contested_resources.rs index af7ca3e25..ce08fde80 100644 --- a/src/backend_task/contested_names/query_dpns_contested_resources.rs +++ b/src/backend_task/contested_names/query_dpns_contested_resources.rs @@ -22,7 +22,9 @@ impl AppContext { let data_contract = self.dpns_contract.as_ref(); let document_type = data_contract .document_type_for_name("domain") - .map_err(|_| TaskError::DataContractNotFound)?; + .map_err(|_| TaskError::ContractSchemaMismatch { + detail: "DPNS contract missing 'domain' document type", + })?; let Some(contested_index) = document_type.find_contested_index() else { return Err(TaskError::ContractSchemaMismatch { detail: "No contested index found on DPNS domain document type", diff --git a/src/backend_task/dashpay/auto_accept_handler.rs b/src/backend_task/dashpay/auto_accept_handler.rs index 0152384e8..51baff39e 100644 --- a/src/backend_task/dashpay/auto_accept_handler.rs +++ b/src/backend_task/dashpay/auto_accept_handler.rs @@ -1,5 +1,6 @@ use crate::backend_task::dashpay::auto_accept_proof::verify_auto_accept_proof; use crate::backend_task::dashpay::contact_requests::accept_contact_request; +use crate::backend_task::dashpay::errors::DashPayError; use crate::backend_task::error::TaskError; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; @@ -26,7 +27,8 @@ pub async fn process_auto_accept_requests( // Query for incoming contact requests let mut incoming_query = DocumentQuery::new(dashpay_contract.clone(), "contactRequest") - .map_err(|e| TaskError::DpnsFetchError { + .map_err(|e| DashPayError::QueryCreation { + query_target: "DashPay contactRequest", source: Box::new(e), })?; diff --git a/src/backend_task/dashpay/contact_info.rs b/src/backend_task/dashpay/contact_info.rs index 9eb704cec..03758990d 100644 --- a/src/backend_task/dashpay/contact_info.rs +++ b/src/backend_task/dashpay/contact_info.rs @@ -297,7 +297,8 @@ pub async fn create_or_update_contact_info( // Query for existing contactInfo document let mut query = DocumentQuery::new(dashpay_contract.clone(), "contactInfo").map_err(|e| { - TaskError::DpnsFetchError { + DashPayError::QueryCreation { + query_target: "DashPay contactInfo", source: Box::new(e), } })?; diff --git a/src/backend_task/dashpay/contact_requests.rs b/src/backend_task/dashpay/contact_requests.rs index 3ef5c04a6..9fb97fd75 100644 --- a/src/backend_task/dashpay/contact_requests.rs +++ b/src/backend_task/dashpay/contact_requests.rs @@ -46,7 +46,8 @@ pub async fn load_contact_requests( // Query for incoming contact requests (where toUserId == our identity) let mut incoming_query = DocumentQuery::new(dashpay_contract.clone(), "contactRequest") - .map_err(|e| TaskError::DpnsFetchError { + .map_err(|e| DashPayError::QueryCreation { + query_target: "DashPay contactRequest", source: Box::new(e), })?; @@ -68,7 +69,8 @@ pub async fn load_contact_requests( // Query for outgoing contact requests (where $ownerId == our identity) let mut outgoing_query = DocumentQuery::new(dashpay_contract, "contactRequest").map_err(|e| { - TaskError::DpnsFetchError { + DashPayError::QueryCreation { + query_target: "DashPay contactRequest", source: Box::new(e), } })?; @@ -214,7 +216,8 @@ pub async fn send_contact_request_with_proof( // Step 2: Check if a contact request already exists let dashpay_contract = app_context.dashpay_contract.clone(); let mut existing_query = DocumentQuery::new(dashpay_contract.clone(), "contactRequest") - .map_err(|e| TaskError::DpnsFetchError { + .map_err(|e| DashPayError::QueryCreation { + query_target: "DashPay contactRequest", source: Box::new(e), })?; @@ -529,7 +532,8 @@ async fn resolve_username_to_identity(sdk: &Sdk, username: &str) -> Result bool { ) } +/// Produce a user-friendly message for SPV subsystem errors. +/// +/// Inspects the specific `SpvError` variant to give actionable guidance. +fn spv_user_message(e: &crate::spv::SpvError) -> &'static str { + use crate::spv::SpvError; + match e { + SpvError::LockPoisoned(_) | SpvError::ChannelError(_) => { + "An internal error occurred. Please restart the application." + } + SpvError::ClientNotInitialized | SpvError::NotRunning => { + "The wallet sync service is not ready. Please restart the application." + } + SpvError::NetworkError(_) | SpvError::SyncFailed(_) => { + "Could not sync wallet data. Please check your connection and retry." + } + SpvError::WalletError(_) => { + "Could not process wallet data. Please check your wallet and retry." + } + SpvError::ConfigError(_) => { + "Wallet sync is not configured properly. Please check your settings." + } + SpvError::Other(_) => "Could not sync wallet data. Please retry.", + } +} + /// Produce a user-friendly message by inspecting the SDK error variant. /// /// The returned text is shown in `MessageBanner` via `Display`. diff --git a/src/backend_task/identity/add_key_to_identity.rs b/src/backend_task/identity/add_key_to_identity.rs index b918e41bb..febaa8610 100644 --- a/src/backend_task/identity/add_key_to_identity.rs +++ b/src/backend_task/identity/add_key_to_identity.rs @@ -121,7 +121,7 @@ impl AppContext { let fee_result = FeeResult::new(estimated_fee, actual_fee); self.update_local_qualified_identity(&qualified_identity) - .map_err(|e| TaskError::IdentitySaveError { source: e })?; + .map_err(|e| TaskError::Database { source: e })?; Ok(BackendTaskSuccessResult::AddedKeyToIdentity(fee_result)) } } diff --git a/src/backend_task/identity/load_identity_from_wallet.rs b/src/backend_task/identity/load_identity_from_wallet.rs index ceed43ffc..45acaccbf 100644 --- a/src/backend_task/identity/load_identity_from_wallet.rs +++ b/src/backend_task/identity/load_identity_from_wallet.rs @@ -251,7 +251,7 @@ impl AppContext { &qualified_identity, &Some((wallet_seed_hash, identity_index)), ) - .map_err(|e| TaskError::IdentitySaveError { source: e })?; + .map_err(|e| TaskError::Database { source: e })?; { let mut wallet = wallet_arc_ref.wallet.write().unwrap(); diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index 560c119c5..318aeb643 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -661,7 +661,7 @@ impl AppContext { // Store the updated identity (use update to preserve wallet association) self.update_local_qualified_identity(&updated_identity) - .map_err(|e| TaskError::IdentitySaveError { source: e })?; + .map_err(|e| TaskError::Database { source: e })?; let fee_result = FeeResult::new(estimated_fee, estimated_fee); Ok(BackendTaskSuccessResult::ToppedUpIdentity( @@ -744,7 +744,7 @@ impl AppContext { // Store the updated identity (use update to preserve wallet association) self.update_local_qualified_identity(&updated_identity) - .map_err(|e| TaskError::IdentitySaveError { source: e })?; + .map_err(|e| TaskError::Database { source: e })?; let fee_result = FeeResult::new(estimated_fee, actual_fee); Ok(BackendTaskSuccessResult::TransferredCredits(fee_result)) diff --git a/src/backend_task/identity/refresh_loaded_identities_dpns_names.rs b/src/backend_task/identity/refresh_loaded_identities_dpns_names.rs index fb99f5e2c..9d32f0794 100644 --- a/src/backend_task/identity/refresh_loaded_identities_dpns_names.rs +++ b/src/backend_task/identity/refresh_loaded_identities_dpns_names.rs @@ -73,7 +73,7 @@ impl AppContext { } self.update_local_qualified_identity(&qualified_identity) - .map_err(|e| TaskError::IdentitySaveError { source: e })?; + .map_err(|e| TaskError::Database { source: e })?; } sender diff --git a/src/backend_task/identity/register_dpns_name.rs b/src/backend_task/identity/register_dpns_name.rs index 0a979bb16..1dc7a5e5c 100644 --- a/src/backend_task/identity/register_dpns_name.rs +++ b/src/backend_task/identity/register_dpns_name.rs @@ -234,7 +234,7 @@ impl AppContext { qualified_identity.identity = refreshed_identity; self.update_local_qualified_identity(&qualified_identity) - .map_err(|e| TaskError::IdentitySaveError { source: e })?; + .map_err(|e| TaskError::Database { source: e })?; let fee_result = FeeResult::new(estimated_fee, actual_fee); Ok(BackendTaskSuccessResult::RegisteredDpnsName(fee_result)) diff --git a/src/backend_task/identity/transfer.rs b/src/backend_task/identity/transfer.rs index 49f199a0b..b660729d0 100644 --- a/src/backend_task/identity/transfer.rs +++ b/src/backend_task/identity/transfer.rs @@ -64,13 +64,13 @@ impl AppContext { { receiver.identity.set_balance(receiver_balance); self.update_local_qualified_identity(receiver) - .map_err(|e| TaskError::IdentitySaveError { source: e })?; + .map_err(|e| TaskError::Database { source: e })?; } let fee_result = FeeResult::new(estimated_fee, actual_fee); self.update_local_qualified_identity(&qualified_identity) .map(|_| BackendTaskSuccessResult::TransferredCredits(fee_result)) - .map_err(|e| TaskError::IdentitySaveError { source: e }) + .map_err(|e| TaskError::Database { source: e }) } } diff --git a/src/backend_task/identity/withdraw_from_identity.rs b/src/backend_task/identity/withdraw_from_identity.rs index 843efc3ea..57df40f6a 100644 --- a/src/backend_task/identity/withdraw_from_identity.rs +++ b/src/backend_task/identity/withdraw_from_identity.rs @@ -114,6 +114,6 @@ impl AppContext { self.update_local_qualified_identity(&qualified_identity) .map(|_| BackendTaskSuccessResult::WithdrewFromIdentity(fee_result)) - .map_err(|e| TaskError::IdentitySaveError { source: e }) + .map_err(|e| TaskError::Database { source: e }) } }