Skip to content

feat(bond): cause-aware bond-slashed handling (dispute vs timeout)#619

Merged
grunch merged 7 commits into
mainfrom
add-slash-bond-dispute-dm
Jun 18, 2026
Merged

feat(bond): cause-aware bond-slashed handling (dispute vs timeout)#619
grunch merged 7 commits into
mainfrom
add-slash-bond-dispute-dm

Conversation

@Catrya

@Catrya Catrya commented Jun 17, 2026

Copy link
Copy Markdown
Member

The daemon sends the same bond-slashed notice for timeout and dispute-resolution slashes (identical payload, no reason field). The client assumed timeout always, causing two issues, both fixed here:

  1. The forfeiture notification always said "you didn't respond in time", even on a dispute slash.
  2. On a dispute slash the loser's session was deleted, so the order vanished from My Trades and showed the wrong buy/sell direction when opened.

Changes

  • Infer the slash cause from the order history (pure helpers in bond_slash_helpers.dart).
  • Notification picks the timeout/dispute message + detail variant (en/es/it/de/fr).
  • Order details shows a "bond lost due to dispute resolution" line (dispute only),
    with the amount from the message.
  • bond-slashed deletes the session only on timeout; a dispute slash keeps it
    (trade stays in My Trades with its role intact).
  • Docs updated.

How to test manually

  1. Enable bonds on a regtest node (apply_to = both) and take/create a bonded order.
  2. Dispute slash: open a dispute, have the admin settle/cancel directing a slash against you. Expect: the notification says it was due to the dispute resolution; the order stays in My Trades showing the correct buy/sell direction; order details shows the "bond lost due to dispute resolution" line with the amount.
  3. Timeout slash: let a waiting-state deadline elapse on a bonded order (slash_on_waiting_timeout = true). Expect: notification says you didn't respond in time; the order is removed from My Trades; no line in order details.

Summary by CodeRabbit

  • New Features

    • Bond loss notifications now distinguish between dispute resolutions and timeouts, displaying tailored messages for each scenario.
    • Improved order session handling based on the type of bond loss.
  • Documentation

    • Updated anti-abuse bond specification with expanded helper coverage and clarified slash behavior distinctions.
  • Tests

    • Added test coverage for bond slash classification logic.

Catrya added 5 commits June 16, 2026 18:22
  - Add pure bondSlashCause(): dispute when the history has a dispute/admin resolution, timeout otherwise
  - Add orderBondWasSlashed() to detect a bond-slashed notice in the history
  - Infer the slash cause (timeout vs dispute) from order history at notification creation and persist it
  - Select the message and detail variants by cause in the mapper and the forfeiture dialog
  - Add dispute-variant strings in en/es/it/de/fr
  - Add a durable order-details line shown only when the bond was slashed by a dispute resolution
  - Read the amount from the bond-slashed message via slashedBondAmount()
  - Timeout slashes show nothing here; add the localized notice in en/es/it/de/fr
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f9464d7-9c1c-47ce-91cd-f70016110afb

📥 Commits

Reviewing files that changed from the base of the PR and between bad3c95 and fdbcd8a.

📒 Files selected for processing (1)
  • lib/features/trades/widgets/mostro_message_detail_widget.dart

Walkthrough

Adds bond_slash_helpers.dart with a BondSlashCause enum and four pure functions that classify a bond-slash event as timeout or dispute from order message history. The inferred cause is persisted into notification data, used to branch notification message keys, dialog content, session deletion behavior in AbstractMostroNotifier, and a new trade message detail rendering path. Five ARB locale files and architecture documentation are updated accordingly.

Changes

Bond-slash dispute differentiation

