From 5c0f3f64f8823112df0bfa036984dedbecfc53c7 Mon Sep 17 00:00:00 2001 From: BRACR10 Date: Wed, 6 May 2026 20:21:59 -0600 Subject: [PATCH 1/7] 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/7] 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 From 96380fb0f070fe6c2692feccd2764039d754e9a8 Mon Sep 17 00:00:00 2001 From: BRACR10 Date: Wed, 10 Jun 2026 17:43:48 -0600 Subject: [PATCH 3/7] fix: Buffer live events and defer order subscriptions during restore During account restoration, live Nostr events could interfere with the integrity of the restored state, leading to inconsistencies or potential state loss. This change introduces a mechanism to: - Buffer all incoming live events via `MostroService` while restoration is active, processing them only after restoration completes. - Temporarily halt live order subscriptions (`limit: 0`) to prevent new order events from conflicting with historical data. This ensures a clean restoration process, prevents state corruption, and guarantees that all live events are eventually processed without loss or interference. --- .../subscriptions/subscription_manager.dart | 2 ++ lib/services/mostro_service.dart | 21 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index ea08f5f78..2af6e8016 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -9,6 +9,7 @@ import 'package:mostro_mobile/features/subscriptions/subscription.dart'; import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; +import 'package:mostro_mobile/features/restore/restore_mode_provider.dart'; /// Manages Nostr subscriptions across different parts of the application. /// @@ -128,6 +129,7 @@ class SubscriptionManager { return NostrFilter( kinds: [1059], p: sessions.map((s) => s.tradeKey.public).toList(), + limit: ref.read(isRestoringProvider) ? 0 : null, ); case SubscriptionType.chat: if (sessions.isEmpty) { diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 285c399bb..e85030c6b 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -22,6 +22,7 @@ class MostroService { Settings _settings; StreamSubscription? _ordersSubscription; + final List _restoreBuffer = []; MostroService(this.ref) : _settings = ref.read(settingsProvider); @@ -45,6 +46,13 @@ class MostroService { }, cancelOnError: false, ); + + // Flush buffered live events when restore completes (success or error path) + ref.listen(isRestoringProvider, (previous, next) { + if (previous == true && next == false) { + unawaited(_flushRestoreBuffer()); + } + }); } void dispose() { @@ -164,7 +172,8 @@ class MostroService { event.id ?? 'msg_${DateTime.now().millisecondsSinceEpoch}'; if (ref.read(isRestoringProvider)) { - logger.i('Restore in progress, skipping storage write for ${msg.action}'); + _restoreBuffer.add(event); + logger.i('Restore: buffered live event ${event.id} for ${msg.action}'); return; } @@ -179,6 +188,16 @@ class MostroService { } } + Future _flushRestoreBuffer() async { + if (_restoreBuffer.isEmpty) return; + final buffer = List.from(_restoreBuffer); + _restoreBuffer.clear(); + logger.i('Restore: flushing ${buffer.length} buffered live events'); + for (final event in buffer) { + await _onData(event); + } + } + Future _maybeLinkChildOrder( MostroMessage message, Session session, From 65fd8adf33dcd3da48bfdaa33d4579ad0bb031a9 Mon Sep 17 00:00:00 2001 From: BRACR10 Date: Wed, 10 Jun 2026 18:07:23 -0600 Subject: [PATCH 4/7] docs: Enhance explanation of dispute state restoration fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the documentation for "Issue 3 — Dispute State Not Persisted After Restore + App Kill" in `DISPUTE_CHAT_RESTORE.md`. This revision clarifies the two-part fix for robust account restoration: 1. `SubscriptionManager` now filters historical order events by passing `limit: 0` to relays during restore. 2. `MostroService._onData` buffers any incoming live events, applying them only after the restoration process has completed. This ensures a clean and consistent state on app relaunch by preventing interference from live or historical events during the restoration window. --- docs/architecture/DISPUTE_CHAT_RESTORE.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/architecture/DISPUTE_CHAT_RESTORE.md b/docs/architecture/DISPUTE_CHAT_RESTORE.md index 34788480a..9d81d880e 100644 --- a/docs/architecture/DISPUTE_CHAT_RESTORE.md +++ b/docs/architecture/DISPUTE_CHAT_RESTORE.md @@ -191,7 +191,7 @@ case and degrade gracefully (e.g. strip the peer field and continue parsing). ### Issue 3 — Dispute State Not Persisted After Restore + App Kill -**Status: Fixed** — `lib/services/mostro_service.dart` +**Status: Fixed** — `lib/services/mostro_service.dart`, `lib/features/subscriptions/subscription_manager.dart` #### Root Cause @@ -203,7 +203,15 @@ a stale relay event representing an earlier trade stage instead of the correct r #### Fix -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. +Two-part fix: + +1. **`SubscriptionManager`** passes `limit: 0` on the orders filter while `isRestoringProvider` + is true. Relays deliver only new events during the restore window — no historical replay. + +2. **`MostroService._onData`** buffers any live event that arrives during restore into + `_restoreBuffer` instead of discarding it. A `ref.listen(isRestoringProvider)` in `init()` + flushes the buffer through normal `_onData` processing once restore completes, so live + events are applied on top of the synthetic messages written by the restore process. + +This guarantees: historical events never arrive during restore (relay-side filter), live events +are not lost (client-side buffer), and state on relaunch is always correct. From 31e205b88e8abd04861ca2697ee6f6729f4f3d37 Mon Sep 17 00:00:00 2001 From: BRACR10 Date: Wed, 10 Jun 2026 18:47:57 -0600 Subject: [PATCH 5/7] fix: Orchestrate event buffering and subscription transitions during restore This refines the account restoration process by formalizing the management of incoming Nostr events and subscription states. Dedicated listeners in `SubscriptionManager` and `MostroService` now precisely control: - When subscriptions request `limit:0` to block historical backfill during restore. - When live events are buffered by `MostroService`. - When the buffer is flushed and subscriptions resume normal operation with `limit:null` after restoration completes. This ensures a clean, consistent state by preventing interference from live or historical events with the authoritative synthetic state built during restore. --- docs/architecture/DISPUTE_CHAT_RESTORE.md | 27 +++++++++++-------- .../subscriptions/subscription_manager.dart | 16 +++++++++++ lib/services/mostro_service.dart | 8 +++--- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/docs/architecture/DISPUTE_CHAT_RESTORE.md b/docs/architecture/DISPUTE_CHAT_RESTORE.md index 9d81d880e..2eedea5d0 100644 --- a/docs/architecture/DISPUTE_CHAT_RESTORE.md +++ b/docs/architecture/DISPUTE_CHAT_RESTORE.md @@ -62,7 +62,10 @@ RestoreService.importMnemonicAndRestore() ▼ restore(ordersMap, lastTradeIndex, ordersResponse, disputes) ├─ keyManager.setCurrentKeyIndex(lastTradeIndex + 1) - ├─ isRestoringProvider = true (blocks MostroService._onData processing) + ├─ isRestoringProvider = true + │ ├─ SubscriptionManager: orders filter uses limit:0 + │ │ → relay delivers NO historical events during restore window + │ └─ MostroService: any arriving event goes to _restoreBuffer │ ├─ FOR EACH order in ordersIds: │ ├─ derive tradeKey from tradeIndex @@ -75,24 +78,26 @@ RestoreService.importMnemonicAndRestore() │ │ ) │ ├─ sessionNotifier.saveSession(session) │ │ └─ triggers SubscriptionManager._updateAllSubscriptions() - │ │ → relay REQ recreated for this tradeKey + │ │ → relay REQ with limit:0 (no historical replay) │ └─ if peer != null: chatRoomsProvider.subscribe() │ ├─ Future.delayed(10 seconds) - │ └─ relay delivers historical gift-wrap events during this window; - │ MostroService._onData stores them in mostroStorage but - │ isRestoringProvider=true blocks state.updateWith() - │ - ├─ storage.deleteAll() - │ └─ clears relay-replayed events that arrived during 10s window - │ (prevents stale events from overwriting restore messages) + │ └─ only genuinely live events can arrive (limit:0 blocks history); + │ MostroService buffers them in _restoreBuffer instead of storing │ ├─ FOR EACH order in ordersResponse: │ ├─ build MostroMessage from OrderDetail + dispute state - │ ├─ storage.addMessage(key, message) + │ ├─ storage.addMessage(key, message) ← authoritative synthetic state │ └─ notifier.updateStateFromMessage(message) │ - └─ isRestoringProvider = false + ├─ isRestoringProvider = false + │ ├─ MostroService._restoreListener fires → _flushRestoreBuffer() + │ │ → buffered live events processed on top of synthetic state + │ └─ SubscriptionManager._restoreModeListener fires + │ → _updateAllSubscriptions() with limit:null + │ → relay backfill resumes normally; eventStorage dedup + │ discards already-seen events + └─ (done) ``` ### Dispute Chat Subscription During Restore diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index 2af6e8016..b564690e9 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -21,6 +21,7 @@ class SubscriptionManager { final Map _subscriptions = {}; late final ProviderSubscription _sessionListener; + ProviderSubscription? _restoreModeListener; final _ordersController = StreamController.broadcast(); final _chatController = StreamController.broadcast(); @@ -34,11 +35,25 @@ class SubscriptionManager { SubscriptionManager(this.ref) { _initSessionListener(); + _initRestoreModeListener(); // Ensure resources are released with provider/container lifecycle ref.onDispose(dispose); _initializeExistingSessions(); } + void _initRestoreModeListener() { + _restoreModeListener = ref.listen( + isRestoringProvider, + (previous, next) { + if (previous == true && next == false) { + // Rebuild subscriptions without limit:0 so relay backfill works normally + _updateAllSubscriptions(ref.read(sessionNotifierProvider)); + } + }, + fireImmediately: false, + ); + } + void _initSessionListener() { _sessionListener = ref.listen>( sessionNotifierProvider, @@ -344,6 +359,7 @@ class SubscriptionManager { void dispose() { _sessionListener.close(); + _restoreModeListener?.close(); unsubscribeAll(); _ordersController.close(); _chatController.close(); diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index e85030c6b..be01b8fa4 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -22,16 +22,15 @@ class MostroService { Settings _settings; StreamSubscription? _ordersSubscription; + ProviderSubscription? _restoreListener; final List _restoreBuffer = []; MostroService(this.ref) : _settings = ref.read(settingsProvider); void init() { - // Cancel any existing subscription to prevent leaks on re-init _ordersSubscription?.cancel(); + _restoreListener?.close(); - // Subscribe to the orders stream from SubscriptionManager - // The SubscriptionManager will automatically manage subscriptions based on SessionNotifier changes _ordersSubscription = ref .read(subscriptionManagerProvider) .orders @@ -48,7 +47,7 @@ class MostroService { ); // Flush buffered live events when restore completes (success or error path) - ref.listen(isRestoringProvider, (previous, next) { + _restoreListener = ref.listen(isRestoringProvider, (previous, next) { if (previous == true && next == false) { unawaited(_flushRestoreBuffer()); } @@ -57,6 +56,7 @@ class MostroService { void dispose() { _ordersSubscription?.cancel(); + _restoreListener?.close(); logger.i('MostroService disposed'); } From 29134046eee5d3ed20c665e3af5b2fc76b293f8d Mon Sep 17 00:00:00 2001 From: BRACR10 Date: Fri, 26 Jun 2026 09:35:27 -0600 Subject: [PATCH 6/7] fix: Remove redundant restore mode listener The `_restoreModeListener` was responsible for updating subscriptions when the restoration process completed, specifically by rebuilding them without the `limit:0` constraint. This functionality has now been consolidated into the broader orchestration of event buffering and subscription transitions, making this dedicated listener redundant. --- .../subscriptions/subscription_manager.dart | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index b564690e9..2af6e8016 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -21,7 +21,6 @@ class SubscriptionManager { final Map _subscriptions = {}; late final ProviderSubscription _sessionListener; - ProviderSubscription? _restoreModeListener; final _ordersController = StreamController.broadcast(); final _chatController = StreamController.broadcast(); @@ -35,25 +34,11 @@ class SubscriptionManager { SubscriptionManager(this.ref) { _initSessionListener(); - _initRestoreModeListener(); // Ensure resources are released with provider/container lifecycle ref.onDispose(dispose); _initializeExistingSessions(); } - void _initRestoreModeListener() { - _restoreModeListener = ref.listen( - isRestoringProvider, - (previous, next) { - if (previous == true && next == false) { - // Rebuild subscriptions without limit:0 so relay backfill works normally - _updateAllSubscriptions(ref.read(sessionNotifierProvider)); - } - }, - fireImmediately: false, - ); - } - void _initSessionListener() { _sessionListener = ref.listen>( sessionNotifierProvider, @@ -359,7 +344,6 @@ class SubscriptionManager { void dispose() { _sessionListener.close(); - _restoreModeListener?.close(); unsubscribeAll(); _ordersController.close(); _chatController.close(); From 38454f9f77f92d0c21c5aa2da8210f8a66acf048 Mon Sep 17 00:00:00 2001 From: BRACR10 Date: Fri, 26 Jun 2026 18:28:36 -0600 Subject: [PATCH 7/7] fix: Ensure consistent message timestamps during restore and NIP-59 parsing Standardizes timestamps for synthetic messages generated during account restoration by using a single `_restoreStartTime`. This prevents inconsistencies from dynamically generated timestamps in message keys and content. Additionally, NIP-59 gift-wrapped events now use the `createdAt` of the inner, decrypted event as the message timestamp. This captures the true send time, as the outer event's `createdAt` is randomized for privacy. These changes improve overall state consistency, particularly during restoration. --- lib/features/restore/restore_manager.dart | 14 ++++++-------- lib/services/mostro_service.dart | 9 +++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/features/restore/restore_manager.dart b/lib/features/restore/restore_manager.dart index d98763dfa..a1c839b44 100644 --- a/lib/features/restore/restore_manager.dart +++ b/lib/features/restore/restore_manager.dart @@ -58,6 +58,7 @@ class RestoreService { NostrKeyPairs? _masterKey; // Master key pair used during restore process bool _operationInProgress = false; Completer? _operationCompleter; + int _restoreStartTime = 0; RestoreService(this.ref); @@ -806,14 +807,12 @@ class RestoreService { id: orderDetail.id, action: action, payload: dispute, - timestamp: - orderDetail.createdAt ?? - DateTime.now().millisecondsSinceEpoch, + timestamp: _restoreStartTime, ); // Save dispute message to storage final disputeKey = - '${orderDetail.id}_restore_${action.value}_${DateTime.now().millisecondsSinceEpoch}'; + '${orderDetail.id}_restore_${action.value}_$_restoreStartTime'; await storage.addMessage(disputeKey, disputeMessage); // Update state with dispute message @@ -848,14 +847,12 @@ class RestoreService { id: orderDetail.id, action: action, payload: order, - timestamp: - orderDetail.createdAt ?? - DateTime.now().millisecondsSinceEpoch, + timestamp: _restoreStartTime, ); // Save order message to storage final key = - '${orderDetail.id}_restore_${action.value}_${DateTime.now().millisecondsSinceEpoch}'; + '${orderDetail.id}_restore_${action.value}_$_restoreStartTime'; await storage.addMessage(key, mostroMessage); // Update state with order message @@ -903,6 +900,7 @@ class RestoreService { _operationInProgress = true; _operationCompleter = Completer(); + _restoreStartTime = DateTime.now().millisecondsSinceEpoch; bool success = false; bool noHistoryFound = false; try { diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 710d144d7..43844d281 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -141,6 +141,9 @@ class MostroService { // decrypts straight to the tuple. Both converge on jsonDecode below. String? content; String? decryptedId; + // Inner rumor's created_at is the real send time (outer gift wrap is + // NIP-59 randomized for privacy). Use it for timestamp anchoring below. + DateTime? innerCreatedAt; if (event.kind == 14) { content = await NostrUtils.decryptNIP44DirectEvent( event, @@ -151,6 +154,7 @@ class MostroService { final decryptedEvent = await event.unWrap(privateKey); content = decryptedEvent.content; decryptedId = decryptedEvent.id; + innerCreatedAt = decryptedEvent.createdAt; } if (content == null) return; @@ -192,6 +196,11 @@ class MostroService { return; } + // Use inner rumor's created_at (real send time) — outer gift wrap is + // NIP-59 randomized. Historical events have old inner timestamps (sort + // below synthetics); live events have recent inner timestamps (gate passes). + msg.timestamp ??= innerCreatedAt?.millisecondsSinceEpoch; + await messageStorage.addMessage(messageKey, msg); logger.i( 'Received DM, Event ID: ${decryptedId ?? event.id} with payload: $content',