From 5c0f3f64f8823112df0bfa036984dedbecfc53c7 Mon Sep 17 00:00:00 2001 From: BRACR10 Date: Wed, 6 May 2026 20:21:59 -0600 Subject: [PATCH 1/2] fix: prevent live DMs from overwriting restored state during account restore During account restoration, live direct messages (DMs) were being written to local storage. This could interfere with the restoration process, potentially leading to an inconsistent or corrupted state where restored data is overwritten by new events, causing issues upon app relaunch. This change prevents new DMs from being saved while a restoration is active, ensuring the integrity of the restored account data. --- lib/services/mostro_service.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 5c4e28a92..ce6f4b9b1 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -15,6 +15,7 @@ import 'package:mostro_mobile/features/order/providers/order_notifier_provider.d import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; +import 'package:mostro_mobile/features/restore/restore_mode_provider.dart'; class MostroService { final Ref ref; @@ -162,6 +163,11 @@ class MostroService { decryptedEvent.id ?? event.id ?? 'msg_${DateTime.now().millisecondsSinceEpoch}'; + if (ref.read(isRestoringProvider)) { + logger.i('Restore in progress, skipping storage write for ${msg.action}'); + return; + } + await messageStorage.addMessage(messageKey, msg); logger.i( 'Received DM, Event ID: ${decryptedEvent.id ?? event.id} with payload: ${decryptedEvent.content}', From 80398e41d9c28d6eddb05929426eb53b409acaf7 Mon Sep 17 00:00:00 2001 From: BRACR10 Date: Wed, 6 May 2026 20:30:51 -0600 Subject: [PATCH 2/2] docs: Update docs During account restoration, `MostroService` saved historical relay events with current timestamps. This caused them to be replayed after the authoritative synthetic messages (which use original creation timestamps), leading to incorrect order state after an app relaunch. The `isRestoringProvider` flag now prevents `addMessage` during restore, ensuring only the correct restored state persists. --- docs/architecture/DISPUTE_CHAT_RESTORE.md | 44 +++++-------------- .../SESSION_RECOVERY_ARCHITECTURE.md | 40 +++++++---------- 2 files changed, 29 insertions(+), 55 deletions(-) diff --git a/docs/architecture/DISPUTE_CHAT_RESTORE.md b/docs/architecture/DISPUTE_CHAT_RESTORE.md index 4f36db8c0..34788480a 100644 --- a/docs/architecture/DISPUTE_CHAT_RESTORE.md +++ b/docs/architecture/DISPUTE_CHAT_RESTORE.md @@ -191,39 +191,19 @@ case and degrade gracefully (e.g. strip the peer field and continue parsing). ### Issue 3 — Dispute State Not Persisted After Restore + App Kill -#### Description - -After a successful restore, if the user force-kills the app and relaunches, disputed order -state is not recovered. The orders either show an incorrect status or disappear from -"My Trades". This does **not** happen for users who have never performed a restore. - -#### Root Cause (Preliminary) - -The normal (non-restore) app startup path relies on `mostroStorage` containing -`MostroMessage` records that were received live from the relay. On restart, -`OrderNotifier.sync()` reads all messages for each orderId from storage and reconstructs -state by replaying them in timestamp order. +**Status: Fixed** — `lib/services/mostro_service.dart` -After restore, `restore_manager` calls `storage.deleteAll()` to clear relay-replayed events -and then writes fresh `MostroMessage` records derived from `OrdersResponse`. These records -are written with `orderDetail.createdAt` timestamps (original order creation time, which -may be months old). On the next app start, `sync()` replays these messages correctly — but -relay-replayed events that arrive after `isRestoringProvider = false` may be stored with -`DateTime.now()` timestamps (see `MostroService._onData` timestamp behavior) and therefore -sort after the restore messages in `watchLatestMessage` (DESC), causing `state.updateWith` -to apply a stale relay event over the correct restored state. - -Additionally, if the `Session` persisted to Sembast after restore does not include -`adminPubkey` / `disputeId` (e.g. due to a serialization gap in `Session.toJson` / -`Session.fromJson`), then on relaunch `adminSharedKey` will be null and dispute chat -subscriptions will not start. - -#### Scope +#### Root Cause -Out of scope for the current restore feature milestone. Tracked here for future resolution. +During restore, `MostroService._onData` saved all relay-replayed historical gift-wrap events +to `mostroStorage` with `DateTime.now()` timestamps — newer than the authoritative synthetic +messages written by the restore process (which use `orderDetail.createdAt`). On the next +app launch, `OrderNotifier.sync()` replayed messages in ascending timestamp order, ending on +a stale relay event representing an earlier trade stage instead of the correct restored state. -#### Suspected Files +#### Fix -- `lib/features/order/notifiers/abstract_mostro_notifier.dart` — `sync()` and `subscribe()` replay logic -- `lib/services/mostro_service.dart` — timestamp assignment on relay-replayed events -- `lib/data/models/session.dart` — `toJson()` / `fromJson()` for `adminPubkey` / `disputeId` +Added `isRestoringProvider` check in `_onData` to skip `addMessage` during restore. Event +IDs are still registered in `eventStorage` for deduplication, preventing relay re-processing +after restore completes. Only the authoritative synthetic messages written after the +10-second delay remain in `mostroStorage`, ensuring correct state on relaunch. diff --git a/docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md b/docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md index fcd4d7a12..98d1ef53d 100644 --- a/docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md +++ b/docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md @@ -459,35 +459,29 @@ Action _getActionFromStatus(Status status, Role? userRole) { ### Restore Mode Protection -**File**: `lib/features/restore/restore_manager.dart:466-468` +**File**: `lib/features/restore/restore_manager.dart` -During recovery, a global flag prevents processing of old messages: +During recovery, a global flag is set to `true` before sessions are created and cleared after +synthetic messages are written. It serves two purposes: + +1. **`MostroService._onData`** — skips `addMessage` to `mostroStorage` while restoring. + Event IDs are still registered in `eventStorage` for deduplication. This prevents + relay-replayed historical events (timestamped with `DateTime.now()`) from sorting after + the authoritative synthetic messages (timestamped with `orderDetail.createdAt`) and + corrupting state on the next app launch. + +2. **`AbstractMostroNotifier.subscribe()`** — skips `state.updateWith()` so that DB stream + emissions triggered by synthetic message writes do not double-apply state updates already + applied by `notifier.updateStateFromMessage()`. ```dart -// Enable restore mode to block all old message processing +// Enable restore mode ref.read(isRestoringProvider.notifier).state = true; -_logger.i('Restore: enabled restore mode - blocking all old message processing'); -``` -**File**: `lib/services/mostro_service.dart:44-96` +// ... 10s delay, synthetic messages written ... -```dart -bool _isRestorePayload(Map json) { - // Check if this is a restore-specific payload that should be ignored - // during normal operation - - final wrapper = json['restore'] ?? json['order']; - if (wrapper == null || wrapper is! Map) return false; - - final payload = wrapper['payload']; - if (payload == null || payload is! Map) return false; - - // Check for restore-specific fields - if (payload.containsKey('restore_data')) return true; - if (payload.containsKey('trade_index')) return true; - - return false; -} +// Disable restore mode +ref.read(isRestoringProvider.notifier).state = false; ``` ### Session Validation