Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2433495
fix(wallet): make incompatible-data guidance and upgrade backups safe
lklimek Sep 15, 2026
c2b7e45
fix(wallet): address upgrade backup retention and cleanup reviews
lklimek Sep 16, 2026
61708ac
Merge branch 'v1.0-dev' into fix/release-review-blocking-findings
lklimek Sep 16, 2026
095f8c8
fix(wallet): clean crash snapshots and classify upgrade retries
lklimek Sep 17, 2026
38d872f
fix(storage): protect active snapshots and preserve validation retries
lklimek Sep 18, 2026
2b9e407
fix(storage): serialize compatibility open lifecycle
lklimek Sep 18, 2026
3c6dde4
Merge branch 'v1.0-dev' into fix/release-review-blocking-findings
lklimek Sep 22, 2026
7ab12b8
Merge remote-tracking branch 'origin/v1.0-dev' into fix/release-revie…
lklimek Sep 23, 2026
dcb6d8d
fix(wallet): keep resource-exhausted migrations retryable
lklimek Sep 23, 2026
14f08fb
fix(wallet): remove valid upgrade backups despite a rejected candidate
lklimek Sep 23, 2026
65d064a
Merge remote-tracking branch 'origin/v1.0-dev' into fix/release-revie…
lklimek Sep 24, 2026
f9081f3
fix(wallet): sync backup directories before reporting removal done
lklimek Sep 24, 2026
9624d01
fix(wallet): keep successful wallet open when post-open backup retent…
lklimek Sep 24, 2026
8d2d905
fix(wallet): keep current data openable and never drop the last upgra…
lklimek Sep 25, 2026
0d1c6a5
test(wallet): prove the current-only probe leaves no files behind
lklimek Sep 25, 2026
e64ba3b
fix(wallet): retain only a proven-usable upgrade snapshot
lklimek Sep 25, 2026
5058b11
Merge branch 'v1.0-dev' into fix/release-review-blocking-findings
lklimek Sep 28, 2026
89e77f3
fix(identity): persist upgrade-backup removal authorization across re…
lklimek Sep 28, 2026
820b5a6
fix(wallet): validate upgrade snapshots beneath symlinked ancestors
lklimek Sep 28, 2026
430a1ae
fix(identity): scope upgrade-backup removal to snapshots present at r…
lklimek Sep 29, 2026
72924cd
Merge remote-tracking branch 'origin/v1.0-dev' into fix/release-revie…
lklimek Sep 30, 2026
4dd746c
fix(wallet): expire upgrade backups by age instead of on identity rem…
lklimek Sep 30, 2026
e3773e0
fix(wallet): keep upgrade backups when removing a wallet
lklimek Sep 30, 2026
87d8260
fix(wallet): confirm stricter backup retention and harden pruning
lklimek Sep 30, 2026
81cdf8a
fix(ui): keep backup retention saves reliable across navigation and b…
lklimek Sep 30, 2026
0c66764
fix(storage): route every file deletion through one protected chokepoint
lklimek Sep 30, 2026
fbbb7f7
fix(storage): honor backup retention globally and harden the deletion…
lklimek Sep 30, 2026
0c9e4f9
fix(storage): retry the upgrade lifecycle lock through fork-inherited…
lklimek Sep 30, 2026
3fc061d
fix(storage): keep the newest usable upgrade backup and bound failed-…
lklimek Sep 30, 2026
ea999c4
fix(storage): never prune a backup whose usability is unproven
lklimek Sep 30, 2026
4046287
fix(storage): sweep crash-left legacy backup copies
lklimek Sep 30, 2026
15ef632
fix(storage): never let a legacy backup name collision block startup
lklimek Sep 30, 2026
5ff47a7
refactor(storage): drop the dead legacy data.db backup write path
lklimek Sep 30, 2026
c122e55
Merge remote-tracking branch 'origin/v1.0-dev' into fix/release-revie…
lklimek Sep 30, 2026
660e4b3
test(storage): trim duplicated backup-retention tests and docs
lklimek Sep 30, 2026
eea9e76
refactor(storage): make upgrade-backup housekeeping best-effort
lklimek Sep 30, 2026
681ddac
fix(storage): never treat a hard-linked file as an upgrade backup
lklimek Sep 30, 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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Allowing 3.x needs an upstream change in `dashpay/rust-dashcore` first. A TODO
in `Cargo.toml` marks the re-check.