Layer / File(s) Summary
BondSlashCause enum and pure helpers
lib/shared/utils/bond_slash_helpers.dart, test/shared/utils/bond_slash_helpers_test.dart
Introduces BondSlashCause (timeout/dispute) and four exported helpers: bondSlashCause, orderBondWasSlashed, bondSlashIsTimeout, slashedBondAmount, all derived from order message history; full unit test suite covers all four functions including defensive/edge cases.
Slash-cause inference in notification extractor
lib/features/notifications/utils/notification_data_extractor.dart
NotificationDataExtractor fetches stored order message history for Action.bondSlashed, infers cause via bondSlashCause, defaults to timeout on failure, and writes slash_cause into the notification values map.
Notification message mapping and dialog rendering
lib/features/notifications/utils/notification_message_mapper.dart, lib/features/notifications/widgets/notification_item.dart
NotificationMessageMapper branches on slash_cause == 'dispute' to return notification_bond_slashed_dispute_message and resolves it in _resolveLocalizationKey; NotificationItem dialog selects between dispute-specific and generic detail strings based on the persisted slash_cause.
Session deletion conditioned on slash cause
lib/features/order/notifiers/abstract_mostro_notifier.dart
Action.bondSlashed handler now reads stored Mostro messages, cancels the deferred deletion timer only on successful read, and deletes the order session only for timeout slashes; dispute slashes retain the session.
Bond-lost-by-dispute notice in trade message detail widget
lib/features/trades/widgets/mostro_message_detail_widget.dart
Adds Action.bondSlashed branch in _renderActionMessage routing to _getBondSlashedMessage, which extracts slashedBondAmount from history and returns a localized bondLostByDisputeNotice string or empty string when data is unavailable.
Localization strings for dispute bond-slash scenario
lib/l10n/intl_en.arb, lib/l10n/intl_de.arb, lib/l10n/intl_es.arb, lib/l10n/intl_fr.arb, lib/l10n/intl_it.arb
Adds notification_bond_slashed_dispute_message, notification_bond_slashed_dispute_detail, and bondLostByDisputeNotice keys with amount/orderId/fiatAmount/fiatCode/paymentMethod placeholders across all five supported locales.
Architecture documentation
docs/architecture/ANTI_ABUSE_BOND.md
Expands the bond-slashed section to document timeout vs dispute behavior, slash_cause persistence, heuristic inference limitation, new helper and test file coverage; bumps Last Updated to 2026-06-16.

Sequence Diagram(s)

sequenceDiagram
  participant MostroEvent as Mostro Event
  participant Extractor as NotificationDataExtractor
  participant Helpers as bond_slash_helpers
  participant Storage as OrderMessageStorage
  participant Mapper as NotificationMessageMapper
  participant UI as NotificationItem / MostroMessageDetail

  MostroEvent->>Extractor: Action.bondSlashed received
  Extractor->>Storage: fetchMessages(orderId)
  Storage-->>Extractor: List<MostroMessage>
  Extractor->>Helpers: bondSlashCause(messages)
  Helpers-->>Extractor: BondSlashCause.timeout | .dispute
  Extractor-->>Mapper: values{slash_cause, amount, order_id, ...}
  Mapper->>Mapper: slash_cause == 'dispute'?
  alt dispute
    Mapper-->>UI: notification_bond_slashed_dispute_message
    UI->>UI: show dispute-specific detail dialog
  else timeout
    Mapper-->>UI: notification_bond_slashed_message
    UI->>UI: show generic timeout detail dialog
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • MostroP2P/mobile#596: Both PRs extend the same Action switch in MostroMessageDetail._renderActionMessage — PR #596 adds addBondInvoice, this PR adds bondSlashed.
  • MostroP2P/mobile#604: Both PRs wire Action.bondSlashed end-to-end through notification parsing, mapping, dialog rendering, and AbstractMostroNotifier session deletion; this PR extends that foundation with dispute vs timeout classification.
  • MostroP2P/mobile#616: Both PRs modify the same docs/architecture/ANTI_ABUSE_BOND.md file, with overlapping coverage of the bond-slashed behavior and session lifecycle sections.

Suggested reviewers

  • AndreaDiazCorreia
  • BraCR10
  • mostronatorcoder

Poem

🐇 A bond was slashed — but why, you ask?
Was it timeout's cold, indifferent grasp?
Or dispute's fire that burned the deal?
Now helpers infer with heuristic zeal,
slash_cause persisted, the UI knows best —
Dispute keeps sessions, timeout gets the rest! 🪙

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: implementing cause-aware bond-slashed handling that distinguishes between dispute and timeout scenarios.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-slash-bond-dispute-dm

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/shared/utils/bond_slash_helpers_test.dart (1)

