Skip to content

feat(bond): Phase 1.5 — dedicated PayBondInvoice action + WaitingTakerBond status#736

Merged
grunch merged 3 commits into
mainfrom
feat/bond-phase-1.5-pay-bond-invoice
May 12, 2026
Merged

feat(bond): Phase 1.5 — dedicated PayBondInvoice action + WaitingTakerBond status#736
grunch merged 3 commits into
mainfrom
feat/bond-phase-1.5-pay-bond-invoice

Conversation

@grunch
Copy link
Copy Markdown
Member

@grunch grunch commented May 11, 2026

Summary

Implements Phase 1.5 of the anti-abuse bond rollout (docs/ANTI_ABUSE_BOND.md §6.5): retire the Phase 1 reuse of Action::PayInvoice / Status::Pending for bond DMs in favour of dedicated Action::PayBondInvoice and Status::WaitingTakerBond variants. Pure protocol clean-up — no new slashing, no schema change, no behavioural change to the concurrent-bonds race already on main.

After this lands:

  • Clients dispatch bond invoices on action type alone (no memo parsing).
  • The order's daemon-internal status during the bond window is WaitingTakerBond (distinct from "no taker yet"), but the wire-published NIP-69 bucket stays pending so the order keeps advertising as takeable.
  • cancel_action recognises WaitingTakerBond as a pre-trade state.
  • The seller-as-taker case (buy-order taken) emits one PayBondInvoice followed by one PayInvoice for the trade hold invoice, both visible as distinct action types on the wire.

Changes

Protocol

  • Bump mostro-core 0.10.0 → 0.11.0 so Action::PayBondInvoice and Status::WaitingTakerBond are reachable.
  • src/app/bond/flow.rs::request_taker_bond: emit Action::PayBondInvoice (not PayInvoice); after the DM ships, flip the order from Pending to WaitingTakerBond via update_order_event and persist. The flip is gated on current status so concurrent takers don't re-publish.
  • src/app/take_buy.rs / take_sell.rs idempotent-retry path: also emits PayBondInvoice when re-sending the same bolt11 to a taker whose bond is still Requested.

Status transitions

  • request_taker_bond flips Pending → WaitingTakerBond post-DM.
  • resume_take_after_bond (unchanged) drives WaitingTakerBond → WaitingPayment / WaitingBuyerInvoice via the existing trade-flow helpers.
  • on_bond_invoice_canceled + new maybe_drop_waiting_taker_bond helper: when the last active bond on an order is released and the order is in WaitingTakerBond, flip back to Pending and republish. No-op if other bonds are still racing.

NIP-69 mapping (load-bearing — docs/ANTI_ABUSE_BOND.md §2 principle 8)

  • src/nip33.rs::create_status_tags adds Status::WaitingTakerBond → (true, Status::Pending). This is what keeps the order advertised under the pending bucket during the bond window; a regression here would let a non-paying taker park the order off the orderbook.
  • create_fiat_amt_array and create_source_tag also recognise WaitingTakerBond so range-order min/max advertising and the mostro: source URL stay consistent with the wire bucket.
  • src/util.rs::get_ratings_for_pending_order: include maker ratings on the published event during WaitingTakerBond too.

Take + cancel guards

  • src/app/take_buy.rs / take_sell.rs entry status check widens to accept either Pending or WaitingTakerBond. The locked-bond gate, idempotent-retry, and fresh-row creation logic inside the bond block are unchanged.
  • src/app/cancel.rs::cancel_action_generic "pre-trade cancel" guard widens from if Pending to if Pending | WaitingTakerBond — without this every cancel during the bond window would fall through to NotAllowedByStatus.
  • src/app/trade_pubkey.rs entry check widens for consistency.

