Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
99980f5
fix(migration): prompt for wallet passwords before completing migration
lklimek Jul 14, 2026
f99630b
fix(migration): free the password prompt from the SPV overlay, add a …
lklimek Jul 14, 2026
03d4c31
Merge branch 'fix/migration-prompt-ux' into feat/migration-password-p…
lklimek Jul 14, 2026
8c6a96a
wip(migration): headless fail-fast, legacy read-only, registration si…
lklimek Jul 14, 2026
5873570
fix(shielded): gate fund-moving shielded tasks at the backend chokepoint
lklimek Jul 14, 2026
0362aa0
fix(dashpay): stop erasing accepted accounts, double-submits and sile…
lklimek Jul 14, 2026
8614c6d
fix(wallet): report a corrupted wallet envelope as damage, not a wron…
lklimek Jul 14, 2026
f59c81c
fix(dashpay): preserve saved contact details and keep paid actions gu…
lklimek Jul 14, 2026
af39624
fix(wallet): unlock a cold-booted protected wallet with its correct p…
lklimek Jul 14, 2026
b3e0057
fix(ui): block input outside a non-dismissible modal prompt
lklimek Jul 14, 2026
dd347ba
fix(migration): keep wallet work gated while the storage update await…
lklimek Jul 14, 2026
641321b
fix(wallet): collect the redundant legacy seed envelope and stop over…
lklimek Jul 14, 2026
8ea3a14
Merge branch 'fix/dashpay-loss-risks' into integration/review-remedia…
lklimek Jul 14, 2026
37c6813
Merge branch 'fix/shielded-chokepoint' into integration/review-remedi…
lklimek Jul 14, 2026
bf5fa3d
Merge branch 'fix/migration-v2-hardening' into integration/review-rem…
lklimek Jul 14, 2026
cb4b706
fix(ui): make every passphrase prompt own the interaction surface
lklimek Jul 14, 2026
f191629
fix(migration): scope an unlocked seed to the storage update, not to …
lklimek Jul 14, 2026
e6dcb76
fix(dashpay): release the request guard when a dispatch is refused pr…
lklimek Jul 14, 2026
7ee54c3
test(backend): pin ShieldedTask inside the wallet-touching migration …
lklimek Jul 14, 2026
555014d
fix(ui): state the disabled-tool reason once, as one translation unit
lklimek Jul 14, 2026
355543b
docs: correct the data-deletion promise and record the migration chan…
lklimek Jul 14, 2026
c13d2de
docs(wallet): warn that SecretLease::lease() refcounts per call, not …
lklimek Jul 14, 2026
ee2aed5
test(ui): prove the secret prompt's transition-frame click-through
lklimek Jul 15, 2026
3e69b2f
fix(backend): refuse unavailable shielded ops early and scope DashPay…
lklimek Jul 15, 2026
7de49aa
fix(ui): drop the transition-frame click when a passphrase prompt act…
lklimek Jul 15, 2026
3591ea8
test(ui): pin passphrase activation wiring in the real AppState updat…
lklimek Jul 15, 2026
d16cb98
fix(wallets): keep dialogs open on trigger clicks
lklimek Jul 15, 2026
1743f7e
build(deps): bump platform to PR3968 tip (d18020f5), pulls in the Ass…
lklimek Jul 15, 2026
52678c4
test(ui): make the opening-click regression test exercise the real guard
lklimek Jul 15, 2026
c861b7a
fix(contracts): don't panic when contracts can't load on Update Contr…
lklimek Jul 15, 2026
551315f
fix(dashpay): accept all integer encodings for contact-request key in…
lklimek Jul 15, 2026
f99c1e0
fix(mcp): hydrate saved wallets before wallet-facing tools read them
lklimek Jul 15, 2026
a40f16a
fix(ui): stop the opening click from immediately cancelling confirmat…
lklimek Jul 15, 2026
6aa8fac
fix(ui): wire missing identity navigation and fix stale post-refresh …
lklimek Jul 15, 2026
82a96ae
fix(backend): stop silent hangs and panics in backend tasks (HANG-CLA…
lklimek Jul 15, 2026
9f64fd5
docs(user-stories): drop transient review-ID citation from UX-001
lklimek Jul 15, 2026
ffa7ea0
fix(app): stop migration frame race and preserve vote eligibility acr…
lklimek Jul 15, 2026
0fda1f6
fix(dashpay): make contact-request decline/cancel idempotent
lklimek Jul 15, 2026
68a2090
fix(wallet): fail Clear-Database safely and hydrate legacy wallets fo…
lklimek Jul 15, 2026
b633a52
fix(app): correlate task results to their originating operation
lklimek Jul 15, 2026
519690e
fix(wallet): also wipe identity private keys on Clear Database (SEC-001)
lklimek Jul 15, 2026
ac69d8b
fix(wallet): report partial failures when Clear Database can't delete…
lklimek Jul 15, 2026
9c9bf1f
test(dashpay): accept both decrypt-failure variants in unreadable-pri…
lklimek Jul 15, 2026
533d121
test(contracts): isolate update_contract_screen degrade test from sha…
lklimek Jul 15, 2026
feb2282
fix(wallet): report Clear-Database failure when the identity index ca…
lklimek Jul 15, 2026
8ba5cdb
fix(ui): add Back navigation to the Manage Keys screen (dead-end lock…
lklimek Jul 15, 2026
4c0a1e6
fix(ui): show fee estimate and total before sending Dash (SND-005)
lklimek Jul 15, 2026
6e094f6
fix(dashpay): report the correct cause when Add Contact can't resolve…
lklimek Jul 15, 2026
207dad9
docs: correct SND-005 fee-estimate criterion to match inline pre-send…
lklimek Jul 15, 2026
420198a
fix(ui): guard reusable modal components against opening-frame dismis…
lklimek Jul 15, 2026
3fda14d
fix(ui): guard screen-level popups against opening-frame dismiss (NEW…
lklimek Jul 15, 2026
3132c47
fix(dashpay): show payment-history amount in DASH, not raw duffs (NEW…
lklimek Jul 15, 2026
1896ac7
fix(wallets): confirm single-key removal and refresh asset locks afte…
lklimek Jul 15, 2026
c22a508
fix(wallets): restore password-modal focus without leaking background…
lklimek Jul 16, 2026
e606874
feat(migration): gate startup migration on a minimum saved-data version
lklimek Jul 16, 2026
6d625ac
test(migration): cover the too-new fail-fast and upper-accept boundary
lklimek Jul 16, 2026
c7ebd8a
feat(settings): add developer-only Wipe Platform Data control (NEW-006)
lklimek Jul 16, 2026
ef3d5c4
feat(contacts): add View Profile action to Identity Hub contacts (NEW…
lklimek Jul 16, 2026
c72658d
fix(wallet): fail-closed on identity-key wipe and surface Clear-Datab…
lklimek Jul 16, 2026
aeb81f8
test: serialize DASH_EVO_DATA_DIR mutation with one shared lock
lklimek Jul 16, 2026
3e5e33e
fix(ui): give each modal a unique guard id so popups don't dismiss ea…
lklimek Jul 16, 2026
9bc21d3
fix(ui): move Wipe Platform Data beside its sibling network controls …
lklimek Jul 16, 2026
3b766b0
fix(platform): keep cause-less transition results unconfirmed (#897)
lklimek Jul 16, 2026
0e79a89
docs(qa): PR892 user-story QA campaign — full retest record (175/175 …
lklimek Jul 16, 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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **The first launch after an upgrade asks for each password-protected wallet's
password**: the app moves your wallets into a new storage format on that first
launch, and it needs each protected wallet's password to finish the move for
that wallet. You are asked once per wallet, one at a time. If you don't have a
password to hand, you can skip that wallet: it stays locked, no coins are lost,
and its move finishes the next time you unlock it with its password. The
password is used for the update and is not kept unlocked afterwards.

- **The previous version's database is kept on this device as a read-only
recovery copy**: it is never written to, and it is no longer erased by "Clear
Database" or "Remove Wallet". Those actions remove the data *this* version
uses; the older recovery database remains and may still contain wallet recovery
data, which both confirmation dialogs now say before you confirm. Because that
database is read-only, the "Clear Platform Addresses" developer tool is
unavailable, and says why.

- **Masternode and evonode identities no longer appear in the Identity Hub or
Identities picker**: they now live exclusively on the new Masternodes tab,
so you're never offered actions (like registering a username) that don't
Expand Down Expand Up @@ -164,6 +180,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **Shielded actions now say when they are unavailable instead of failing
obscurely**: if shielding, sending, or withdrawing shielded funds is not
available on your network yet, the app says so and points you at a regular
payment, rather than starting the action and failing part-way through.

- **DashPay contact details and request actions are protected from accidental loss or duplicate
fees**: declining, cancelling, unhiding, or renaming a contact now preserves every unrelated
encrypted detail. If another client saved details this app cannot read, the app offers a clear,
confirmed replacement path instead of silently erasing them or leaving the contact permanently
hidden. Switching Identity Hub tabs also keeps paid request actions disabled until their original
task finishes.

- **Your settings and scheduled votes now survive an upgrade**: upgrading from an
earlier version no longer starts the app with a blank configuration. The first
launch after the upgrade brings across your selected network, start screen,
Expand Down
82 changes: 41 additions & 41 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ qrcode = "0.14.1"
nix = { version = "0.31.1", features = ["signal"] }
eframe = { version = "0.35.0", features = ["persistence", "wgpu"] }
base64 = "0.22.1"
dash-sdk = { git = "https://github.com/dashpay/platform", rev = "93b967f9c7ab0164b47fe825d2bae58b3974625c", features = [
dash-sdk = { git = "https://github.com/dashpay/platform", rev = "d18020f526e2a8eb1d1e868b436a7a9735795abb", features = [
"core_key_wallet",
"core_key_wallet_manager",
"core_bincode",
Expand All @@ -28,12 +28,12 @@ dash-sdk = { git = "https://github.com/dashpay/platform", rev = "93b967f9c7ab016
"core_spv",
"shielded",
] }
rs-sdk-trusted-context-provider = { git = "https://github.com/dashpay/platform", rev = "93b967f9c7ab0164b47fe825d2bae58b3974625c" }
platform-wallet = { git = "https://github.com/dashpay/platform", rev = "93b967f9c7ab0164b47fe825d2bae58b3974625c", features = [
rs-sdk-trusted-context-provider = { git = "https://github.com/dashpay/platform", rev = "d18020f526e2a8eb1d1e868b436a7a9735795abb" }
platform-wallet = { git = "https://github.com/dashpay/platform", rev = "d18020f526e2a8eb1d1e868b436a7a9735795abb", features = [
"serde",
"shielded",
] }
platform-wallet-storage = { git = "https://github.com/dashpay/platform", rev = "93b967f9c7ab0164b47fe825d2bae58b3974625c", features = [
platform-wallet-storage = { git = "https://github.com/dashpay/platform", rev = "d18020f526e2a8eb1d1e868b436a7a9735795abb", features = [
"shielded",
] }
zip32 = "0.2.0"
Expand Down
2 changes: 1 addition & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Config precedence (highest to lowest):
### Standalone (default)

When no `MCP_API_KEY` is set, `det-cli` runs its own backend in-process. No running GUI app or server required.
Saved wallets are hydrated from the shared data directory on demand, so one-shot commands can see wallets imported by earlier `det-cli` or GUI runs.

### Connected to Dash Evo Tool GUI

Expand Down Expand Up @@ -218,4 +219,3 @@ det-cli shielded-balance-get wallet-id=shielded-test
The `mnemonic` for the framework wallet is read from `E2E_WALLET_MNEMONIC`
(shell env or the project-root `.env`) in the backend-e2e harness; for the
standalone `det-cli` loop above, pass it directly to `core-wallet-import`.

6 changes: 3 additions & 3 deletions docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Set these in the app's `.env` file (see `.env.example`) or as environment variab
| `network_info` | — | `det-cli network-info` | Show active network and available configured networks |
| `network_reinit_sdk` | `network` | `det-cli network-reinit-sdk` | Rebuild Core RPC client and Platform SDK with current config (use after changing credentials) |
| `network_switch` | `network` | `det-cli network-switch` | Switch the active network (creates context if needed, may take a few seconds) |
| `core_wallets_list` | `network`? | `det-cli core-wallets-list` | List wallets loaded in the app (alias + seed hash) |
| `core_wallets_list` | `network`? | `det-cli core-wallets-list` | List wallets saved for the active network (alias + seed hash) |
| `core_wallet_import` | `mnemonic`, `network`, `alias`? | `det-cli core-wallet-import` | Import a wallet from a BIP-39 recovery phrase (unprotected); returns its seed hash. Idempotent |
| `core_address_create` | `wallet_id`, `network`? | `det-cli core-address-create` | Generate a new receive address for a wallet |
| `core_balances_get` | `wallet_id`, `network`? | `det-cli core-balances-get` | Show wallet balances (total, confirmed, unconfirmed) in duffs |
Expand Down Expand Up @@ -99,9 +99,9 @@ Parameters marked `?` are optional. The `det-cli` column shows the equivalent CL

### SPV requirements

All wallet-facing tools wait for SPV to fully sync before executing. This includes both core-chain tools (`core_address_create`, `core_balances_get`, `core_funds_send`) and platform tools (`platform_addresses_list`, `identity_credits_topup`, `shielded_shield_from_core`). Even DAPI-only operations need SPV because the SDK verifies DAPI proofs against quorum and masternode list data from the synced chain. When another DET instance is already running, SPV falls back to a temporary directory and must sync from scratch.
Wallet-facing tools that need chain or proof state wait for SPV to fully sync before executing. This includes both core-chain tools (`core_address_create`, `core_balances_get`, `core_funds_send`) and proof-verifying platform tools (`platform_addresses_list`, `identity_credits_topup`, `shielded_shield_from_core`). These Platform operations need SPV because the SDK verifies DAPI proofs against quorum and masternode list data from the synced chain. When another DET instance is already running, SPV falls back to a temporary directory and must sync from scratch.

Tools that make no network calls skip the SPV gate: the metadata tools (`core_wallets_list`, `network_info`, `tool_describe`), the local wallet import (`core_wallet_import`), and the shielded snapshot read `shielded_balance_get` (a pure in-memory read of the last synced balance). `shielded_address_get` also skips the SPV gate, but it reads through the wallet backend, so the backend must already be wired — run `shielded_init` (or any SPV-gated tool) first in standalone mode. `shielded_init` and `shielded_sync` still wait for SPV — they wire the wallet backend and drive a coordinator sync.
Tools that make no network calls skip the SPV gate: the metadata tools (`core_wallets_list`, `network_info`, `tool_describe`), the local wallet import (`core_wallet_import`), and the shielded snapshot read `shielded_balance_get` (a pure in-memory read of the last synced balance). Wallet-reading tools hydrate saved wallets from local storage without starting SPV. `shielded_address_get` also skips the SPV gate and wires the wallet backend automatically, but `shielded_init` is still required before it can return an address for a wallet whose shielded keys have not been bound. `shielded_init` and `shielded_sync` still wait for SPV and drive a coordinator sync.

`masternode_credits_withdraw` waits for SPV before dispatching: a withdrawal does proof-verified Platform reads, so it gates like every other proof-verifying tool. (`identity_credits_withdraw` historically skipped this gate; the masternode tool deliberately adds it.)

Expand Down
31 changes: 31 additions & 0 deletions docs/ai-design/2026-07-14-migration-password-prompt/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Storage Update Password Prompt

## Decision

Older wallet data is copied into the current stores during startup. The event is called a **storage update** in every user-facing surface. The previous SQLite database remains a recovery artifact and is opened read-only.

## UI and backend handshake

The backend copies wallet envelopes and metadata, hydrates the wallets, and registers wallets whose seeds are already available. If protected wallets remain locked, it publishes `MigrationState::AwaitingWalletPasswords` with their seed hashes and waits on `MigrationStatus`'s notification.

The egui frame loop owns the human interaction. It selects one hash, renders a non-dismissible wallet-specific password prompt, and either unlocks the wallet or records a skip. A successful seed promotion or a skip notifies the backend. A failed promotion remains a typed `TaskError`, closes the in-memory wallet again, and does not notify the backend, so the update cannot report success without the seed landing in the current vault.

Complete update runs are serialized on `AppContext`. A shared MCP request therefore joins a desktop run instead of creating a second waiter on the password notification.

Modal state is keyed by wallet seed hash rather than window title. Closing the prompt or switching hashes clears the typed buffer before removing its egui cache entry.

## Interactive capability and headless behavior

`AppContext` starts with `NullSecretPrompt`, whose `SecretPrompt::is_interactive()` capability is false. The desktop boot path installs `EguiSecretPromptHost` before backend construction; standalone MCP and CLI construction do not install it.

When protected wallets require input, the backend checks this explicit host capability before publishing an awaiting state. Without it, the update immediately returns `MigrationError::InteractivePromptUnavailable`, wrapped by the dedicated actionable `TaskError`. No timeout, environment variable, or inferred delay can turn a headless caller into an interactive one.

## Previous database invariant

Desktop and standalone boot open every existing `data.db` with SQLite's `SQLITE_OPEN_READ_ONLY` flag and do not run the historical schema ladder against it. Only an absent, fresh compatibility database may be created and initialized. Every production migration and protected-key reader also opens the source read-only. Migration code contains no drop, delete, or update path for legacy tables. Idempotency uses per-network completion sentinels in `det-app.sqlite`; it never uses table absence.

The current vault may contain both the copied recovery envelope and its current raw or password-protected form. Reads prefer the current form, while the recovery envelope remains available. Tests snapshot `data.db` before a complete two-wallet run, unlock one wallet, skip the other, and require byte-for-byte equality afterward.

## Registration concurrency

Upstream wallet registration is single-flight per wallet seed hash. Concurrent callers share a keyed one-shot outcome cell, so one leader reaches upstream and every follower receives the same success or typed error. Completed flights are removed so a later, non-concurrent user retry can try again. Different wallets can register concurrently.
27 changes: 20 additions & 7 deletions docs/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,16 @@ As a user, I want my wallet protected by a passphrase so that others cannot acce
- The passphrase is requested just-in-time, when an operation actually needs the secret (sending funds, registering an identity, signing).
- The prompt offers a "Keep this wallet unlocked until I close the app" option so a busy session is asked only once.
- That option defaults to off: unless the user actively ticks it, every secret access re-prompts, and the seed is not cached.
- The seed is never held in memory between operations: it is decrypted on demand and wiped as soon as the operation finishes.
- After the storage-seam migration, a previously password-protected wallet's secret is re-sealed in the on-device vault under the same password (Tier-2 per-secret encryption: Argon2id + XChaCha20-Poly1305). The wallet continues to prompt just-in-time; the migration is silent (no disclosure notice).
- The seed is never held in memory between operations: it is decrypted on demand and wiped as soon as the operation finishes. An explicit unlock without the keep-unlocked option retains it only until that wallet is ready to use, then wipes it.
- During a storage update, each previously password-protected wallet asks for its password so its secret can be re-sealed in the on-device vault under the same password. The user may skip a wallet they cannot unlock without blocking the rest of the app; the wallet stays locked and protected, and its update finishes the next time the user unlocks it. The prompt makes clear that skipping does not lose any coins.

### WAL-007: Remove a wallet [Implemented]
**Persona:** Priya, Jordan

As a user, I want to remove a wallet I no longer need so that it does not clutter my wallet list.

- Confirmation prompt before removal.
- Wallet data is deleted from local storage.
- Current wallet data is deleted from local storage. If an older recovery database exists, it remains untouched.

### WAL-008: View wallet balances [Implemented]
**Persona:** Alex, Priya, Jordan
Expand Down Expand Up @@ -299,6 +299,16 @@ As a user with an imported single-key wallet, I want its balance and UTXO list t
- The imported address is monitored automatically, the same way recovery-phrase wallet addresses are. No manual refresh action is offered.
- Currently blocked upstream: monitoring requires registering the imported address as a watch-only wallet, but `platform-wallet` exposes no seedless wallet-registration entry point (`register_wallet` is private; the public constructors all require a recovery-phrase seed). Unblocked by a public `register_watch_only_wallet`. Key data and receive still work.

### WAL-032: Finish a storage update without risking old wallet data [Implemented]
**Persona:** Alex, Priya, Jordan

As a user opening an older wallet installation, I want the app to update its storage safely so that I can keep using every wallet without risking my recovery copy.

- The desktop app asks for each password-protected wallet separately and never carries a typed password into another wallet's prompt.
- The user can skip a wallet; skipped wallets stay locked, and the rest of the storage update can finish.
- The previous database is read-only throughout the update, including unlock and skip paths.
- Standalone command-line and MCP use never wait for a window that is not present. They ask the user to open the desktop app once, then try again.

---

## Send and Receive (SND)
Expand Down Expand Up @@ -783,7 +793,8 @@ As a user, I want to edit contact details (nickname, note, hidden status) so tha
- Toggle contact visibility (hidden/visible).
- Hidden contacts stay listed in a collapsed "Show hidden contacts" section of the Identity Hub
Contacts tab, and can be unhidden from there — including contacts hidden as a side effect of
declining or cancelling a request.
declining or cancelling a request. If another client saved details this app cannot read, the app
warns that continuing will replace those details and asks for confirmation before unhiding.
- Changes persist locally.

### DPY-010: Remove a contact [Gap]
Expand Down Expand Up @@ -1270,9 +1281,10 @@ As an expert user, I want the app to automatically begin SPV sync when it opens
### NET-019: Clear all local data for a network [Implemented]
**Persona:** Jordan, Priya

As a user, I want to permanently delete all local data for the current network — wallets, tokens, contacts, and cached identity data — so that I can reset the app to a clean state.
As a user, I want to delete the local data this version uses for the current network — wallets, tokens, contacts, and cached identity data — so that I can reset the app to a clean state.

- Danger-mode confirmation dialog before deletion; the action cannot be undone.
- Danger-mode confirmation dialog before deletion; the deleted data cannot be recovered from within the app.
- If an older recovery database exists, it remains untouched and may still contain wallet recovery data. The confirmation dialog says so before the user confirms.
- Available for the currently selected network, including Mainnet.
- Distinct from NET-011 (Wipe Platform data), which clears only cached Platform state on Devnet/Testnet.

Expand Down Expand Up @@ -1346,7 +1358,8 @@ As a user, while a long operation that is unsafe to interrupt is running (broadc

- A full-window dimming overlay with an indeterminate spinner and an optional "Step N of M" counter and description appears while the operation runs, and lowers automatically when it finishes (success or error).
- All interaction beneath the block is suppressed: pointer clicks hit a sink, and keyboard/text input is claimed at frame start so nothing reaches a focused field beneath (FR-8 / QA-001). The block is never dismissable by Esc, Enter, Space, or Tab.
- The block yields to a passphrase prompt: when a secret prompt is shown above the overlay it keeps the keyboard (Enter/Esc/Tab) so the user can still authenticate or cancel (SEC-004).
- The block yields completely to a passphrase prompt: it remains active but paints no dimmer, pointer sink, card, or focus trap until the prompt resolves, so the user can type and use every prompt action.
- The prompt installs its own pointer sink in the block's place, so interaction beneath it stays blocked while the block is yielding. This holds for every passphrase prompt, dismissible or not: being able to cancel a prompt is not the same as being able to click past it.
- Honest escalation, never a fake exit: after 30 s a calm "This is taking longer than usual." line appears; after 120 s with no progress it escalates to "This is taking much longer than expected…" and logs a one-shot developer error. For these unsafe-to-interrupt operations there is no background/dismiss button — the safety guarantee is that every blocked operation is bounded and always lowers the block through the normal path. _(Exception: the startup/Connect SPV-sync block of UX-002 is unbounded but read-only, so it ships an always-visible "Continue in the background" escape instead.)_

### UX-002: Blocking SPV-sync overlay with a "continue in the background" escape [Implemented]
Expand Down
Loading
Loading