- **Safer guidance for wallet data this version cannot open**: the message no
longer tells you to remove your local wallet data, which could have deleted
the `secrets` folder along with keys no recovery phrase can restore. It now
asks you to write down your recovery phrases and imported keys in the version
you used before, close every running instance, then set aside the `.sqlite`
files together with their matching `-wal` and `-shm` files and keep the
`secrets` folder. Imported private keys are listed separately from recovery phrases.
A database temporarily held by another session now asks you to close that
session and try again instead of reporting incompatible data.

- **Upgrade backups no longer pile up, and old ones expire automatically**: a
failed database upgrade no longer leaves a new backup on every attempt. Each
database keeps at most one upgrade backup, and it is deleted when you clear
a network's data. Upgrade backups older than 90 days are
deleted automatically once the app has opened your data successfully; change
the period or keep backups forever under Settings → Upgrade Backups (a shorter
period asks for confirmation, since it deletes backups at once). The same
retention covers upstream pre-migration snapshots and legacy `data.db`
migration backups. Removing a wallet or an identity does not touch upgrade
backups, since they also hold other wallets' data.
An upgrade that can never succeed now shows its message instead of being
retried silently. Cleanup rejects links and non-files, reports pruning
failures, and removes unfinished snapshots left by a crash. Temporary storage and memory
failures remain retryable; permission and invalid-input failures surface directly.
Concurrent sessions now protect active snapshots from cleanup, and staged
validation preserves retry options and guidance for temporary resource failures.
One lock now covers the complete open, upgrade, backup, and retention cycle.
A backup cleanup problem no longer stops up-to-date wallet data from opening;
it blocks only an upgrade that would create a new backup. A new upgrade backup
replaces the previous one only after it is saved. The retained backup
is always one that opens and passes an integrity check, so an unfinished copy
never replaces a complete one. Clearing a network's data still removes upgrade
backups when an old shielded file cannot be deleted, and reports the clear as
incomplete.

### Added

- **Add wallet-created identity keys**: for identities loaded from a wallet on
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ tz-rs = { version = "0.7.0" }
tempfile = "3.20.0"
arc-swap = "1"
urlencoding = "2"
# Builds SQLite `file:` URIs (percent-encoded, cross-platform); already a transitive dependency.
url = "2.5"
rmcp = { version = "3.3", optional = true, default-features = false }
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"], optional = true }
# Constant-time equality for `model::secret::Secret` (unconditional) and the
Expand Down
12 changes: 11 additions & 1 deletion docs/ai-design/2026-09-10-platform-pin/upgrade-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,17 @@ typed identity rows remain in the backup, while their opaque local metadata is
preserved. An ambiguous or malformed saved identity roster fails closed.
The encrypted seed vault is not migrated or rewritten.

Keep the retained backups. Downgrading does not automatically reverse the
The bridge takes its backup only after the converted copy validates, just before
the rebuild, and keeps at most one backup per database. Retention includes upstream
`backups/auto/pre-migration-<database>-*.db` snapshots and runs before and after
storage opens, including failed opens. Pruning errors stop the operation; backup
candidates must have the exact database-specific name format and be regular files,
not symbolic links (or hard links on Unix). Clearing a network's data also
deletes these backups, because they copy wallet and identity history. Removing a
single wallet or identity does not: the backups also hold other wallets' data.
Instead, backups older than a user-configurable retention period (default 90 days,
or never) are deleted once the databases they protect have opened successfully,
covering bridge, upstream pre-migration, and legacy `data.db` backups. Downgrading does not automatically reverse the
database conversion; recovery requires the corresponding backup and the prior
application version. Validation uses synthetic upstream fixtures, not a user's
real profile.
Expand Down
12 changes: 12 additions & 0 deletions docs/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ As a user, I want to remove a wallet I no longer need so that it does not clutte

- Confirmation prompt before removal.
- Current wallet data is deleted from local storage. If an older recovery database exists, it remains untouched.
- Upgrade backups are not deleted with the wallet, since they also hold other wallets' data; they expire through backup retention (WAL-034).

### WAL-008: View wallet balances [Implemented]
**Persona:** Alex, Priya, Jordan
Expand Down Expand Up @@ -325,6 +326,17 @@ As a user opening an older wallet installation, I want the app to update its sto
- Standalone command-line and MCP use never wait for a window that is not present. Without a password they ask the user to open the desktop app once, then try again.
- An operator without the desktop app can supply the wallet password non-interactively (`app_storage_update`; det-cli `--password-stdin` or `--password-file`, never as a command-line argument or environment variable). The update then finishes for every wallet sharing that password, or fails at once without skipping any wallet.

### WAL-034: Control how long upgrade backups are kept [Implemented]
**Persona:** Alex, Priya, Jordan

