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 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}',