Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Screen::ui() → AppAction::BackendTask(task)

**Backend task enums**: `BackendTask` has variants like `IdentityTask(IdentityTask)`, `WalletTask(WalletTask)`, `TokenTask(Box<TokenTask>)`, 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<T, TaskError>` (`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<String>` ensures backwards compatibility: existing `Result<T, String>` 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<T, TaskError>` (`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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion src/backend_task/dashpay/auto_accept_handler.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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),
})?;

Expand Down
3 changes: 2 additions & 1 deletion src/backend_task/dashpay/contact_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
})?;
Expand Down
21 changes: 14 additions & 7 deletions src/backend_task/dashpay/contact_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})?;

Expand All @@ -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),
}
})?;
Expand Down Expand Up @@ -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),
})?;

Expand Down Expand Up @@ -529,7 +532,8 @@ async fn resolve_username_to_identity(sdk: &Sdk, username: &str) -> Result<Ident
.ok_or(TaskError::DataContractNotFound)?;

let mut query = DocumentQuery::new(Arc::new(dpns_contract), "domain").map_err(|e| {
TaskError::DpnsFetchError {
DashPayError::QueryCreation {
query_target: "DPNS domain",
source: Box::new(e),
}
})?;
Expand Down Expand Up @@ -573,7 +577,8 @@ pub async fn accept_contact_request(

// Fetch the specific contact request document by creating a query with its ID
let query = DocumentQuery::new(dashpay_contract.clone(), "contactRequest").map_err(|e| {
TaskError::DpnsFetchError {
DashPayError::QueryCreation {
query_target: "DashPay contactRequest",
source: Box::new(e),
}
})?;
Expand All @@ -588,7 +593,8 @@ pub async fn accept_contact_request(

// Check if we already sent a contact request to this identity
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),
})?;

Expand Down Expand Up @@ -657,7 +663,8 @@ pub async fn reject_contact_request(
let dashpay_contract = app_context.dashpay_contract.clone();

let query = DocumentQuery::new(dashpay_contract.clone(), "contactRequest").map_err(|e| {
TaskError::DpnsFetchError {
DashPayError::QueryCreation {
query_target: "DashPay contactRequest",
source: Box::new(e),
}
})?;
Expand Down
9 changes: 6 additions & 3 deletions src/backend_task/dashpay/contacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ pub async fn load_contacts(

// Query for contact requests where we are the sender (ownerId)
let mut outgoing_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),
})?;

Expand All @@ -220,7 +221,8 @@ pub async fn load_contacts(

// Query for contact requests where we are the recipient (toUserId)
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),
})?;

Expand Down Expand Up @@ -280,7 +282,8 @@ pub async fn load_contacts(

// Now query for contact info documents
let mut contact_info_query = DocumentQuery::new(dashpay_contract.clone(), "contactInfo")
.map_err(|e| TaskError::DpnsFetchError {
.map_err(|e| DashPayError::QueryCreation {
query_target: "DashPay contactInfo",
source: Box::new(e),
})?;

Expand Down
30 changes: 19 additions & 11 deletions src/backend_task/dashpay/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ pub async fn load_profile(
let dashpay_contract = app_context.dashpay_contract.clone();

// Query for profile document owned by this identity
let mut profile_query =
DocumentQuery::new(dashpay_contract, "profile").map_err(|e| TaskError::DpnsFetchError {
let mut profile_query = DocumentQuery::new(dashpay_contract, "profile").map_err(|e| {
DashPayError::QueryCreation {
query_target: "DashPay profile",
source: Box::new(e),
})?;
}
})?;

profile_query = profile_query.with_where(WhereClause {
field: "$ownerId".to_string(),
Expand Down Expand Up @@ -129,7 +131,8 @@ pub async fn update_profile(
// Check if profile already exists
let mut profile_query =
DocumentQuery::new(dashpay_contract.clone(), "profile").map_err(|e| {
TaskError::DpnsFetchError {
DashPayError::QueryCreation {
query_target: "DashPay profile",
source: Box::new(e),
}
})?;
Expand Down Expand Up @@ -406,10 +409,12 @@ pub async fn fetch_contact_profile(
let dashpay_contract = app_context.dashpay_contract.clone();

// Query for the contact's profile document
let mut query =
DocumentQuery::new(dashpay_contract, "profile").map_err(|e| TaskError::DpnsFetchError {
let mut query = DocumentQuery::new(dashpay_contract, "profile").map_err(|e| {
DashPayError::QueryCreation {
query_target: "DashPay profile",
source: Box::new(e),
})?;
}
})?;

query = query.with_where(WhereClause {
field: "$ownerId".to_string(),
Expand Down Expand Up @@ -450,10 +455,12 @@ pub async fn search_profiles(
let normalized_query = query_trimmed.to_lowercase();

// Search DPNS for usernames starting with the query
let mut dpns_query =
DocumentQuery::new(dpns_contract, "domain").map_err(|e| TaskError::DpnsFetchError {
let mut dpns_query = DocumentQuery::new(dpns_contract, "domain").map_err(|e| {
DashPayError::QueryCreation {
query_target: "DPNS domain",
source: Box::new(e),
})?;
}
})?;

dpns_query = dpns_query
.with_where(WhereClause {
Expand Down Expand Up @@ -496,7 +503,8 @@ pub async fn search_profiles(
// Query for profile document owned by this identity
let mut profile_query =
DocumentQuery::new(dashpay_contract.clone(), "profile").map_err(|e| {
TaskError::DpnsFetchError {
DashPayError::QueryCreation {
query_target: "DashPay profile",
source: Box::new(e),
}
})?;
Expand Down
40 changes: 29 additions & 11 deletions src/backend_task/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const RPC_WALLET_NOT_SPECIFIED: i32 = -19;
#[derive(Debug, Error)]
pub enum TaskError {
/// SPV subsystem errors.
#[error(transparent)]
#[error("{}", spv_user_message(.0))]
Spv(#[from] crate::spv::SpvError),

/// DashPay domain errors.
Expand All @@ -32,7 +32,7 @@ pub enum TaskError {
Config(#[from] crate::config::ConfigError),

/// GroveSTARK prover errors.
#[error(transparent)]
#[error("Could not verify platform data. Please retry.")]
GroveStark(#[from] crate::model::grovestark_prover::GroveSTARKError),

/// Wallet errors.
Expand All @@ -46,15 +46,8 @@ pub enum TaskError {
source: rusqlite::Error,
},

/// Failed to persist an identity update to the local database.
#[error("Could not save identity changes. Check available disk space and retry.")]
IdentitySaveError {
#[source]
source: rusqlite::Error,
},

/// Tokio task join errors.
#[error(transparent)]
#[error("An internal operation failed unexpectedly. Please restart the application.")]
JoinError(#[from] tokio::task::JoinError),

/// Core wallet not configured for this wallet on a multi-wallet Core node.
Expand Down Expand Up @@ -401,7 +394,7 @@ pub enum TaskError {
/// The Dash Platform SDK could not be initialised with the current config,
/// or a context provider could not be bound to the current AppContext.
#[error(
"Could not start the SDK. Please check your network settings and restart the application."
"Could not connect to the Dash network. Please check your network settings and restart the application."
)]
SdkInitializationFailed { detail: String },

Expand Down Expand Up @@ -630,6 +623,31 @@ pub fn is_instant_lock_proof_invalid(error: &SdkError) -> 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`.
Expand Down
2 changes: 1 addition & 1 deletion src/backend_task/identity/add_key_to_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
2 changes: 1 addition & 1 deletion src/backend_task/identity/load_identity_from_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/backend_task/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/backend_task/identity/register_dpns_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions src/backend_task/identity/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}
2 changes: 1 addition & 1 deletion src/backend_task/identity/withdraw_from_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}
Loading