Skip to content

feat(bond): maker anti-abuse bond on order creation (Phase 5)#608

Merged
grunch merged 7 commits into
mainfrom
bond-phase5
Jun 10, 2026
Merged

feat(bond): maker anti-abuse bond on order creation (Phase 5)#608
grunch merged 7 commits into
mainfrom
bond-phase5

Conversation

@Catrya

@Catrya Catrya commented Jun 3, 2026

Copy link
Copy Markdown
Member

Implements the mobile side of anti-abuse bond Phase 5: when a node has the bond enabled for makers (apply_to = make | both), creating an order now requires paying a bond before the order is published.

What this does

  • AddOrderNotifier now handles pay-bond-invoice during creation: cancels the create timeout, shows the pay-bond
    screen, and stays alive so the post-bond new-order ack still confirms via _confirmOrder.
  • The uncommitted order stays ephemeral: its session lives in memory only (new registerSessionInMemory) and a
    transient, non-serialized Session.bondPending flag keeps the shared pay-bond handler from persisting it. An
    abandoned order leaves nothing on disk and never reappears after a restart; it is also never shown in My Trades (no
    published NIP-33 event).
  • Cancel for the maker abandons locally (clears the stored bond message + in-memory session) instead of sending a
    cancel the daemon rejects during WaitingMakerBond; the bond hold invoice expires server-side. Taker bonds and
    published orders keep the existing cancel flow.
  • Maker-specific bond screen copy warning the user to keep the screen open and pay, or the order won't be created.

How to test manually

Requires a Mostro node with the anti-abuse bond enabled for makers ([anti_abuse_bond] enabled = true, apply_to = make or both). The info event should show bond_enabled: true.

Bond-enabled node (maker)

  1. Happy path — Create a sell order. The pay-bond screen appears with a QR and the maker-specific copy ("Before your order can be created…"). Pay the bond invoice from a Lightning wallet → the order is published and you land on the order-created confirmation. The order shows in My Trades as pending.
  2. No false timeout — Confirm the old "no response received" message no longer appears while the bond invoice is outstanding (it used to fire after 10s).
  3. Cancel button — Create another order; on the pay-bond screen tap Cancel and confirm. You return to the order book. The order is not created, nothing lingers in My Trades, and no cancel is sent to Mostro (the bond invoice just expires server-side ~5 min later).
  4. Leave the screen — Create an order, reach the pay-bond screen, then press back without paying. The order is
    not in My Trades.
  5. Ephemeral on restart — Create an order, reach the pay-bond screen, do NOT pay, kill the app and reopen.
    Nothing is waiting and nothing shows in My Trades; you must create the order again.

Bond-disabled node (regression)

  1. Point the app at a node without the bond (or apply_to = take). Create an order → it publishes immediately with
    no pay-bond screen, exactly as before.

Taker (regression)

  1. On the bond-enabled node, take an existing order. The taker pay-bond flow is unchanged: the bond screen shows the taker copy, paying proceeds, and tapping Cancel sends a cancel to Mostro and frees the order.

Nodes without the bond are unaffected: the new-order path is untouched, so creation behaves exactly as before.

Summary by CodeRabbit

  • New Features

    • Maker bond payment flow: transient in-memory sessions used during bond payment and a dedicated pay-bond screen flow with maker-specific messaging and navigation.
  • Bug Fixes

    • Session persistence and cancellation now respect transient bond state so unfinished bond payments aren’t persisted or treated as final.
  • Localization

    • Updated/added bond-related strings in EN/DE/ES/FR/IT, including maker-specific guidance.
  • Tests

    • Added tests for transient bond flag, maker bond state transitions, and timeout cleanup behavior.