As a user whose wallet data was copied before a storage upgrade, I want old upgrade backups to be deleted after a while so that copies of my wallet data do not stay on this device forever.

- Upgrade backups older than 90 days are deleted automatically by default, covering app, wallet, upstream pre-migration and legacy `data.db` backups.
- Settings → Upgrade Backups lets the user change the period (1 to 3650 days) or keep backups forever. Turning automatic deletion on lets the user choose the period before applying it, and any stricter setting asks for confirmation first, because it deletes backups immediately.
- A saved change applies right away to the shared app backups, the legacy backups, and the current network's wallet backups; other networks' wallet backups follow the next time that network is opened.
- Old backups are deleted only after the app has opened the data they protect, so a backup is never removed while it may still be needed for recovery.
- Removing a wallet or an identity does not delete upgrade backups; retention alone decides when they go. Clearing a network's data still deletes them.

### WAL-033: Wallets, addresses and identities survive an app upgrade [Gap]
**Persona:** Alex, Priya, Jordan

Expand Down
52 changes: 47 additions & 5 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,15 @@ pub enum AppAction {
SwitchIdentityHubTab(crate::ui::identity::IdentityHubTab),
}

/// Whether results of the task behind `context` belong to the network chooser (settings)
/// screen whichever screen is visible: it tracks the task and must hear how it ended.
fn network_chooser_owns_task(context: &BackendTaskContext) -> bool {
matches!(
context,
BackendTaskContext::ClearNetworkDatabase | BackendTaskContext::UpdateBackupRetention
)
}

impl BitOrAssign for AppAction {
fn bitor_assign(&mut self, rhs: Self) {
if matches!(rhs, AppAction::None) {
Expand Down Expand Up @@ -2955,6 +2964,16 @@ impl App for AppState {
);
}
}
result @ BackendTaskSuccessResult::UpdatedBackupRetention { .. } => {
// The settings form waits for this answer even after the user
// navigates away, so it goes to the chooser, not the visible screen.
if let Some(screen) = self
.main_screens
.get_mut(&RootScreenType::RootScreenNetworkChooser)
{
screen.display_backend_task_result(&context, result);
}
}
BackendTaskSuccessResult::DashPayIncomingDetected(outputs) => {
// The EventBridge surfaced received outputs on a
// freshly-seen wallet transaction. Run the owner-
Expand Down Expand Up @@ -3184,13 +3203,13 @@ impl App for AppState {
&err,
);
self.route_contact_request_error_to_hidden_hub(&err);
let is_database_clear = context == BackendTaskContext::ClearNetworkDatabase;
let suppress_stale_error = !is_database_clear
let chooser_owned = network_chooser_owns_task(&context);
let suppress_stale_error = !chooser_owned
&& !recovery_delivered
&& self
.visible_screen_mut()
.should_suppress_backend_task_error(&context, &err);
if is_database_clear {
if chooser_owned {
if let Some(screen) = self
.main_screens
.get_mut(&RootScreenType::RootScreenNetworkChooser)
Expand All @@ -3204,7 +3223,7 @@ impl App for AppState {
// Let the screen handle specific error types first.
// If handled, skip the generic error banner.
let handled = suppress_stale_error
|| (!is_database_clear
|| (!chooser_owned
&& !recovery_delivered
&& self.visible_screen_mut().display_task_error(&err));

Expand Down Expand Up @@ -3241,7 +3260,7 @@ impl App for AppState {
}
_ => {}
}
if !is_database_clear && !recovery_delivered {
if !chooser_owned && !recovery_delivered {
self.visible_screen_mut()
.display_message(&msg, MessageType::Error);
}
Expand Down Expand Up @@ -4513,3 +4532,26 @@ mod shutdown_tests {
);
}
}

#[cfg(test)]
mod network_chooser_routing_tests {
use super::*;
use crate::backend_task::system_task::SystemTask;
use crate::model::backup_retention::BackupRetention;

/// A retention save outlives navigation: its result and error must reach the
/// chooser even when another screen is visible, or its form stays disabled.
#[test]
fn backup_retention_task_results_route_to_network_chooser() {
let task = BackendTask::SystemTask(SystemTask::UpdateBackupRetention(
BackupRetention::KeepForever,
));
let context = BackendTaskContext::from(&task);
assert_eq!(context, BackendTaskContext::UpdateBackupRetention);
assert!(network_chooser_owns_task(&context));
assert!(network_chooser_owns_task(
&BackendTaskContext::ClearNetworkDatabase
));
assert!(!network_chooser_owns_task(&BackendTaskContext::Other));
}
}
Loading
Loading