fix: resolve trade index desync, session lookup crash#566
Conversation
…ntDo variant This commit addresses several interconnected issues discovered when running the app on Linux desktop after clearing SharedPreferences data. **Session creation crash (session_notifier.dart)** The duplicate-session check in newSession() compared s.orderId == orderId even when orderId was null. This caused any session with a null orderId (i.e., any pending order creation) to match, returning a stale session without registering the new requestId in _requestIdToSession. The subsequent _getSession() lookup by requestId then threw "No session found for requestId". Fixed by only performing the duplicate check when orderId is non-null. **Trade index desync after SharedPreferences loss (app_init_provider.dart, restore_manager.dart)** On Linux, flutter_secure_storage uses libsecret/GNOME Keyring, which persists outside ~/.local/share/. When that directory is deleted, the mnemonic survives but the trade key index (stored in SharedPreferences) resets to the default of 1. Mostro then rejects all messages with invalid_trade_index because the index is far behind the server's record. Added a lightweight syncTradeIndex() method to RestoreService that fetches only the last-trade-index from Mostro and updates local storage, without performing a full restore. The app initializer now detects the inconsistency (master key exists but trade index is at default) and triggers this sync in the background at startup. **invalid_trade_index not handled in order creation (add_order_notifier.dart)** When Mostro responded with invalid_trade_index during order creation, the app showed a snackbar but did not clean up the temporary session or navigate back to the order book, leaving the UI stuck. Added invalidTradeIndex to the terminal-error branch that triggers _cleanupSessionAndNavigateBack(). **Missing too_many_requests CantDo variant (cant_do_reason.dart)** Mostro can respond with cant_do reason "too_many_requests", which was not present in the CantDoReason enum. This caused a FormatException during JSON parsing, silently dropping the message. Added the variant to the enum, localization strings (en/es/it), and the notification message map.
StreamSubscription.cancel() returns a Future. Not awaiting it could cause incomplete cleanup if the method is called in quick succession. This aligns with the awaited cleanup pattern used elsewhere in the RestoreService.
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 53 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughAdds a new CantDo reason Changes
Sequence DiagramsequenceDiagram
participant App as App Initializer
participant KM as KeyManager
participant RS as RestoreService
participant Mostro as Mostro API
participant Sub as Temp Subscription
App->>KM: hasMasterKey()
alt master key exists && !hasPersistedTradeKeyIndex
App->>RS: syncTradeIndex() (unawaited)
RS->>RS: if _operationInProgress -> return
RS->>KM: read masterKey / derive tempTradeKey
RS->>Mostro: send last-trade-index request
Mostro-->>Sub: emit event with trade index
Sub-->>RS: deliver event
RS->>RS: extract tradeIndex
alt tradeIndex >= 0
RS->>KM: set current key index = tradeIndex + 1
end
RS->>Sub: cancel subscription
RS->>RS: clear temp keys & _operationInProgress=false
else
App->>RS: skip sync
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
lib/features/order/notifiers/add_order_notifier.dart (1)
49-54: Consider aligning terminal-error naming/logging with the new branch logic.Now that
invalid_trade_indexis also terminal here, it would help to rename/comment/log this path generically (instead of fiat-specific wording) to keep debugging signals accurate.♻️ Small cleanup idea
- /// Clean up session and navigate back for invalid_fiat_currency error + /// Clean up session and navigate back for terminal cant-do errors void _cleanupSessionAndNavigateBack() { - logger.i('Cleaning up session and navigating back after invalid_fiat_currency'); + logger.i('Cleaning up session and navigating back after terminal cant-do error');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/order/notifiers/add_order_notifier.dart` around lines 49 - 54, The branch handling terminal errors currently treats invalidTradeIndex alongside invalidFiatCurrency but uses fiat-specific wording; update the logic around CantDo handling (referencing CantDo, CantDoReason, and the _resetForRetry() call) to treat terminal errors generically: rename any variable/comment/log message that mentions fiat to something like "terminal error" or "invalid request parameter", and update logging/messages for the else branch (the path checking CantDoReason.invalidFiatCurrency and CantDoReason.invalidTradeIndex) so it describes a generic terminal error instead of fiat-specific wording to make debugging accurate.lib/shared/providers/app_init_provider.dart (1)
24-29: Desync detection condition is a bit too broad.
tradeKeyIndex == 1can also be a valid steady state, so this may triggersyncTradeIndex()unnecessarily. Consider gating on “missing persisted index” (or equivalent explicit desync signal) rather than the value alone.💡 Possible direction
- if (hadMasterKey && keyManager.tradeKeyIndex == 1) { + if (hadMasterKey && await keyManager.isTradeIndexMissingLocally()) { unawaited(ref.read(restoreServiceProvider).syncTradeIndex()); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/shared/providers/app_init_provider.dart` around lines 24 - 29, The current desync check uses keyManager.tradeKeyIndex == 1 which can be a valid state; instead gate the sync on an explicit “missing persisted index” signal: replace the condition with hadMasterKey && !keyManager.hasPersistedTradeIndex (or hadMasterKey && keyManager.persistentTradeIndex == null) and keep the call to unawaited(ref.read(restoreServiceProvider).syncTradeIndex()); if KeyManager lacks a hasPersistedTradeIndex/persistentTradeIndex accessor, add one to KeyManager and use it here so we only call syncTradeIndex() when the persisted index is actually absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/data/models/enums/cant_do_reason.dart`:
- Around line 26-27: The switch that formats/displayes CantDoReason values in
lib/features/trades/widgets/mostro_message_detail_widget.dart doesn't handle the
new enum value CantDoReason.tooManyRequests, so it falls through to default; add
a case for CantDoReason.tooManyRequests in the same switch (or the helper that
maps CantDoReason to UI text) and return the appropriate localized message using
the same localization pattern used for other cases (match how pendingOrderExists
is handled) so users see the specific "too many requests" reason instead of the
generic default.
In `@lib/features/restore/restore_manager.dart`:
- Around line 980-982: The catch in syncTradeIndex currently logs only the error
object; change the handler to capture the stack trace (use catch (e,
stackTrace)) and pass it into the logger.e call (e.g., logger.e('syncTradeIndex:
failed', error: e, stackTrace: stackTrace)) so failures include stack traces
like the rest of restore_manager.dart.
- Around line 956-987: syncTradeIndex mutates shared instance fields
(_tempSubscription, _tempTradeKey, _masterKey, _currentStage) that
initRestoreProcess also uses, causing race conditions; either serialize the
operations with an operation-level lock or make syncTradeIndex use only local
variables and avoid touching those shared fields. Add a simple mutex/flag (e.g.,
a private Future<void>? _syncLock or bool _syncInProgress) and await/guard
around the critical section in syncTradeIndex (or refactor to use local vars for
tempSubscription/tempTradeKey/masterKey/currentStage and only read/write
persistent state via keyManager/setCurrentKeyIndex), ensuring you still cancel
the local subscription and clear local state in finally without altering the
shared fields used by initRestoreProcess.
---
Nitpick comments:
In `@lib/features/order/notifiers/add_order_notifier.dart`:
- Around line 49-54: The branch handling terminal errors currently treats
invalidTradeIndex alongside invalidFiatCurrency but uses fiat-specific wording;
update the logic around CantDo handling (referencing CantDo, CantDoReason, and
the _resetForRetry() call) to treat terminal errors generically: rename any
variable/comment/log message that mentions fiat to something like "terminal
error" or "invalid request parameter", and update logging/messages for the else
branch (the path checking CantDoReason.invalidFiatCurrency and
CantDoReason.invalidTradeIndex) so it describes a generic terminal error instead
of fiat-specific wording to make debugging accurate.
In `@lib/shared/providers/app_init_provider.dart`:
- Around line 24-29: The current desync check uses keyManager.tradeKeyIndex == 1
which can be a valid state; instead gate the sync on an explicit “missing
persisted index” signal: replace the condition with hadMasterKey &&
!keyManager.hasPersistedTradeIndex (or hadMasterKey &&
keyManager.persistentTradeIndex == null) and keep the call to
unawaited(ref.read(restoreServiceProvider).syncTradeIndex()); if KeyManager
lacks a hasPersistedTradeIndex/persistentTradeIndex accessor, add one to
KeyManager and use it here so we only call syncTradeIndex() when the persisted
index is actually absent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b9e8dd7d-d839-4292-8e2b-9efd59ff4f35
📒 Files selected for processing (9)
lib/data/models/enums/cant_do_reason.dartlib/features/order/notifiers/add_order_notifier.dartlib/features/restore/restore_manager.dartlib/l10n/intl_en.arblib/l10n/intl_es.arblib/l10n/intl_it.arblib/shared/notifiers/session_notifier.dartlib/shared/providers/app_init_provider.dartlib/shared/widgets/notification_listener_widget.dart
- Add tooManyRequests case to _getCantDoMessage switch in mostro_message_detail_widget.dart so users see the specific localized message instead of the generic default fallback. - Add operation guard (_operationInProgress) to RestoreService to prevent race conditions between syncTradeIndex and initRestoreProcess sharing instance fields (_tempSubscription, _tempTradeKey, _masterKey, _currentStage). Both methods now set the flag on entry and clear it in finally. - Capture stack trace in syncTradeIndex catch block (catch (e, stack)) and pass it to logger.e for consistency with the rest of restore_manager.dart. - Replace tradeKeyIndex == 1 heuristic in app_init_provider with explicit hasPersistedTradeKeyIndex() check via KeyStorage and KeyManager. This correctly distinguishes between a genuinely new user at index 1 and a missing SharedPreferences entry that defaults to 1. - Update _cleanupSessionAndNavigateBack doc comment and log message to say "terminal error" instead of "invalid_fiat_currency" since the method now also handles invalidTradeIndex.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/features/restore/restore_manager.dart`:
- Around line 824-826: initRestoreProcess() must guard against concurrent
operations like syncTradeIndex() to avoid corrupting shared state
(_tempSubscription, _currentCompleter, etc.); add a check at the start of
initRestoreProcess() that returns/fails early (and logs) when
_operationInProgress is true instead of setting _operationInProgress = true
unconditionally, so the method only proceeds when no other operation is active
and then sets _operationInProgress before touching shared fields.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bd638260-2dc0-4a89-98c4-0a1a650ab3fd
📒 Files selected for processing (6)
lib/features/key_manager/key_manager.dartlib/features/key_manager/key_storage.dartlib/features/order/notifiers/add_order_notifier.dartlib/features/restore/restore_manager.dartlib/features/trades/widgets/mostro_message_detail_widget.dartlib/shared/providers/app_init_provider.dart
🚧 Files skipped from review as they are similar to previous changes (2)
- lib/features/order/notifiers/add_order_notifier.dart
- lib/shared/providers/app_init_provider.dart
initRestoreProcess() now mirrors the same guard pattern already used by syncTradeIndex() it checks _operationInProgress at the top and returns false early (with a warning log) if another operation is active, preventing concurrent corruption of shared state (_tempSubscription, _currentCompleter, etc.). Analyzer passes clean.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/features/restore/restore_manager.dart`:
- Around line 988-993: The code currently skips persisting when
response.tradeIndex == 0, so wallets with no prior trades never get the default
persisted; update the syncTradeIndex path to always persist the default index
instead of only when response.tradeIndex > 0 — call
keyManager.setCurrentKeyIndex(response.tradeIndex + 1) (and log via logger.i)
for tradeIndex == 0 as well (e.g., change the conditional or remove it) so the
defaulted value is written to storage and prevents repeated resync on cold
start.
- Around line 984-985: A race occurs because _sendLastTradeIndexRequest() can
trigger a reply before _currentCompleter is set by _waitForEvent(), causing
_handleTempSubscriptionsResponse() to drop the event; fix by arming the awaiting
mechanism before sending the request: create or initialize the completer for
RestoreStage.gettingTradeIndex (i.e. invoke the logic that _waitForEvent() would
set up) prior to calling _sendLastTradeIndexRequest(), then call _waitForEvent()
or await the already-created completer; reference the methods
_sendLastTradeIndexRequest(), _waitForEvent(),
_handleTempSubscriptionsResponse(), and the
_currentCompleter/RestoreStage.gettingTradeIndex symbols when making the change.
- Around line 824-828: The guard in initRestoreProcess that returns false when
_operationInProgress is true causes silent no-ops upstream (caller doesn’t check
the bool); change initRestoreProcess (and related call sites) so that instead of
silently returning false it either awaits the active operation to finish or
throws a handled error. Concretely, replace the early return on
_operationInProgress in initRestoreProcess with one of: 1) wait for the existing
operation to complete (e.g., track an in-progress Future/Completer and await it)
and then proceed to run syncTradeIndex()/restore logic, or 2) throw a specific
exception (e.g., RestoreInProgressException) so callers can surface a
user-facing error; update callers that currently ignore the boolean to handle
the thrown exception if you choose option 2. Ensure you reference and update the
_operationInProgress flag, the in-progress Future/Completer you add, and the
initRestoreProcess method so restores are not silently skipped.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 02906c67-e074-4857-a813-ef9f634d7716
📒 Files selected for processing (1)
lib/features/restore/restore_manager.dart
… restore - Arm completer before sending trade index request in syncTradeIndex() to prevent reply from being dropped when it arrives before _waitForEvent() sets up _currentCompleter - Always persist trade index in syncTradeIndex() even when tradeIndex is 0, preventing repeated resync on cold start for new wallets - Replace silent return false in initRestoreProcess() with awaiting the in-progress operation via Completer, so callers get meaningful results instead of silent no-ops
There was a problem hiding this comment.
🧹 Nitpick comments (2)
lib/features/restore/restore_manager.dart (2)
1012-1018: Consider clearing_currentCompleterin finally for consistency.
initRestoreProcess()clears_currentCompleterin its finally block (line 951), butsyncTradeIndex()does not. While the completed completer won't cause functional issues, clearing it maintains consistent cleanup patterns across both methods.♻️ Suggested cleanup addition
} finally { await _tempSubscription?.cancel(); _tempSubscription = null; + _currentCompleter = null; _tempTradeKey = null; _masterKey = null; _operationInProgress = false; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/restore/restore_manager.dart` around lines 1012 - 1018, The finally block in syncTradeIndex() should mirror initRestoreProcess() by clearing the shared cleanup state; specifically, add resetting _currentCompleter to null in the finally block alongside _tempSubscription, _tempTradeKey, _masterKey, and _operationInProgress so the completer is consistently cleared after syncTradeIndex() completes or fails.
971-1018: Good implementation addressing prior feedback.The method correctly:
- Arms the completer before sending the request (lines 992-993)
- Logs failures with stack trace (line 1011)
- Always persists the trade index regardless of value (line 1006)
One minor observation:
syncTradeIndex()sets_operationInProgress = truebut doesn't set_operationCompleter. IfinitRestoreProcess()is called whilesyncTradeIndex()is running, it will see_operationInProgress == trueand_operationCompleter == null, causing it to returnfalseimmediately (line 831) rather than awaiting completion.Given that
syncTradeIndex()is a brief background operation (~10s max) and this race is unlikely in practice, this is acceptable. For full consistency, you could optionally set a completer here too.♻️ Optional: Add completer for full coordination symmetry
Future<void> syncTradeIndex() async { if (_operationInProgress) { logger.w('syncTradeIndex: another operation in progress, skipping'); return; } final keyManager = ref.read(keyManagerProvider); if (keyManager.masterKeyPair == null) { logger.w('syncTradeIndex: no master key, skipping'); return; } _operationInProgress = true; + _operationCompleter = Completer<bool>(); try { // ... existing code ... + _operationCompleter?.complete(true); } catch (e, stack) { logger.e('syncTradeIndex: failed', error: e, stackTrace: stack); + _operationCompleter?.complete(false); } finally { await _tempSubscription?.cancel(); _tempSubscription = null; _tempTradeKey = null; _masterKey = null; _operationInProgress = false; + _operationCompleter = null; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/restore/restore_manager.dart` around lines 971 - 1018, syncTradeIndex sets _operationInProgress = true but never assigns _operationCompleter, so concurrent callers to initRestoreProcess (which checks _operationInProgress and _operationCompleter) may bail out instead of awaiting completion; fix by creating and assigning a Completer<bool> to _operationCompleter at the start of syncTradeIndex (before async work) and ensure you complete or completeError that completer in both the try and catch/finally paths (use the existing finally block to complete if not already completed), so initRestoreProcess can await _operationCompleter while _operationInProgress is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@lib/features/restore/restore_manager.dart`:
- Around line 1012-1018: The finally block in syncTradeIndex() should mirror
initRestoreProcess() by clearing the shared cleanup state; specifically, add
resetting _currentCompleter to null in the finally block alongside
_tempSubscription, _tempTradeKey, _masterKey, and _operationInProgress so the
completer is consistently cleared after syncTradeIndex() completes or fails.
- Around line 971-1018: syncTradeIndex sets _operationInProgress = true but
never assigns _operationCompleter, so concurrent callers to initRestoreProcess
(which checks _operationInProgress and _operationCompleter) may bail out instead
of awaiting completion; fix by creating and assigning a Completer<bool> to
_operationCompleter at the start of syncTradeIndex (before async work) and
ensure you complete or completeError that completer in both the try and
catch/finally paths (use the existing finally block to complete if not already
completed), so initRestoreProcess can await _operationCompleter while
_operationInProgress is true.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 183392d4-bfa7-4a5e-a0a4-c632c6240b20
📒 Files selected for processing (1)
lib/features/restore/restore_manager.dart
This commit addresses several interconnected issues discovered when
running the app on Linux desktop after clearing SharedPreferences data.
Session creation crash (session_notifier.dart)
The duplicate-session check in newSession() compared s.orderId == orderId
even when orderId was null. This caused any session with a null orderId
(i.e., any pending order creation) to match, returning a stale session
without registering the new requestId in _requestIdToSession. The
subsequent _getSession() lookup by requestId then threw
"No session found for requestId". Fixed by only performing the duplicate
check when orderId is non-null.
Trade index desync after SharedPreferences loss (app_init_provider.dart, restore_manager.dart)
On Linux, flutter_secure_storage uses libsecret/GNOME Keyring, which
persists outside ~/.local/share/. When that directory is deleted,
the mnemonic survives but the trade key index (stored in
SharedPreferences) resets to the default of 1. Mostro then rejects
all messages with invalid_trade_index because the index is far behind
the server's record.
Added a lightweight syncTradeIndex() method to RestoreService that
fetches only the last-trade-index from Mostro and updates local
storage, without performing a full restore. The app initializer now
detects the inconsistency (master key exists but trade index is at
default) and triggers this sync in the background at startup.
invalid_trade_index not handled in order creation (add_order_notifier.dart)
When Mostro responded with invalid_trade_index during order creation,
the app showed a snackbar but did not clean up the temporary session
or navigate back to the order book, leaving the UI stuck. Added
invalidTradeIndex to the terminal-error branch that triggers
_cleanupSessionAndNavigateBack().
Missing too_many_requests CantDo variant (cant_do_reason.dart)
Mostro can respond with cant_do reason "too_many_requests", which was
not present in the CantDoReason enum. This caused a FormatException
during JSON parsing, silently dropping the message. Added the variant
to the enum, localization strings (en/es/it), and the notification
message map.
Summary by CodeRabbit
New Features
Bug Fixes