Catrya added 5 commits June 3, 2026 14:38
  - handle pay-bond-invoice in AddOrderNotifier so a bond-enabled node prompts the maker for the deposit instead of
  timing out the create flow
  - add SessionNotifier.registerSessionInMemory to keep the unconfirmed order ephemeral (no disk persist) while
  reachable by orderId for the pay-bond screen
  - cancel the 10s create timeout on the bond message and keep the notifier alive so the post-bond new-order ack still
  confirms via _confirmOrder
  - no change to bond-disabled nodes: the new-order path is untouched
  - add a transient Session.bondPending flag (never serialized) set while a maker order awaits its bond
  - skip saveSession in the shared pay-bond handler when bondPending, so the screen's OrderNotifier cannot persist the
  uncommitted order
  - clear the flag and persist for real in _confirmOrder once the bond locks and the order is published
  - an abandoned maker bond now leaves nothing on disk and never reappears after a restart
  - the daemon rejects an explicit cancel while an order sits at WaitingMakerBond, so cancelOrder now detects the
  uncommitted maker bond (session.bondPending) and tears down the local session and stored pay-bond message instead of
  sending a cancel that would be refused
  - the bond hold invoice expires server-side and the stranded order is deleted, so no HTLC or ghost order is left
  behind
  - taker bonds and published orders keep the existing cancel flow unchanged
  - session_bond_pending_test: bondPending defaults to false, is absent from toJson, and is never restored by fromJson
  — locks the ephemerality invariant so an abandoned order cannot survive a restart
  - order_state_maker_bond_test: pending → pay-bond-invoice captures the bolt11 and moves to the bond-waiting state,
  then the post-bond new-order confirms back to pending
  - add bondExplanationMaker string (en/es/it/de/fr)
  warning the maker to keep the screen open and pay, or
  the order won't be created
  - pay-bond screen shows the maker copy when
  session.bondPending is set, taker copy otherwise
  - regenerate localizations
@coderabbitai

coderabbitai Bot commented Jun 3, 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: c1641669-56aa-4981-9b86-f6626204d8bc

📥 Commits

Reviewing files that changed from the base of the PR and between 6c9500c and 362d4db.

📒 Files selected for processing (2)
  • lib/features/order/notifiers/abstract_mostro_notifier.dart
  • test/features/order/notifiers/maker_bond_timeout_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/order/notifiers/abstract_mostro_notifier.dart

Walkthrough

Adds a transient Session.bondPending flag, an in-memory session registration API, conditional session persistence and cancellation behavior for maker bond flows, payBondInvoice event handling and navigation, UI selection for maker/taker explanations, localization updates across locales, and tests validating transient session and order-state transitions.

Changes

Maker anti-abuse bond flow

Layer / File(s) Summary
Session model and in-memory registration
lib/data/models/session.dart, lib/shared/notifiers/session_notifier.dart, test/data/models/session_bond_pending_test.dart
Session.bondPending added as a transient flag (not persisted); SessionNotifier.registerSessionInMemory() stores sessions by orderId without disk persistence; tests verify default=false, omission from toJson(), and ignored on fromJson().
Maker bond order creation
lib/features/order/notifiers/add_order_notifier.dart
subscribe() routes Action.payBondInvoice to _handleMakerBondInvoice(), which cancels request-timeout cleanup, sets bondPending=true, registers the session in memory, and navigates to /pay_bond/<orderId>; _confirmOrder() clears bondPending=false before persisting.
Session persistence and bond cancellation
lib/features/order/notifiers/abstract_mostro_notifier.dart, lib/features/order/notifiers/order_notifier.dart, test/features/order/notifiers/maker_bond_timeout_test.dart
AbstractMostroNotifier skips saveSession() when bondPending=true to keep maker sessions ephemeral; OrderNotifier.cancelOrder() deletes local messages and session and returns early when bondPending=true rather than calling the remote cancel; adds hasRequestTimeout test helper and tests for timeout cancel idempotency.
UI and multi-language localization
lib/features/order/screens/pay_bond_invoice_screen.dart, lib/l10n/intl_*.arb, test/features/order/models/order_state_maker_bond_test.dart
PayBondInvoiceScreen computes isMakerBond from session and selects bondExplanationMaker vs bondExplanation; adds bondExplanationMaker strings for English, German, Spanish, French, and Italian; order-state tests validate the maker bond invoice → waitingTakerBond → pending transitions and lnInvoice preservation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Anti-Abuse Bond Support #591: Matches the maker anti-abuse bond handling objectives (payBondInvoice flow, in-memory pending state, and per-trade bond tracking).

Possibly related PRs

  • MostroP2P/mobile#592: Also modifies payBondInvoice flow and session persistence handling in AbstractMostroNotifier.
  • MostroP2P/mobile#318: Related requestId-based session timeout cleanup APIs used during maker bond handling.

