This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
masteris a release-only branch, updated every few months. Do not use it as a base for diffs or PRs during active development.v1.0-devis the current active development branch. Use it as the base for general diffs, comparisons, and new feature branches.- PR and commits should follow conventional commit naming rules.
cargo build # Debug build
cargo build --release # Release build
cargo run # Run application
cargo fmt --all # Format code
cargo clippy --all-features --all-targets -- -D warnings # Lint (warnings as errors)cargo test --all-features --workspace # All tests
cargo test --doc --all-features --workspace # Doc tests only
cargo test <test_name> --all-features # Single test
cargo test --test kittest --all-features # UI integration tests (egui_kittest)
cargo test --test e2e --all-features # End-to-end testsTest locations:
- Unit tests: inline in source files (
#[test]) - UI integration:
tests/kittest/ - E2E:
tests/e2e/
Always run cargo clippy and cargo +nightly fmt when finalizing your work.
When a PR adds or significantly changes user-facing features, check docs/user-stories.md:
- If a new feature matches no existing story, add one following the existing format (ID, persona, description, acceptance criteria,
[Implemented]tag). - If a
[Gap]story is now implemented, flip its tag to[Implemented]. - Skip user-story updates for non-functional changes (CI, docs, formatting, refactoring).
In GitHub Actions (Claude Code workflow), use scripts/safe-cargo.sh instead of cargo directly. This wrapper strips CI secrets from the environment before running cargo, preventing build scripts from accessing credentials.
scripts/safe-cargo.sh build --all-features
scripts/safe-cargo.sh test --all-features --workspace
scripts/safe-cargo.sh clippy --all-features --all-targets -- -D warnings
scripts/safe-cargo.sh +nightly fmt --all- When a method takes
&AppContext(orOption<&AppContext>), place it as the first parameter afterself. - Screen constructors handle errors internally via
MessageBannerand returnSelfwith degraded state. Keepcreate_screen()clean — no error handling at callsites.
User-facing error messages (shown in MessageBanner via Display) must follow these rules:
- 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. - 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.
- Tone: Calm, direct, brief. Not apologetic ("Sorry!"), not alarming ("Something went wrong!"), not vague ("An error occurred").
- Technical details: Never in the message itself — no raw error strings, stack traces, SDK internals, or error codes. Attach via
BannerHandle::with_details(e)— theDebugrepr 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. - 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
.ftlfiles later — one message ID per string, placeholders only for dynamic values ({ $seconds },{ $name }), no logic in the text itself. - Reference implementation:
sdk_error_user_message()insrc/backend_task/error.rsdemonstrates the pattern for SDK errors. NewTaskErrorvariants should follow the same style. - 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. - Prefer granular
TaskErrorvariants overGeneric: When mapping errors to add context, add a dedicatedTaskErrorvariant with a#[source]field rather than converting toTaskError::Generic(format!(...)). Granular variants preserve the error chain, enable structural matching by callers, and makeDisplay/Debugseparation explicit.TaskError::Genericis a last resort for one-off strings with no upstream error to preserve.
Dash Evo Tool is a cross-platform GUI application (Rust + egui) for interacting with Dash Evolution. It enables DPNS username registration, contest voting, state transition viewing, wallet management, and identity operations across Mainnet/Testnet/Devnet.
- docs/ai-design should contain architecture, technical design and manual testing scenarios files, grouped in subdirectories prefixed with ISO-formatted date. Exception:
docs/user-stories.mdis a living document maintained at the top level — not date-grouped. - docs/personas contains user personas (Everyday User, Power User, Platform Developer) that define the three target user archetypes and the progressive disclosure model for UI complexity. Consult these when making UX decisions about what to show/hide or how to structure wallet features.
- docs/user-stories.md catalogs user stories across feature areas, tagged by persona and marked
[Implemented]or[Gap]. Reference when planning new features or verifying coverage. - docs/ux-design-patterns.md is the UI/UX reference card — explains when and how to use design tokens, buttons, dialogs, forms, accessibility rules, and progressive disclosure. For exact values (sizes, colors, padding), refer to source files (
src/ui/theme.rs,src/ui/components/). Consult when building or reviewing UI. - end-user documentation is in a separate repo: https://github.com/dashpay/docs/tree/HEAD/docs/user/network/dash-evo-tool , published at https://docs.dash.org/en/stable/docs/user/network/dash-evo-tool/
- app.rs -
AppState: owns all screens, polls task results each frame, dispatches to visible screen - ui/ - Screens and reusable components (
ui/components/) - backend_task/ - Async business logic, one submodule per domain (identity, wallet, contract, etc.)
- model/ - Data types (amounts, fees, settings, wallet/identity models)
- database/ - SQLite persistence (rusqlite), one module per domain
- context/ -
AppContext: network config, SDK client, database, wallets, settings cache (split into submodules:identity_db.rs,wallet_lifecycle.rs,settings_db.rs, etc.) - spv/ - Simplified Payment Verification for light wallet support
- components/core_zmq_listener - Real-time Dash Core event listening via ZMQ
dash-sdk- Dash blockchain SDK (git dep from dashpay/platform)egui/eframe 0.33- Immediate mode GUI frameworktokio- Async runtime (12 worker threads)rusqlite- SQLite with bundled library- Rust edition 2024, minimum rust-version 1.92
Environment config via .env in app directory:
- macOS:
~/Library/Application Support/Dash-Evo-Tool/.env - Linux:
~/.config/dash-evo-tool/.env - Windows:
C:\Users\<User>\AppData\Roaming\Dash-Evo-Tool\config\.env
See .env.example for network configuration options.
The UI and async backend communicate through an action/channel pattern:
- Screens return
AppActionfrom theirui()method (e.g.,AppAction::BackendTask(task)) AppStatespawns a tokio task that callsapp_context.run_backend_task(task, sender)AppContext::run_backend_task()matches on theBackendTaskenum and dispatches to domain-specific async methods- Results come back via tokio MPSC channel as
TaskResult(Success/Error/Refresh) - Main
update()loop pollstask_result_receiver.try_recv()each frame and routes results to the visible screen'sdisplay_task_result()
Screen::ui() → AppAction::BackendTask(task)
→ tokio::spawn → AppContext::run_backend_task()
→ sender.send(TaskResult::Success(result))
→ AppState::update() polls receiver → Screen::display_task_result()
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.
All screens implement the ScreenLike trait:
ui(&mut self, ctx: &Context) -> AppAction- Render UI, return actionsdisplay_task_result(&mut self, result: BackendTaskSuccessResult)- Handle async resultsdisplay_message(&mut self, msg: &str, type: MessageType)- Show user feedbackrefresh(&mut self)/refresh_on_arrival(&mut self)- Re-fetch datachange_context(&mut self, app_context: &Arc<AppContext>)- Handle network switch
Screen types:
- Root screens: Stored in
AppState.main_screens(BTreeMap byRootScreenType), persist across navigation - Modal/detail screens: Pushed onto
AppState.screen_stack, popped when dismissed
Screens hold Arc<AppContext> and manage their own UI state.
AppContext (~50 fields) is Arc-wrapped and shared across all screens and async tasks. Key contents:
sdk: RwLock<Sdk>- Dash SDK (clone for async use to avoid holding lock across await)db: Arc<Database>- SQLite persistencewallets: RwLock<BTreeMap<...>>- Loaded wallets- Cached system contracts (DPNS, DashPay, withdrawals, tokens, keyword search)
connection_status,developer_mode,fee_multiplier_permille- Per-network instances (mainnet always present, others created on demand)
Components follow a lazy initialization pattern (see docs/COMPONENT_DESIGN_PATTERN.md):
struct MyScreen {
amount: Option<Amount>, // Domain data
amount_widget: Option<AmountInput>, // UI component (lazy)
}
// In show():
let widget = self.amount_widget.get_or_insert_with(|| AmountInput::new(type));
let response = widget.show(ui);
response.inner.update(&mut self.amount);Requirements:
- Private fields only
- Builder methods for configuration (
with_label(), etc.) - Response struct with
ComponentResponsetrait (has_changed(),is_valid(),changed_value()) - Self-contained validation and error handling
- Support both light and dark mode via
ComponentStyles
Anti-patterns: public mutable fields, eager initialization, not clearing invalid data
User-facing messages (errors, warnings, success, infos) use MessageBanner (src/ui/components/message_banner.rs). Global banners are rendered centrally by island_central_panel() — AppState::update() sets them automatically for backend task results. When using MessageBanner::set_global(), no guard is needed — it is idempotent and automatically logs at the appropriate level (error/warn/debug). Screens only override display_message() for side-effects. See the component's doc comments and docs/ai-design/2026-02-17-unified-messages/ for details.
BannerHandle lifecycle: Screens that run backend tasks typically store a refresh_banner: Option<BannerHandle> field. On task dispatch, set it via MessageBanner::set_global() with an info/progress message. In display_message() (called as a side-effect by AppState), dismiss the progress banner via self.refresh_banner.take_and_clear() (from OptionBannerExt). Simply setting the field to None would leak the banner — take_and_clear() removes it from the egui context. AppState handles displaying the actual result banner.
Logging: MessageBanner logs all displayed messages (with details) automatically. Additional logging is unnecessary.
Error banners: Never expose raw backend/database errors to users. Use a user-friendly message in the banner and attach technical details via BannerHandle::with_details(). When the error implements Display and its text is user-appropriate, pass it directly to set_global; otherwise write a descriptive, actionable message:
MessageBanner::set_global(ctx, "Failed to load token balances", MessageType::Error)
.with_details(e);Consider whether a repeated or reused message belongs in a dedicated TaskError variant instead of being written as a string literal at the callsite. A variant centralises the wording, keeps Display / Debug separation clean, and makes the error testable. This is a soft guideline — a one-off screen-level message that wraps no upstream error is fine as a literal; errors that originate in backend tasks should generally live in TaskError.
Single SQLite connection wrapped in Mutex<Connection>. Schema initialized in database/initialization.rs. Domain modules provide typed CRUD methods. Backend task errors use TaskError (src/backend_task/error.rs) — see App Task System section above.
Linux (x86_64/aarch64), Windows (x86_64), macOS (x86_64/aarch64 with code signing)
Requires protoc v25.2+ for protocol buffer compilation. Different ZMQ libraries for Windows (zeromq) vs Unix (zmq).