Skip to content

feat(cashu): take flow escrow request + unblock creation — Track A TA-2 - #830

Open
grunch wants to merge 2 commits into
mainfrom
feat/cashu-ta2-take-flow
Open

feat(cashu): take flow escrow request + unblock creation — Track A TA-2#830
grunch wants to merge 2 commits into
mainfrom
feat/cashu-ta2-take-flow

Conversation

@grunch

@grunch grunch commented Jul 20, 2026

Copy link
Copy Markdown
Member

Stacked on #829 (TA-1) → #828 (CF-5). Review/merge those first; the base retargets as the stack lands. The diff shown is TA-2 only.

What & why

Completes the Cashu lock flow end-to-end with TA-1 (docs/cashu/02-track-a-lock.md §5). A taken order on a cashu node now asks the seller to lock a 2-of-3 escrow instead of paying a hold invoice, and unblocks order creation + the take flow in dispatch_cashu.

Creation and taking ship together: unblocking NewOrder any earlier would populate the book with orders that can never be taken to completion (no escrow) — the orphan-order hazard CF-5's rationale warns about.

Changes

  • util::show_cashu_escrow_request (new, the Cashu analogue of show_hold_invoice): advance the order to WaitingPayment (where the TA-1 CAS expects it), record both trade pubkeys, publish the order event, and enqueue:
    • to the sellerAction::WaitingSellerToPay + Payload::Order(SmallOrder) carrying amount + buyer_trade_pubkey + seller_trade_pubkey (everything the client needs to build the 2-of-3);
    • to the buyer — a bare "waiting for the seller" notice.
  • take_sell / take_buy: early-return through show_cashu_escrow_request when is_cashu_enabled(), skipping the hold-invoice / buyer-invoice path. The buyer redeems ecash directly, so there is no buyer payout invoice (a supplied one is ignored). Lightning path byte-for-byte unchanged.
  • dispatch_cashu: NewOrder/TakeBuy/TakeSellhandle_message_action_no_ln (their real handlers). The CF-5 gate test drops them from the InvalidAction list.

Design notes

  • Amount: the escrow locks order.amount exactly — the Mostro fee is a separate token (Option 2, TA-1f).
  • Mint URL + locktime are not carried in the request payload (the 0.14.0 protocol has no field for them). They are node policy; the daemon enforces them authoritatively when the seller submits (add_cashu_escrow_action §5/§7), so a client funding against the wrong mint or too short a locktime is simply rejected and retries.
  • The escrow request reuses Action::WaitingSellerToPay + Payload::Order (with the trade pubkeys set) — on a cashu node the seller's client reads that as "lock the escrow", the same convention the first-attempt branch used.

Tests

  • show_cashu_escrow_request advances the status to WaitingPayment, records both trade pubkeys, and enqueues the seller escrow request (asserting the Order payload carries the trade pubkeys + bare amount, no buyer invoice) and the buyer notice.

Checklist

  • cargo fmt --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test — 1027 passed
  • Off-by-default: no behaviour change when Cashu is disabled

Depends on #829 (TA-1), #828 (CF-5). Refs: docs/cashu/02-track-a-lock.md (TA-2).


🧪 Manual testing — step by step