Suggested reviewers

  • AndreaDiazCorreia
  • BraCR10
  • mostronatorcoder

Poem

🐰 A bond most noble, transient and true,
Stays in memory, not on disk—makers' debut,
Keep the screen open while the payment sings,
Then confirmation clears the little strings.
A rabbit hops and celebrates this change!

🚥 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 primary change: implementing maker anti-abuse bond on order creation in Phase 5. It is clear, concise, and directly related to the changeset.
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 bond-phase5

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: 1

🧹 Nitpick comments (2)
test/features/order/models/order_state_maker_bond_test.dart (2)

11-11: ⚡ Quick win

Update comment to match actual enum value.

The comment mentions "WaitingMakerBond" but the test checks for Status.waitingTakerBond (line 56). Either update the comment to reference the actual enum value or explain why the status is named "waitingTakerBond" when it's used for maker bonds.

📝 Suggested clarification
-  // The daemon parks the order at WaitingMakerBond and asks the maker to pay a
+  // The daemon parks the order at Status.waitingTakerBond and asks the maker to pay a
   // bond first; the bolt11 arrives as a PaymentRequest with the bond amount.
🤖 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/features/order/models/order_state_maker_bond_test.dart` at line 11,
Update the misleading comment so it matches the enum used in the test: replace
"WaitingMakerBond" with "waitingTakerBond" or otherwise explain why
Status.waitingTakerBond is used for maker bonds; reference the test
name/order_state_maker_bond_test.dart and the enum symbol
Status.waitingTakerBond (and the comment text near the daemon description) so
the comment accurately reflects the status value being asserted in the test.

38-38: ⚡ Quick win

Clarify why publishedAck uses amount: 0 in maker bond flow (test/features/order/models/order_state_maker_bond_test.dart:38)

publishedAck (Action.newOrder) sets Order.amount to 0, even though the earlier bondInvoice (Action.payBondInvoice) uses amount: 300. This matches the protocol examples for new-order confirmations in docs/architecture/ORDER_CREATION_PROCESS.md (which also show amount: 0). Add a short test comment next to publishedAck explaining that new-order acks intentionally carry amount: 0.

🤖 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/features/order/models/order_state_maker_bond_test.dart` at line 38, Add
a short inline test comment next to the publishedAck variable (the
Action.newOrder instance) explaining that Order.amount is intentionally set to 0
for new-order acknowledgements as per the protocol examples; reference the
bondInvoice (Action.payBondInvoice) using amount: 300 to contrast the values and
mention that this matches docs/architecture/ORDER_CREATION_PROCESS.md showing
new-order confirmations carry amount: 0.
🤖 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/l10n/intl_de.arb`:
- Line 709: Replace the English value for the ARB key "bondExplanationMaker"
with a German translation so German users see localized copy; update the value
of bondExplanationMaker in lib/l10n/intl_de.arb to something like: "Bevor Ihre
Bestellung erstellt werden kann, müssen Sie eine Anti-Missbrauchs-Kaution
zahlen. Lassen Sie diesen Bildschirm geöffnet, bis die Zahlung abgeschlossen ist
– wenn Sie ihn schließen oder nicht zahlen, wird die Bestellung nicht erstellt.
So funktioniert’s:\n\n• Ihre Sats werden in Ihrer Wallet gehalten, sie werden
nicht ausgegeben.\n• Wenn der Austausch erfolgreich abgeschlossen wird, erhalten
Sie die Kaution automatisch zurück.\n• Wenn es einen Streitfall zu dieser
Bestellung gibt und Sie den Streit verlieren, verlieren Sie die Kaution.\n•
Dieser Mechanismus schützt alle Nutzer vor Betrügern." and ensure proper
escaping/formatting for ARB newlines and punctuation.

---

Nitpick comments:
In `@test/features/order/models/order_state_maker_bond_test.dart`:
- Line 11: Update the misleading comment so it matches the enum used in the
test: replace "WaitingMakerBond" with "waitingTakerBond" or otherwise explain
why Status.waitingTakerBond is used for maker bonds; reference the test
name/order_state_maker_bond_test.dart and the enum symbol
Status.waitingTakerBond (and the comment text near the daemon description) so
the comment accurately reflects the status value being asserted in the test.
- Line 38: Add a short inline test comment next to the publishedAck variable
(the Action.newOrder instance) explaining that Order.amount is intentionally set
to 0 for new-order acknowledgements as per the protocol examples; reference the
bondInvoice (Action.payBondInvoice) using amount: 300 to contrast the values and
mention that this matches docs/architecture/ORDER_CREATION_PROCESS.md showing
new-order confirmations carry amount: 0.
🪄 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: d97ef52b-0977-4d48-9078-3ae7be81fff5

📥 Commits

Reviewing files that changed from the base of the PR and between 215d022 and 58054f7.

📒 Files selected for processing (13)
  • lib/data/models/session.dart
  • lib/features/order/notifiers/abstract_mostro_notifier.dart
  • lib/features/order/notifiers/add_order_notifier.dart
  • lib/features/order/notifiers/order_notifier.dart
  • lib/features/order/screens/pay_bond_invoice_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/notifiers/session_notifier.dart
  • test/data/models/session_bond_pending_test.dart
  • test/features/order/models/order_state_maker_bond_test.dart

Comment thread lib/l10n/intl_de.arb Outdated

@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 here, but I do see one remaining correctness issue that I would block on before merge.

Blocking issue:

  • In the maker-bond abandon path, OrderNotifier.cancelOrder() now deletes the local session and stored pay-bond messages when session.bondPending == true, but it does not also cancel the outstanding create-order timeout cleanup that was originally started for this requestId.
  • This PR already correctly cancels that timeout when payBondInvoice arrives in _handleMakerBondInvoice(), which means the timeout is still considered part of the maker-bond flow lifecycle. If the user abandons locally while that timer is still armed, it can still fire later against a session/flow that you already tore down. At best that is redundant cleanup; at worst it can interfere with a new flow reusing nearby state and make the behavior timing-dependent.

I think the local-abandon branch should explicitly cancel the same create-timeout/session-timeout machinery before deleting messages/session, so the maker-bond flow is fully torn down in one place.

The rest of the approach makes sense to me: keeping the pre-confirmation maker order ephemeral is the right model, bondPending being transient is good, and the localized maker copy is useful. But I would still request changes until the timeout lifecycle is closed cleanly on manual abandon as well.

  - add @VisibleForTesting AbstractMostroNotifier.hasRequestTimeout
  - regression test: cancelSessionTimeoutCleanupForRequestId disarms the request timer and a repeated cancel is a safe
  no-op
  - documents that the create timeout is torn down when pay-bond-invoice is handled, before the abandon path is
  reachable
@Catrya

Catrya commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

Added a regression test (maker_bond_timeout_test.dart) for this. I don't think the issue is reachable: the create-order timeout is cancelled on the first line of _handleMakerBondInvoice, the same method that sets bondPending and navigates to the pay-bond screen. Since the abandon branch in cancelOrder() is only reachable from that screen, the timer is always already disarmed before abandon runs — the two states can't coexist. Also, OrderNotifier doesn't hold the requestId (it's keyed by orderId), so cancelling that timer from the abandon branch isn't even possible, nor needed.

The test confirms the mechanism it relies on: cancelSessionTimeoutCleanupForRequestId disarms the timer and a second cancel is a safe no-op.

@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 the new commit and your explanation, and I agree the blocker I raised does not hold under the actual state flow here.

What changed my mind:

  • bondPending is only set inside _handleMakerBondInvoice().
  • That same method cancels the create-order timeout on its first line, before it marks the session as bond-pending and before navigation reaches the pay-bond screen.
  • The local-abandon branch in OrderNotifier.cancelOrder() is only reachable from that post-payBondInvoice state, so the supposedly outstanding create timeout and bondPending == true do not actually coexist.
  • The new regression test documenting that cancelSessionTimeoutCleanupForRequestId() disarms the timer and that a second cancel is a safe no-op is a good lock on that invariant.

So I’m retracting the timeout-lifecycle blocker.

I re-checked the rest of the PR and still think the main model is sound: the unconfirmed maker order stays ephemeral, bondPending is transient and never serialized, the maker-specific bond copy is useful, and the local abandon path avoids sending a cancel the daemon would reject.

Approved on the current revision.

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

utACK

@grunch grunch merged commit 9a6f8a8 into main Jun 10, 2026
2 checks passed
@grunch grunch deleted the bond-phase5 branch June 10, 2026 20:40
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