Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 12 additions & 32 deletions docs/architecture/DISPUTE_CHAT_RESTORE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
40 changes: 17 additions & 23 deletions docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> 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<String, dynamic>) return false;

final payload = wrapper['payload'];
if (payload == null || payload is! Map<String, dynamic>) 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
Expand Down
6 changes: 6 additions & 0 deletions lib/services/mostro_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}',
Expand Down
Loading