Prereqs: the Cashu-mode mostrod setup from CF-5 (#828) — mint up (docker compose -f docker-compose.cashu.yml up -d), [cashu] enabled = true. This branch is stacked on TA-1, so check it out (it contains CF-5 + TA-1 + TA-2). Run with RUST_LOG=mostro=info.

1. Order creation + taking are unblocked in Cashu mode

  1. From a client, create a sell order (NewOrder).
  2. Expected: the order is created (no longer InvalidAction as in CF-5) and appears in the book as Pending. ✔️
  3. Take it (TakeSell, or TakeBuy for a buy order).
  4. Expected: the take succeeds — no hold invoice is created, and no buyer payout invoice is requested (a supplied one is ignored). ✔️

2. The seller gets an escrow request; the order waits for the lock

  1. After the take, inspect the message the seller receives.
  2. Expected: an Action::WaitingSellerToPay carrying a Payload::Order (SmallOrder) whose amount = the order amount and whose buyer_trade_pubkey / seller_trade_pubkey are set — everything the seller's client needs to build the 2-of-3. The buyer receives a bare WaitingSellerToPay "waiting for the seller" notice. ✔️
  3. Confirm the order state in the DB:
    SELECT status, buyer_pubkey, seller_pubkey FROM orders WHERE id = '<order-id>';
    Expected: status = 'waiting-payment', both trade pubkeys recorded. This is exactly where TA-1's lock CAS expects the order. ✔️

3. Unit test

cargo test --bin mostrod show_cashu_escrow_request

Expected: the helper advances the order to WaitingPayment, records both trade pubkeys, and enqueues the seller escrow request (Order payload with the trade pubkeys + bare amount) plus the buyer notice. ✔️

4. Lightning regression

  1. With [cashu] enabled = false, create + take an order on an LND node.
  2. Expected: the classic hold-invoice / buyer-invoice flow is unchanged — the Cashu branch is never taken. ✔️

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@grunch, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 35931753-a215-4ad6-a171-2a6897822f9e

📥 Commits

Reviewing files that changed from the base of the PR and between ec4a046 and f76d5fe.

📒 Files selected for processing (7)
  • cashu-e2e-keys.json
  • docs/cashu/02-track-a-lock.md
  • src/app.rs
  • src/app/take_buy.rs
  • src/app/take_sell.rs
  • src/db.rs
  • src/util.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cashu-ta2-take-flow

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.

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

Copy link
Copy Markdown

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: 6652b7079f

ℹ️ 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/util.rs
Comment on lines +1090 to +1092
order_updated
.update(pool)
.await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard Cashu take updates with a CAS

When two TakeBuy/TakeSell events for the same pending Cashu order run concurrently, both handlers can hold a stale Pending copy and reach this unconditional full-row update. If the first request has already let the seller submit AddCashuEscrow and the CAS has advanced the row to Active with cashu_* fields populated, the later stale update can rewrite the row back to WaitingPayment and clear the persisted escrow while notifications have already gone out. The take transition should be conditional on the current status/empty escrow before enqueueing the request, or re-read and abort once the order has moved on.

Useful? React with 👍 / 👎.

@grunch
grunch force-pushed the feat/cashu-ta1-lock-handler branch from e2f032a to 760467f Compare July 21, 2026 01:50
@grunch
grunch force-pushed the feat/cashu-ta2-take-flow branch from 6652b70 to a16c745 Compare July 21, 2026 01:50
@grunch
grunch force-pushed the feat/cashu-ta1-lock-handler branch from 760467f to 18cc7d6 Compare July 24, 2026 19:18
Base automatically changed from feat/cashu-ta1-lock-handler to main July 24, 2026 20:52
grunch added 2 commits July 24, 2026 17:54
Add the Cashu branch to `take_sell_action`/`take_buy_action` and the
`show_cashu_escrow_request` helper (docs/cashu/02-track-a-lock.md §5), and
unblock `NewOrder`/`TakeBuy`/`TakeSell` in `dispatch_cashu`. A taken order on
a cashu node now asks the SELLER to lock a 2-of-3 escrow instead of paying a
hold invoice, leaving the order in `WaitingPayment` where the TA-1 CAS
expects it. Creatable and takeable ship together so the book never fills with
untakeable orders (the orphan-order hazard CF-5 warns about).

- `util::show_cashu_escrow_request`: advance to `WaitingPayment`, record both
  trade pubkeys, publish the order event, and enqueue an escrow request to the
  seller (`Action::WaitingSellerToPay` + `Payload::Order` carrying
  `amount`/`buyer_trade_pubkey`/`seller_trade_pubkey`) plus a bare "waiting for
  the seller" notice to the buyer. The buyer redeems ecash directly, so there
  is no buyer payout invoice. The escrow locks `order.amount` exactly (the fee
  is a separate token in TA-1f); the mint URL + locktime floor are node policy
  the daemon enforces authoritatively at lock validation (TA-1 §5/§7), so they
  are not carried in the request (the 0.14.0 protocol has no field for them).
- `take_sell`/`take_buy`: early-return through `show_cashu_escrow_request` when
  `is_cashu_enabled()`, skipping the hold-invoice / buyer-invoice path. A
  supplied buyer invoice is ignored in Cashu mode. Lightning path unchanged.
- `dispatch_cashu`: route `NewOrder`/`TakeBuy`/`TakeSell` to
  `handle_message_action_no_ln` (their real handlers); the gate test drops them
  from the `InvalidAction` list accordingly.

Test: `show_cashu_escrow_request` advances the status, records both pubkeys,
and enqueues the seller request (with the trade pubkeys + bare amount) and the
buyer notice.

Depends on CF-5 (+ TA-1 for the full e2e lock). Base: feat/cashu-ta1-lock-handler
Refs: docs/cashu/02-track-a-lock.md (TA-2)
Addresses the P1 review finding on the TA-2 take flow.

show_cashu_escrow_request built a full row from an order copy read at the top of
the take handler and wrote it unconditionally. Two concurrent TakeBuy/TakeSell
for the same pending order both pass the caller's in-memory check_status, so the
loser's write lands second — dragging the status back to WaitingPayment with its
own trade keys and nulling every column its stale copy does not carry. If the
seller had already submitted AddCashuEscrow in the window, that includes the
persisted cashu_escrow_token: a funded escrow silently erased, with the lock
notifications already sent.

The helper now claims the Pending -> WaitingPayment transition through
db::claim_order_status before touching anything, so only one taker reaches the
write and the loser aborts with CantDo(NotAllowedByStatus) having changed
nothing. The claim additionally refuses any order whose escrow is already
funded, so a late take can never drag a funded order backwards whatever its
current status.

Note the Lightning path has the same shape — show_hold_invoice writes a full row
just as unconditionally — but its stale write clears an invoice hash/preimage
that the invoice-subscription flow reconciles, not an escrow token that nothing
can recover. Worth a separate issue; not changed here.

Track A §5 is updated to match.
@grunch
grunch force-pushed the feat/cashu-ta2-take-flow branch from a16c745 to f76d5fe Compare July 24, 2026 20:58
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.

1 participant