Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7f4291d
docs: promote i18n-ready strings to general coding rule (#660)
lklimek Mar 12, 2026
b934df5
refactor: add typed error variants for migration (#660)
lklimek Mar 12, 2026
aab8178
refactor: replace string errors with typed errors in spv/config/infra…
lklimek Mar 12, 2026
73704b7
refactor: replace string errors with typed TaskError in model/databas…
lklimek Mar 12, 2026
1e98cef
refactor: replace string errors with typed TaskError in backend_task/…
lklimek Mar 12, 2026
21bd380
refactor: replace string errors with typed TaskError in ui/ (#660)
lklimek Mar 12, 2026
5e5ea87
fix: remove useless TaskError::from conversions in wallet task dispat…
lklimek Mar 12, 2026
6184482
Merge branch 'v1.0-dev' into task/660-i18n-ready-strings-general-rule
lklimek Mar 12, 2026
d466668
Merge branch 'v1.0-dev' into task/660-i18n-ready-strings-general-rule
lklimek Mar 12, 2026
a5c577d
refactor: fix all unresolved review comments — proof errors, asset lo…
Copilot Mar 12, 2026
d44ade4
refactor: remove TaskError::Generic and TaskError::Other, replace wit…
Copilot Mar 13, 2026
0b635e5
refactor: address PR #739 review comments — structural error matching…
lklimek Mar 13, 2026
bd55bee
refactor: address remaining PR #739 review comments — lock poisoning,…
lklimek Mar 13, 2026
1d62eb7
Merge branch 'v1.0-dev' into task/660-i18n-ready-strings-general-rule
lklimek Mar 13, 2026
22b0a45
refactor: address final PR #739 review comments — jargon removal, dua…
lklimek Mar 13, 2026
cb9af82
refactor: add 6 new TaskError variants for LegacyError removal
lklimek Mar 13, 2026
4f61876
refactor: migrate system/grovestark/mnlist/platform_info to TaskError
lklimek Mar 13, 2026
751117c
refactor: remove From<String> for TaskError and LegacyError variant
lklimek Mar 13, 2026
2f58f08
refactor: address 16 review findings across error handling and conven…
lklimek Mar 13, 2026
9c9ea5a
Merge branch 'v1.0-dev' into task/660-i18n-ready-strings-general-rule
lklimek Mar 13, 2026
ab31046
refactor: replace `UserInput(String)` with typed `TaskError` variants…
Copilot Mar 14, 2026
b27c1c5
refactor: address review feedback on TaskError migration — jargon, tr…
Copilot Mar 14, 2026
de44eeb
fix: resolve CI formatting failures (#754)
Copilot Mar 14, 2026
bf40da6
fix: address 3 PR review findings — error chain, transitional note, v…
lklimek Mar 15, 2026
b5017b8
fix: address review round 4 — typed errors, dead code, lock safety
lklimek Mar 15, 2026
1ef019d
docs: strengthen error variant policy — never use String fields for u…
lklimek Mar 15, 2026
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
12 changes: 6 additions & 6 deletions CLAUDE.md
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ scripts/safe-cargo.sh +nightly fmt --all

* When a method takes `&AppContext` (or `Option<&AppContext>`), place it as the first parameter after `self`.
* Screen constructors handle errors internally via `MessageBanner` and return `Self` with degraded state. Keep `create_screen()` clean — no error handling at callsites.
* **i18n-ready strings**: All user-facing strings (labels, messages, tooltips, errors) must be simple, complete sentences. Avoid concatenating fragments, positional assumptions, or grammar that breaks in other languages. Each string should be extractable as a single translation unit with named placeholders for dynamic values and no logic in the text itself. Current code uses standard Rust format specifiers (`{name}`, `{max}`). When i18n extraction happens later, these will become Fluent-style placeholders (`{ $name }`, `{ $max }`).

### Error messages

Expand All @@ -69,11 +70,10 @@ User-facing error messages (shown in `MessageBanner` via `Display`) must follow
1. **Audience**: Write for the Everyday User persona (`docs/personas/everyday-user.md`). No jargon — no "consensus error", "nonce", "state transition", "SDK", "RPC", or error codes.
2. **Structure**: *What happened* + *what to do*. Every message must include a concrete action the user can take themselves: retry, wait, try a different approach. Never redirect to "contact support" — users must be able to self-resolve.
3. **Tone**: Calm, direct, brief. Not apologetic ("Sorry!"), not alarming ("Something went wrong!"), not vague ("An error occurred").
4. **Technical details**: Never in the message itself — no raw error strings, stack traces, SDK internals, or error codes. Attach via `BannerHandle::with_details(e)` — the `Debug` repr goes to the collapsible details panel and logs. Never refer users to "details" or "details panel" — these are not visible in basic mode. Exception: Base58 identifiers (see rule 7) are not technical details — they are user-meaningful handles.
5. **i18n-ready**: Write messages as simple, complete sentences without interpolation tricks. Avoid concatenating fragments, positional assumptions, or grammar that breaks in other languages. Messages should be straightforward to extract into [Fluent](https://projectfluent.org/) `.ftl` files later — one message ID per string, placeholders only for dynamic values (`{ $seconds }`, `{ $name }`), no logic in the text itself.
6. **Reference implementation**: `sdk_error_user_message()` in `src/backend_task/error.rs` demonstrates the pattern for SDK errors. New `TaskError` variants should follow the same style.
7. **Base58 IDs are allowed in messages**: Contract IDs, identity IDs, document IDs, and similar Base58-encoded identifiers may appear in user-facing messages when they help the user identify which object is involved (e.g., *"This key conflicts with an existing key bound to contract `Abc123…`."*). They are not jargon — they are opaque-but-copyable handles the user can look up.
8. **Prefer granular `TaskError` variants over `Generic`**: When mapping errors to add context, add a dedicated `TaskError` variant with a `#[source]` field rather than converting to `TaskError::Generic(format!(...))`. Granular variants preserve the error chain, enable structural matching by callers, and make `Display` / `Debug` separation explicit. `TaskError::Generic` is a last resort for one-off strings with no upstream error to preserve. For `#[source]` fields in SDK-originated error variants, use `Box<SdkError>` — convert upstream types (e.g. `ProtocolError`) via `SdkError::Protocol(e)`. Use the concrete domain type directly for non-SDK errors (e.g. `rusqlite::Error`). Omit `#[source]` entirely when the upstream error carries no useful diagnostic information (e.g. a channel `SendError`).
4. **Technical details**: Never in the message itself — no raw error strings, stack traces, SDK internals, or error codes. Attach via `BannerHandle::with_details(e)` — the `Debug` repr goes to the collapsible details panel and logs. Never refer users to "details" or "details panel" — these are not visible in basic mode. Exception: Base58 identifiers (see rule 6) are not technical details — they are user-meaningful handles.
5. **Reference implementation**: `sdk_error_user_message()` in `src/backend_task/error.rs` demonstrates the pattern for SDK errors. New `TaskError` variants should follow the same style.
6. **Base58 IDs are allowed in messages**: Contract IDs, identity IDs, document IDs, and similar Base58-encoded identifiers may appear in user-facing messages when they help the user identify which object is involved (e.g., *"This key conflicts with an existing key bound to contract `Abc123…`."*). They are not jargon — they are opaque-but-copyable handles the user can look up.
7. **Use dedicated `TaskError` variants**: Every error should get a dedicated `TaskError` variant with a `#[source]` field that preserves the error chain, enables structural matching, and keeps `Display` / `Debug` separation explicit. For `#[source]` fields in SDK-originated error variants, use `Box<SdkError>` — convert upstream types (e.g. `ProtocolError`) via `SdkError::Protocol(e)`. Use the concrete domain type directly for non-SDK errors (e.g. `rusqlite::Error`). Omit `#[source]` entirely when the upstream error carries no useful diagnostic information (e.g. a channel `SendError`).

## Architecture Overview

Expand Down 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
12 changes: 7 additions & 5 deletions src/backend_task/broadcast_state_transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use dash_sdk::{
platform::transition::broadcast::BroadcastStateTransition,
};

use crate::backend_task::error::TaskError;
use crate::context::AppContext;

use super::BackendTaskSuccessResult;
Expand All @@ -12,10 +13,11 @@ impl AppContext {
&self,
state_transition: StateTransition,
sdk: &Sdk,
) -> Result<BackendTaskSuccessResult, String> {
match state_transition.broadcast(sdk, None).await {
Ok(_) => Ok(BackendTaskSuccessResult::BroadcastedStateTransition),
Err(e) => Err(format!("Error broadcasting state transition: {}", e)),
}
) -> Result<BackendTaskSuccessResult, TaskError> {
state_transition
.broadcast(sdk, None)
.await
.map(|_| BackendTaskSuccessResult::BroadcastedStateTransition)
.map_err(TaskError::from)
}
}
64 changes: 31 additions & 33 deletions src/backend_task/contested_names/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod vote_on_dpns_name;

use crate::app::TaskResult;
use crate::backend_task::BackendTaskSuccessResult;
use crate::backend_task::error::TaskError;
use crate::context::AppContext;
use crate::model::qualified_identity::QualifiedIdentity;
use dash_sdk::Sdk;
Expand Down Expand Up @@ -39,14 +40,13 @@ impl AppContext {
task: ContestedResourceTask,
sdk: &Sdk,
sender: crate::utils::egui_mpsc::SenderAsync<TaskResult>,
) -> Result<BackendTaskSuccessResult, String> {
) -> Result<BackendTaskSuccessResult, TaskError> {
match &task {
ContestedResourceTask::QueryDPNSContests => self
.query_dpns_contested_resources(sdk, sender)
.await
.map(|_| BackendTaskSuccessResult::None),
ContestedResourceTask::VoteOnDPNSNames(votes, all_voters) => {
// Create a vector of async closures that will vote on each name concurrently
let futures = votes
.iter()
.map(|(name, choice)| {
Expand All @@ -63,58 +63,56 @@ impl AppContext {
})
.collect::<Vec<_>>();

// Run all futures concurrently
let results = join_all(futures).await;

let final_results = results
.into_iter()
.flat_map(|(name, vote_choice, det_execution_result)| {
match det_execution_result {
.flat_map(
|(name, vote_choice, det_execution_result)| match det_execution_result {
Ok(BackendTaskSuccessResult::DPNSVoteResults(platform_results)) => {
// Voting succeeded in DET, return the Platform results
platform_results
}
Err(det_err_msg) => {
// Voting failed in DET, return the error message
vec![(name.clone(), *vote_choice, Err(det_err_msg))]
Err(det_err) => {
vec![(name.clone(), *vote_choice, Err(det_err.to_string()))]
}
Ok(_) => {
// Got some other BackendTaskSuccessResult, this shouldn't occur
vec![(name.clone(), *vote_choice, Ok(()))]
}
}
})
},
)
.collect::<Vec<_>>();

Ok(BackendTaskSuccessResult::DPNSVoteResults(final_results))
}
ContestedResourceTask::ScheduleDPNSVotes(scheduled_votes) => self
.insert_scheduled_votes(scheduled_votes)
.map(|_| BackendTaskSuccessResult::ScheduledVotes)
.map_err(|e| format!("Error inserting scheduled votes: {}", e)),
ContestedResourceTask::CastScheduledVote(scheduled_vote, voter) => self
.vote_on_dpns_name(
ContestedResourceTask::ScheduleDPNSVotes(scheduled_votes) => {
self.insert_scheduled_votes(scheduled_votes)?;
Ok(BackendTaskSuccessResult::ScheduledVotes)
}
ContestedResourceTask::CastScheduledVote(scheduled_vote, voter) => {
self.vote_on_dpns_name(
&scheduled_vote.contested_name,
scheduled_vote.choice,
&[(**voter).clone()],
sdk,
sender,
)
.await
.map(|_| BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone()))
.map_err(|e| format!("Error casting scheduled vote: {}", e)),
ContestedResourceTask::ClearAllScheduledVotes => self
.clear_all_scheduled_votes()
.map(|_| BackendTaskSuccessResult::Refresh)
.map_err(|e| format!("Error clearing all scheduled votes: {}", e)),
ContestedResourceTask::ClearExecutedScheduledVotes => self
.clear_executed_scheduled_votes()
.map(|_| BackendTaskSuccessResult::Refresh)
.map_err(|e| format!("Error clearing executed scheduled votes: {}", e)),
ContestedResourceTask::DeleteScheduledVote(voter_id, contested_name) => self
.delete_scheduled_vote(voter_id.as_slice(), contested_name)
.map(|_| BackendTaskSuccessResult::Refresh)
.map_err(|e| format!("Error clearing scheduled vote: {}", e)),
.await?;
Ok(BackendTaskSuccessResult::CastScheduledVote(
scheduled_vote.clone(),
))
}
ContestedResourceTask::ClearAllScheduledVotes => {
self.clear_all_scheduled_votes()?;
Ok(BackendTaskSuccessResult::Refresh)
}
ContestedResourceTask::ClearExecutedScheduledVotes => {
self.clear_executed_scheduled_votes()?;
Ok(BackendTaskSuccessResult::Refresh)
}
ContestedResourceTask::DeleteScheduledVote(voter_id, contested_name) => {
self.delete_scheduled_vote(voter_id.as_slice(), contested_name)?;
Ok(BackendTaskSuccessResult::Refresh)
}
}
}
}
Loading
Loading