24-79: ⚡ Quick win

Add regression coverage for legacy and mixed history markers

The suite doesn’t currently lock behavior for legacy dispute/admin actions (Action.dispute, Action.adminSettle, Action.adminCancel) or a mixed canceled + dispute history. Adding those cases will prevent silent drift in slash classification and timeout deletion behavior.

Suggested test additions
+    test('dispute for legacy dispute/admin actions', () {
+      expect(bondSlashCause([_msg(Action.dispute)]), BondSlashCause.dispute);
+      expect(
+        bondSlashCause([_msg(Action.adminSettle)]),
+        BondSlashCause.dispute,
+      );
+      expect(
+        bondSlashCause([_msg(Action.adminCancel)]),
+        BondSlashCause.dispute,
+      );
+    });
@@
+    test('false when canceled and dispute markers coexist', () {
+      expect(
+        bondSlashIsTimeout([
+          _msg(Action.canceled),
+          _msg(Action.disputeInitiatedByPeer),
+          _bondSlashedMsg(300),
+        ]),
+        isFalse,
+      );
+    });

Also applies to: 107-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/shared/utils/bond_slash_helpers_test.dart` around lines 24 - 79, The
bondSlashCause test group is missing regression coverage for legacy action types
and mixed history scenarios. Add new test cases within the bondSlashCause group
to cover legacy Action types such as Action.dispute, Action.adminSettle, and
Action.adminCancel, as well as mixed history patterns combining canceled with
dispute-related actions. This will prevent silent drift in slash classification
behavior for these edge cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/features/order/notifiers/abstract_mostro_notifier.dart`:
- Around line 270-280: The session deletion logic has two issues: the
_bondCancelDeletionTimers.remove(orderId)?.cancel() call occurs before
validating the message history, which can strand timeout sessions if the history
read fails, and the bondSlashIsTimeout() check doesn't verify that dispute or
admin actions don't exist in the bondSlashMessages. Move the timer cancellation
to after the getAllMessagesForOrderId() call succeeds, and update the deletion
condition to ensure the session is only deleted when bondSlashIsTimeout()
returns true AND the bondSlashMessages contain no dispute or admin actions that
would indicate a terminal trade that should be preserved.

In `@lib/shared/utils/bond_slash_helpers.dart`:
- Around line 17-22: The _disputeActions set on line 17 is incomplete and
missing the base action variants Action.dispute, Action.adminSettle, and
Action.adminCancel, which should be included alongside the existing variant
actions. Additionally, the timeout detection logic around line 40-41 currently
classifies any Action.canceled as a timeout without checking if dispute markers
also exist, which can misclassify dispute slashes. Fix this by adding the
missing base action variants to the _disputeActions set, and modify the timeout
detection logic to ensure it only classifies an action as a timeout when no
dispute markers are present, preventing incorrect classification of
dispute-related actions as simple timeouts.

---

Nitpick comments:
In `@test/shared/utils/bond_slash_helpers_test.dart`:
- Around line 24-79: The bondSlashCause test group is missing regression
coverage for legacy action types and mixed history scenarios. Add new test cases
within the bondSlashCause group to cover legacy Action types such as
Action.dispute, Action.adminSettle, and Action.adminCancel, as well as mixed
history patterns combining canceled with dispute-related actions. This will
prevent silent drift in slash classification behavior for these edge cases.
🪄 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: 21d47ee8-136e-43e5-a145-d8c6a5d1818e

📥 Commits

Reviewing files that changed from the base of the PR and between d947795 and 16d889e.

📒 Files selected for processing (13)
  • docs/architecture/ANTI_ABUSE_BOND.md
  • lib/features/notifications/utils/notification_data_extractor.dart
  • lib/features/notifications/utils/notification_message_mapper.dart
  • lib/features/notifications/widgets/notification_item.dart
  • lib/features/order/notifiers/abstract_mostro_notifier.dart
  • lib/features/trades/screens/trade_detail_screen.dart
  • lib/l10n/intl_de.arb
  • lib/l10n/intl_en.arb
  • lib/l10n/intl_es.arb
  • lib/l10n/intl_fr.arb
  • lib/l10n/intl_it.arb
  • lib/shared/utils/bond_slash_helpers.dart
  • test/shared/utils/bond_slash_helpers_test.dart