Acceptance criteria (docs/ANTI_ABUSE_BOND.md §6.5.4)

  • Phase 1 Action::PayInvoice-for-bond workaround is retired — mostrod now only emits PayInvoice for trade hold invoices and PayBondInvoice for bonds.
  • §6.3 client-side memo parsing recommendation becomes unnecessary; clients dispatch on action type alone.
  • §2 non-blockability invariant preserved: WaitingTakerBond maps to NIP-69 pending and re-take from a different pubkey is accepted (concurrent-bonds race, unchanged from main).
  • bond::supersede_prior_taker_bonds is already removed (landed in PR feat(bond): concurrent taker bonds, first-to-lock wins (Phase 0+1) #733).
  • Phase 2 can rely on the clean API when it ships.

Tests

5 new tests (264 total, all green):

  • nip33::tests::waiting_taker_bond_maps_to_pending_on_wire — load-bearing NIP-69 mapping invariant.
  • nip33::tests::pending_and_waiting_taker_bond_publish_the_same_wire_status — bucket-equivalence regression guard.
  • app::bond::flow::tests::maybe_drop_waiting_taker_bond_noop_when_other_bonds_active — taker self-cancel with surviving racers leaves status alone.
  • app::bond::flow::tests::maybe_drop_waiting_taker_bond_noop_when_order_not_in_waiting_status — defensive no-op for already-transitioned orders.
  • app::bond::flow::tests::maybe_drop_waiting_taker_bond_noop_when_order_missing — defensive no-op for missing orders so the LND-cancel callback never propagates a hard error.

Existing tests covering the concurrent-bonds race (lock_race_guard_admits_only_one_winner, concurrent_requested_bonds_coexist), find_active_bond_by_taker scoping, release_bond idempotency, and taker_bond_required gating remain green.

cargo fmt --all -- --check, cargo clippy --all-targets --all-features -D warnings, cargo test — all green.

Test plan

  • Code review of the WaitingTakerBond → Pending NIP-33 mapping (src/nip33.rs::create_status_tags)
  • Code review of the post-DM status flip in request_taker_bond
  • Code review of maybe_drop_waiting_taker_bond as the "last bond released" chokepoint
  • Smoke test with bonds enabled in a staging node:
    • Taker A takes a sell order → receives Action::PayBondInvoice (not PayInvoice); the order's daemon-side status is WaitingTakerBond; the published NIP-33 event has s=pending.
    • Taker B (different pubkey) takes the same order → also receives PayBondInvoice; A's bond remains payable; A does not receive Action::Canceled yet.
    • Buy-order taken (taker = seller): the taker receives one PayBondInvoice and, once the bond locks, one PayInvoice for the trade hold invoice — two distinct action types on the wire, no memo parsing.
    • Cancel from a third party: rejected with IsNotYourOrder.
    • Cancel from the maker during the bond window: order published as Status::Canceled, every concurrent prospective taker receives Action::Canceled.
    • All takers abandon their bonds (LND TTL expires): the order's status flips back from WaitingTakerBond to Pending and is republished.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Implemented Phase 1.5 bond lifecycle improvements with enhanced order state management during bond payment window.
    • Added dedicated bond invoice action for improved bond payment handling.
  • Bug Fixes

    • Improved bond cancellation handling to properly transition orders when bonds are cancelled.
  • Chores

    • Updated mostro-core dependency from 0.10.0 to 0.11.0.
    • Maintained client protocol compatibility by presenting internal bond states as pending orders.

Review Change Stack

…rBond status

Switch the bond protocol layer from the Phase 1 reuse of
`Action::PayInvoice` / `Status::Pending` to the dedicated
`Action::PayBondInvoice` and `Status::WaitingTakerBond` variants
shipped by `mostro-core` 0.11.0. Pure protocol clean-up: no new
slashing, no schema change, no behavioural change to the
concurrent-bonds race that already landed on `main`.

See `docs/ANTI_ABUSE_BOND.md` §6.5 for the spec.

Changes:

- **Cargo.toml**: bump `mostro-core` from 0.10.0 to 0.11.0.
- **`src/app/bond/flow.rs::request_taker_bond`**: emit
  `Action::PayBondInvoice` (not `PayInvoice`); after the DM ships,
  flip the order from `Pending` to `WaitingTakerBond` and republish
  via NIP-33. The flip is best-effort and skipped when the order is
  already in `WaitingTakerBond` (concurrent takers).
- **`src/app/bond/flow.rs::on_bond_invoice_accepted`**: defense-in-
  depth status check widens to accept both `Pending` and
  `WaitingTakerBond` as valid pre-trade states before resuming the
  take. `resume_take_after_bond` (via `show_hold_invoice` /
  `set_waiting_invoice_status`) drives the order out to
  `WaitingPayment` / `WaitingBuyerInvoice` exactly as before.
- **`src/app/bond/flow.rs::on_bond_invoice_canceled`** + new
  `maybe_drop_waiting_taker_bond` helper: when the last active bond
  on an order is released and the order is parked at
  `WaitingTakerBond`, flip back to `Pending` and republish so the
  orderbook reflects the empty-bond state. No-op if other bonds are
  still racing or the order has already moved on.
- **`src/nip33.rs::create_status_tags`**: add the load-bearing
  `WaitingTakerBond → (true, Status::Pending)` mapping (§2 principle
  8). This is what keeps the order advertised under NIP-69's
  `pending` bucket during the bond window — the non-blockability
  invariant.
- **`src/nip33.rs`**: `create_fiat_amt_array` and `create_source_tag`
  also recognise `WaitingTakerBond` as pre-trade so range-order
  min/max advertising and the mostro: source URL stay consistent
  with the NIP-69 mapping.
- **`src/util.rs::get_ratings_for_pending_order`**: include maker
  ratings on the published event during `WaitingTakerBond` too — the
  wire status is `pending` and clients browsing the orderbook expect
  the rating attached either way.
- **`src/app/take_buy.rs` / `take_sell.rs`**: entry status check
  widens to accept either `Pending` or `WaitingTakerBond`. The
  idempotent-retry path also emits `PayBondInvoice` (not
  `PayInvoice`). The locked-bond gate inside the bond block is
  unchanged.
- **`src/app/cancel.rs::cancel_action_generic`**: the "Pending →
  maker / taker pre-trade cancel" guard widens to accept
  `WaitingTakerBond`, otherwise every cancel during the bond window
  would fall through to `NotAllowedByStatus` (regression vs Phase 1).
  Inside the branch, the existing two-route logic (maker self-cancel
  publishes `Canceled` + fans out bond releases; taker self-cancel
  releases only the sender's bond and only republishes Pending when
  no others remain) is unchanged.
- **`src/app/trade_pubkey.rs`**: entry check widens for consistency
  with the other pre-trade actions.

Tests (264 passing, +5 new):

- `nip33::tests::waiting_taker_bond_maps_to_pending_on_wire` — the
  load-bearing NIP-69 invariant test.
- `nip33::tests::pending_and_waiting_taker_bond_publish_the_same_wire_status`
  — regression guard for bucket equivalence.
- `app::bond::flow::tests::maybe_drop_waiting_taker_bond_noop_when_other_bonds_active`
  — taker self-cancel with surviving racers leaves status alone.
- `app::bond::flow::tests::maybe_drop_waiting_taker_bond_noop_when_order_not_in_waiting_status`
  — defensive no-op for already-transitioned orders.
- `app::bond::flow::tests::maybe_drop_waiting_taker_bond_noop_when_order_missing`
  — defensive no-op for missing orders so the LND-cancel callback
  never propagates a hard error.

`cargo fmt`, `cargo clippy --all-targets --all-features -D warnings`,
and `cargo test` all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Warning

Rate limit exceeded

@grunch has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 21 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8767e48c-351e-4dab-9b28-567c4e7f453d

📥 Commits

Reviewing files that changed from the base of the PR and between bab2663 and 85f0f19.

📒 Files selected for processing (2)
  • src/app/bond/flow.rs
  • src/db.rs

Walkthrough

This PR implements Phase 1.5 anti-abuse bond lifecycle semantics by introducing a dedicated bond invoice action, parking orders in a new WaitingTakerBond status during bond lockup, updating handler entry points to accept the new status, mapping it to Pending on the wire, and adding cancellation helpers to drop orders back to Pending when bonds are released.

Changes

Phase 1.5 Bond Lifecycle

Layer / File(s) Summary
Dependency Update
Cargo.toml
mostro-core upgraded from 0.10.0 to 0.11.0 with sqlx feature retained.
Bond Action & Status Lifecycle
src/app/bond/flow.rs
Orders transition from Pending to WaitingTakerBond during bond outstanding window; bond invoices dispatched via dedicated Action::PayBondInvoice instead of generic PayInvoice; module-level documentation updated with Phase 1.5 protocol details.
Bond Resume Gate
src/app/bond/flow.rs
on_bond_invoice_accepted expanded to accept both Pending (Phase 1) and WaitingTakerBond (Phase 1.5) as valid pre-trade states before resuming take flow.
Bond Cancellation & Drop Helper
src/app/bond/flow.rs
New maybe_drop_waiting_taker_bond helper function attempts best-effort transition from WaitingTakerBond back to Pending when last active bond is released; integrated into on_bond_invoice_canceled.
Order Entry-Point Gate Updates
src/app/take_buy.rs, src/app/take_sell.rs, src/app/trade_pubkey.rs, src/app/cancel.rs
All order-handling action functions updated to accept both Pending and WaitingTakerBond pre-trade statuses; idempotent retry paths changed from Action::PayInvoice to Action::PayBondInvoice.
Wire Protocol Mapping (NIP-33)
src/nip33.rs
create_status_tags explicitly maps WaitingTakerBond to Pending; create_fiat_amt_array applies range-order advertising for WaitingTakerBond; create_source_tag emits mostro: references for both statuses; unit tests verify identical wire output from both statuses.
Ratings Utility Update
src/util.rs
get_ratings_for_pending_order extended to include WaitingTakerBond alongside Pending when determining rating data inclusion.
Test Infrastructure & Coverage
src/app/bond/flow.rs
Test database setup applies dev_fee migration; new unit tests cover maybe_drop_waiting_taker_bond no-op conditions (other active bonds present, non-WaitingTakerBond status, missing order).

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • MostroP2P/mostro#733: Concurrent-taker-bonds and TakerContext implementation that shares the bond flow code path and bond lifecycle handlers modified in this PR.
  • MostroP2P/mostro#725: Documentation PR for Phase 1.5 bond protocol that corresponds directly to the protocol description and flow changes in this implementation PR.
  • MostroP2P/mostro#467: Prior changes to create_status_tags and order_to_tags in src/nip33.rs that are extended by this PR's wire-protocol mapping logic.

Suggested Reviewers

  • arkanoider
  • Catrya

A taker seeks a bond, dear friend so true, 🐰
Phase 1.5 pauses the dance—just wait, just brew,
WaitingTakerBond keeps the order in queue,
When the invoice is paid, the trade marches through.

🚥 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 changes: introducing Phase 1.5 anti-abuse bond functionality with a dedicated PayBondInvoice action and WaitingTakerBond status, which are the core technical changes across the entire changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 feat/bond-phase-1.5-pay-bond-invoice

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.

@grunch grunch requested review from Catrya and arkanoider May 11, 2026 22:29
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bab26639f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/app/bond/flow.rs Outdated
Comment thread src/app/bond/flow.rs
@grunch
Copy link
Copy Markdown
Member Author

grunch commented May 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/app/bond/flow.rs (2)

229-247: ⚖️ Poor tradeoff

Verify error-handling contract for partial failures.

The status update happens after the DM ships (line 226), which is correct for atomicity. However, if update_order_event succeeds but updated.update(pool) fails (line 233), the order event is republished on the wire as WaitingTakerBond but the DB still shows Pending. This mismatch would self-heal on the next request_taker_bond call (idempotent guard at line 229), but could briefly confuse clients.

Consider whether the warn-and-continue behavior is the right tradeoff here, or if the failure should propagate to prevent the bond from being created in an inconsistent state.

🤖 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 `@src/app/bond/flow.rs` around lines 229 - 247, The current flow republished
the WaitingTakerBond event via update_order_event but only logs a warning if
updated.update(pool) fails, causing a wire/DB mismatch; change this to avoid
proceeding with an inconsistent state by either (preferred) propagating the
error from request_taker_bond so bond creation is aborted (replace the warn with
an Err return using the function's error type) or (alternative) attempt a
compensating action: republish a Pending event or retry the DB persist until
success; locate the logic around request_taker_bond, get_keys,
update_order_event and the updated.update(pool) call and implement one of these
fixes so a republished WaitingTakerBond is never left unpersisted.

827-867: ⚖️ Poor tradeoff

Race condition: check-then-act on active bonds.

Lines 850-852 query for active bonds, then line 855 republishes if the list is empty. Between these two operations, another concurrent taker could lock their bond (via on_bond_invoice_accepted), making the "empty" check stale. The order would be dropped back to Pending even though a winner exists.

However, this race is self-healing: the winning bond's on_bond_invoice_accepted (line 702) will immediately promote the taker context and drive the trade forward, republishing with the correct status. The brief flicker to Pending is a cosmetic glitch, not a data-corruption or user-impact issue.

Fixing this would require a transactional check-and-update or advisory locking, which is high effort for minimal reward given the self-healing property.

🤖 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 `@src/app/bond/flow.rs` around lines 827 - 867, The check-then-act in
maybe_drop_waiting_taker_bond races with concurrent bond acceptance:
find_active_bonds_for_order() is called, then update_order_event() may run on
stale state; fix by performing the re-check-and-update under a database
transaction or row lock so the decision is atomic — e.g., begin a transaction,
SELECT the Order row FOR UPDATE (or otherwise lock it), re-run
find_active_bonds_for_order() (or count active bonds) within that transaction,
and only call update_order_event() and persist the change if the order is still
Status::WaitingTakerBond and there are zero active bonds; alternatively
implement the conditional update in update_order_event() to fail unless the
order status and active-bond count match expectations so
on_bond_invoice_accepted() wins any concurrent race.
🤖 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.

Nitpick comments:
In `@src/app/bond/flow.rs`:
- Around line 229-247: The current flow republished the WaitingTakerBond event
via update_order_event but only logs a warning if updated.update(pool) fails,
causing a wire/DB mismatch; change this to avoid proceeding with an inconsistent
state by either (preferred) propagating the error from request_taker_bond so
bond creation is aborted (replace the warn with an Err return using the
function's error type) or (alternative) attempt a compensating action: republish
a Pending event or retry the DB persist until success; locate the logic around
request_taker_bond, get_keys, update_order_event and the updated.update(pool)
call and implement one of these fixes so a republished WaitingTakerBond is never
left unpersisted.
- Around line 827-867: The check-then-act in maybe_drop_waiting_taker_bond races
with concurrent bond acceptance: find_active_bonds_for_order() is called, then
update_order_event() may run on stale state; fix by performing the
re-check-and-update under a database transaction or row lock so the decision is
atomic — e.g., begin a transaction, SELECT the Order row FOR UPDATE (or
otherwise lock it), re-run find_active_bonds_for_order() (or count active bonds)
within that transaction, and only call update_order_event() and persist the
change if the order is still Status::WaitingTakerBond and there are zero active
bonds; alternatively implement the conditional update in update_order_event() to
fail unless the order status and active-bond count match expectations so
on_bond_invoice_accepted() wins any concurrent race.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0201bbba-0143-46e3-b97b-0a28b1525f41

📥 Commits

Reviewing files that changed from the base of the PR and between 42e035a and bab2663.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • Cargo.toml
  • src/app/bond/flow.rs
  • src/app/cancel.rs
  • src/app/take_buy.rs
  • src/app/take_sell.rs
  • src/app/trade_pubkey.rs
  • src/nip33.rs
  • src/util.rs

grunch and others added 2 commits May 11, 2026 19:40
…ry coverage

Two findings from the bot review of PR #736:

P1: request_taker_bond was reading order.status from the caller's
snapshot, then unconditionally persisting a cloned order as
WaitingTakerBond. The bond subscriber is armed before the DM ships,
so a fast Accepted callback (taker pays instantly) could transition
the order to WaitingPayment / WaitingBuyerInvoice before this block
runs; a concurrent maker-cancel could move it to Canceled; another
take from a different pubkey could already have flipped it to
WaitingTakerBond. The stale write would revert any of those.

Replace the blind write with a compare-and-swap UPDATE
(`SET status = ? WHERE id = ? AND status = 'pending'`). When
`rows_affected == 0` we exit cleanly — somebody else owns the
order's status. When we win, we publish the NIP-33 event and then
patch the persisted `event_id` with a targeted UPDATE rather than
saving the full cloned row, so concurrent field writes aren't
clobbered.

P2: db::find_order_by_date — the query backing
job_expire_pending_older_orders — filtered on `status = 'pending'`
only. Phase 1.5 added the WaitingTakerBond pre-trade status; orders
parked there past their expires_at would never expire, leaving the
bond HTLCs tying up taker funds until CLTV expiry. Widen the query
to `status IN ('pending', 'waiting-taker-bond')`.

Tests (267 total, +3 new):

- pending_to_waiting_taker_bond_cas_refuses_to_overwrite_concurrent_transition
  — the CAS SQL refuses to flip an already-transitioned order.
- pending_to_waiting_taker_bond_cas_flips_when_status_unchanged —
  companion happy-path assertion.
- test_find_order_by_date_includes_waiting_taker_bond — verifies
  the expiry query picks up both Pending and WaitingTakerBond
  expired orders, and excludes fresh / post-trade ones.

`cargo fmt`, `cargo clippy --all-targets --all-features -D warnings`,
`cargo test` — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second codex review finding: the check-then-act sequence in
maybe_drop_waiting_taker_bond (read status → list active bonds →
publish+persist Pending) raced with concurrent
on_bond_invoice_accepted: between the bond listing and the persist,
a new bond could be created and locked, transitioning the order to
WaitingPayment, and our path would then overwrite WaitingPayment with
Pending.

Replace the three-step check-then-act with a single conditional
UPDATE that checks both predicates atomically:

    UPDATE orders SET status = 'pending'
    WHERE id = ? AND status = 'waiting-taker-bond'
      AND NOT EXISTS (
        SELECT 1 FROM bonds WHERE order_id = ? AND state IN ('requested', 'locked')
      )

If `rows_affected == 0`, either the status moved on (winner promoted
to WaitingPayment, maker cancelled, etc.) or a concurrent bond is
still racing — both safe no-ops. If we win, we re-fetch the order to
build NIP-33 tags from a fresh snapshot, publish via
`update_order_event`, and patch the persisted `event_id` only — same
non-clobbering pattern used by the P1 CAS in `request_taker_bond`.

The codex finding's other note about `request_taker_bond`'s
warn-and-continue on publish failure was already addressed by the P1
fix (replaced the unconditional `updated.update(pool)` with a
targeted `event_id`-only UPDATE that runs only after the CAS
succeeds). The remaining failure modes (Nostr publish fails or
event_id patch fails) cause at most a one-event lag on the
orderbook, self-healed by the next status transition — and Pending
and WaitingTakerBond map to the same NIP-69 `pending` bucket
externally, so there's no observable regression.

Test: `maybe_drop_does_not_revert_concurrent_lock` — inserts a
`Locked` bond on an order in `WaitingTakerBond`, calls the helper,
and verifies the status stayed at `WaitingTakerBond` rather than
reverting to `Pending`.

268 tests pass (1 new); `cargo fmt`, `cargo clippy -D warnings`
green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@mostronatorcoder mostronatorcoder Bot left a comment

Choose a reason for hiding this comment

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

I did a strict pass and I can't approve this PR yet.

The protocol cleanup itself looks good, and the compare-and-swap fix for Pending -> WaitingTakerBond addresses the earlier stale-write race correctly. I also agree with the dedicated PayBondInvoice action and the NIP-33 wire mapping strategy.

However, I see one blocking issue that changes behavior in a bad way:

  1. Orders in WaitingTakerBond no longer participate in the pending-expiry job.

find_order_by_date() still selects only status == 'pending', but this PR introduces WaitingTakerBond as the normal pre-trade parked state while bonds are outstanding. That means an order can sit past expires_at and remain effectively available on the wire until the bond invoices resolve, instead of expiring on schedule.

This is especially problematic because WaitingTakerBond is intentionally published to Nostr as the same pending bucket, so from the outside the order still looks takeable while the daemon-side expiry sweep is no longer considering it.

I think the expiry path should include both pre-trade statuses (Pending and WaitingTakerBond) so Phase 1.5 preserves the previous expiration semantics.

Once that is fixed, I think this PR is in good shape.

Copy link
Copy Markdown
Contributor

@mostronatorcoder mostronatorcoder Bot left a comment

Choose a reason for hiding this comment

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

Re-reviewed the current PR head (85f0f196050a43c4178155127e96b9ed0532c4d1).

My previous blocking point about the expiry path no longer applies: the current head already updates find_order_by_date() to include both pending and waiting-taker-bond, which preserves the intended expiration semantics.

I also re-checked the current head for the earlier stale-write race around Pending -> WaitingTakerBond; the compare-and-swap fix is present, the WaitingTakerBond -> Pending drop-back path is implemented, and the related wire-mapping / take-entry / cancel-entry adjustments are consistent.

I ran the current test suite on the current head and it passes (268 passed).

Approving the PR as it stands. Sorry for the earlier stale review on an older state of the branch.

@arkanoider
Copy link
Copy Markdown
Collaborator

arkanoider commented May 12, 2026

Testing this PR, what i did below:

  • created a SELL order from mobile -> took with mostrix, bond popup ok, not taken. Changed seed in mostrix, take again...repetead for times, and payed bond with last seed, confirming just last bond is locked.

  • still with same SELL order I took the bond ( mostro db is waiting-taker-bond ), then not proceed and cancel. Order correctly comes to pending in mostro database

  • Took the SELL order --> paid bond from mostrix --> paid invoice --> seller cancel --> bond released

Same tests repeated for a BUY order, same results.

Copy link
Copy Markdown
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

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

tACK

@grunch grunch merged commit 6a3b0ff into main May 12, 2026
8 checks passed
@grunch grunch deleted the feat/bond-phase-1.5-pay-bond-invoice branch May 12, 2026 16:54
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.

3 participants