Comment thread lib/features/order/notifiers/abstract_mostro_notifier.dart Outdated
Comment thread lib/shared/utils/bond_slash_helpers.dart

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the direction of this PR, but I do see one real correctness issue that I would block on before merge.

Blocking issue:

  • In notification_data_extractor.dart, the slash-cause inference reads history with getAllMessagesForOrderId(event.id!).
  • But in that same branch the canonical order identity is coming from the payload (order.id), and the extractor later persists order_id: order.id into the notification data.
  • If event.id here is the incoming message/event identifier rather than the Mostro order id, the history lookup can miss the actual order history or read the wrong one, and the fallback path will silently persist slash_cause = timeout for a real dispute slash.

Why this matters:

  • The inferred cause is persisted into the notification and later drives which copy the user sees.
  • More importantly, this PR is explicitly about making timeout vs dispute handling cause-aware, so anchoring the inference on the wrong identifier would undermine the core contract of the feature.
  • Because the fallback is timeout, this would fail in the misleading direction instead of failing loudly.

What I would want:

  • Use the actual order id from the payload for the history lookup in this path, not event.id.
  • In other words, the slash-cause inference should be keyed on the same durable order identity that the notification stores as order_id.

Aside from that, the helper extraction and the timeout-vs-dispute session handling direction look good to me.

@Catrya

Catrya commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

I dug into this and event.id here is not a separate event/message identifier: event is a MostroMessage, and MostroMessage.id is the Mostro order id (parsed from the message's id field), not the outer Nostr gift-wrap event id.

On the wire the envelope id and the payload order.id are the same UUID, e.g. for this very bond-slashed:

  "id":"1e47f2dc-...","action":"bond-slashed",
  "payload":{"order":{"id":"1e47f2dc-...", ...}}

The rest of the notifier already treats event.id as the order id (navigation, all getAllMessagesForOrderId lookups), and messages are stored under that same key — so the history lookup is correctly anchored and there's no silent fall-through to timeout.

That said, your readability point stands: I've switched the lookup to order.id so the inference is keyed on the
exact identifier persisted as order_id, removing any ambiguity. Same value, just self-documenting.

mostronatorcoder[bot]
mostronatorcoder Bot previously approved these changes Jun 17, 2026

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-checked your comment and the underlying model, and you are right: the blocker I raised does not hold under the actual semantics here.

What changed my mind:

  • In this path event is a MostroMessage, not the outer Nostr gift-wrap event.
  • MostroMessage.id is the Mostro order id, and that is the same UUID carried in the payload order.id for this bond-slashed notice.
  • So the history lookup is already anchored on the correct durable order identity, not on a separate transport-level event id.

That means my previous concern about silently reading the wrong history and defaulting a real dispute slash to timeout was based on the wrong identifier model.

I still agree with your readability point that order.id is more self-documenting in this branch, but that is not a correctness blocker on the current head.

I re-checked the rest of the PR and I do not see another blocking issue. The cause-aware notification copy, dispute-vs-timeout session handling, and dispute-only order-details notice all look coherent with the inferred-history model.

Approved on the current revision.

@Catrya Catrya requested a review from AndreaDiazCorreia June 18, 2026 19:11
…l card

  - Render the message in MostroMessageDetail instead of the bond-slashed fallback
  - Read the amount from the bond-slashed message; blank while history loads
  - Remove the redundant _buildBondLostDisputeNotice in trade_detail_screen

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the new head and I do not see a new blocking issue.

What I checked on the latest commit:

  • The new UI move from the old trade-detail fallback into MostroMessageDetail does not appear to change the underlying cause inference model or the timeout-vs-dispute session behavior.
  • The dispute-only rendering still depends on the same history-derived slash classification and slashed-amount extraction, so this looks like presentation cleanup rather than a semantic change.
  • I do not see a regression that would justify reopening a blocker on the current revision.

Approved on the latest head.

@grunch grunch merged commit fe3a897 into main Jun 18, 2026
2 checks passed
@grunch grunch deleted the add-slash-bond-dispute-dm branch June 18, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants