Skip to content

feat(cashu): Track A — Cashu 2-of-3 escrow lock end-to-end#765

Open
grunch wants to merge 2 commits into
feat/cashu-mostro-core-0.12from
feat/cashu-track-a-lock
Open

feat(cashu): Track A — Cashu 2-of-3 escrow lock end-to-end#765
grunch wants to merge 2 commits into
feat/cashu-mostro-core-0.12from
feat/cashu-track-a-lock

Conversation

@grunch
Copy link
Copy Markdown
Member

@grunch grunch commented Jun 1, 2026

Track A — Cashu 2-of-3 escrow lock

Implements Track A from docs/CASHU_ESCROW_ARCHITECTURE.md: the Cashu analogue of the seller paying the Lightning hold invoice. Mostro stays a pure coordinator — it holds only 1 of the 3 keys, never takes custody — and only validates the seller-submitted token before advancing the order.

Flow

take (cashu mode) → WaitingPayment → [seller builds & submits 2-of-3 token] → AddCashuEscrow → Active → buyer notified to send fiat

What's in this PR

  • src/app/add_cashu_escrow.rsAction::AddCashuEscrow handler. Requires WaitingPayment + sender == seller, derives the authoritative {P_B, P_S, P_M} from the order's trade pubkeys and Mostro's identity key (never trusting the pubkeys the seller states in the proof), validates via CashuClient::verify_escrow_token (2-of-3 over the right keys, bound to the configured mint, exact amount, all proofs unspent), then atomically CAS-advances WaitingPayment → Active and emits CashuEscrowLocked to both parties.
  • take_sell / take_buy — branch on is_cashu_enabled() before the hold-invoice path and call util::show_cashu_escrow_request, which sets WaitingPayment, persists, publishes the order event, and prompts the seller to fund the token (reusing WaitingSellerToPay + SmallOrder carrying the trade pubkeys).
  • app.rs run_cashu — routes NewOrder/TakeSell/TakeBuy through the no-LN dispatcher (now Cashu-aware); keeps AddInvoice/Release/Cancel/AdminCancel/AdminSettle rejected (AddInvoice would hit the unimplemented CashuBackend::lock; the rest belong to Tracks B/C/D).
  • Escrow seam plumbingAppContext carries Arc<dyn EscrowBackend> + optional CashuClient; show_hold_invoice / settle_seller_hold_invoice go through the backend; main/rpc build the Lightning backend, and main connects the mint + attaches the Cashu client in Cashu mode.
  • cashu/mod.rsverify_escrow_token + cashu_pubkey_from_xonly_hex helpers.

Notable fixes during review

  • Dispatch gap: run_cashu previously rejected NewOrder/TakeSell/TakeBuy, so in Cashu mode no order could be created or taken — the take_* Cashu branches were dead code. Now routed correctly.
  • Amount mismatch: the take-time prompt asked the seller to fund amount + fee, but the lock handler validates token value == order.amount exactly — every lock would have failed. The escrow now locks exactly order.amount.

Design notes

  • Track A's lock is implemented as a dedicated handler outside the EscrowBackend seam (the seam stays LN-shaped for Tracks B/C/D), per design decision.
  • No Mostro fee is taken in Cashu mode. Mostro holds only 1 of 3 keys and cannot split a fee out of the redeemed ecash; Cashu-mode fee collection is the spec's open question and is intentionally left unresolved.

Out of scope (follow-ups)

  • restore_session / scheduler timeout for in-flight Cashu orders (the F5 find_locked_cashu_orders helper exists but isn't wired) — Integration & Hardening.
  • Tracks B (release) / C (cooperative cancel) / D (dispute) remain stubbed and rejected.

Verification

cargo fmt + cargo clippy --all-targets --all-features -D warnings clean; 388 unit tests + Cashu mint integration test pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Cashu token escrow support for securing trade payments when Cashu mode is enabled
    • Sellers can now submit Cashu tokens as collateral for trades
    • Automated token verification validates proper escrow conditions before settlement
  • Improvements

    • Enhanced escrow handling architecture to support multiple payment methods

Implements Track A from docs/CASHU_ESCROW_ARCHITECTURE.md: the Cashu
analogue of paying the Lightning hold invoice. Mostro stays a pure
coordinator (1 of 3 keys, never custodial) — it only validates the
seller-submitted token and advances the order.

- add_cashu_escrow.rs: `AddCashuEscrow` handler. Requires WaitingPayment +
  sender==seller, derives the authoritative {P_B,P_S,P_M} from the order's
  trade pubkeys and Mostro's identity key (never trusting the pubkeys the
  seller states in the proof), validates the token via
  CashuClient::verify_escrow_token (2-of-3 over the right keys, bound to the
  configured mint, exact amount, all proofs unspent), then atomically
  CAS-advances WaitingPayment→Active and notifies both parties
  (CashuEscrowLocked) so the buyer can send fiat.
- take_sell/take_buy: branch on is_cashu_enabled() before the hold-invoice
  path and call util::show_cashu_escrow_request, which sets WaitingPayment,
  persists, publishes the order event, and prompts the seller to fund the
  token (reusing WaitingSellerToPay + SmallOrder carrying the trade pubkeys).
- app.rs run_cashu: route NewOrder/TakeSell/TakeBuy through the no-LN
  dispatcher (they are now Cashu-aware); keep AddInvoice/Release/Cancel/
  AdminCancel/AdminSettle rejected (AddInvoice would hit the unimplemented
  CashuBackend::lock; release/cancel/dispute are Tracks B/C/D).
- escrow seam plumbing: AppContext carries Arc<dyn EscrowBackend> + optional
  CashuClient; show_hold_invoice / settle_seller_hold_invoice go through the
  backend; main/rpc build the Lightning backend, main connects the mint and
  attaches the Cashu client in Cashu mode.
- cashu/mod.rs: verify_escrow_token + cashu_pubkey_from_xonly_hex helpers.

The escrow lock value is exactly order.amount (no Mostro fee added): Mostro
holds only 1 of 3 keys and cannot split a fee out of the redeemed ecash, so
Cashu-mode fee collection is left as an open question per the spec.

Track A lock is implemented as a dedicated handler outside the EscrowBackend
seam (seam stays LN-shaped for Tracks B/C/D), per design decision.

cargo fmt + clippy --all-targets --all-features -D warnings clean; 388 unit
tests + cashu mint integration test pass.

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

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Walkthrough

This PR redesigns the escrow handling architecture by replacing mutable connector-based primitives with an immutable, context-driven EscrowBackend trait, integrating Cashu 2-of-3 escrow token verification and atomic locking, and wiring all handlers to derive escrow from AppContext rather than passing external connectors.

Changes

Cashu Escrow & Escrow Backend Refactor

Layer / File(s) Summary
EscrowBackend Trait & Implementations
src/escrow.rs
EscrowBackend trait replaces mutable hold-invoice primitives with immutable Send + Sync semantic operations (lock, release, cooperative_cancel, dispute_settle, dispute_cancel). LightningBackend wraps LndConnector and implements the trait by cloning per call. settle_for_order and cancel_for_order helpers encode order-based settlement/cancellation semantics. MockEscrowBackend records method invocations for testing.
AppContext Escrow & Cashu Integration
src/app/context.rs
AppContext now holds escrow: Arc<dyn EscrowBackend> and cashu_client: Option<Arc<CashuClient>>. Constructor requires escrow backend; with_cashu_client builder attaches optional Cashu client. Accessors escrow() and cashu_client() expose both. TestContextBuilder supports injecting/defaulting escrow backends for tests.
Cashu Token Validation & Client Enhancements
src/cashu/mod.rs
Adds mint_url() accessor. Refactors verify_2of3_condition with explicit signature/locktime/refund/pubkey validation. Introduces verify_escrow_token for end-to-end validation: 2-of-3 condition, mint URL binding, amount check, DLEQ proof verification, and Unspent state. Adds cashu_pubkey_from_xonly_hex helper for key conversion and CashuBackend implementing EscrowBackend with track-gated stubs. Simplifies DLEQ/state error mapping.
Utility Functions: Hold Invoice & Settlement
src/util.rs
show_hold_invoice refactored to accept escrow: &Arc<dyn EscrowBackend> and call escrow.lock(...) instead of creating Lightning connector internally. New show_cashu_escrow_request transitions order to WaitingPayment, persists changes, publishes event, and enqueues escrow-funding prompts. settle_seller_hold_invoice updated to take ctx: &AppContext and route through ctx.escrow().release() (normal) or ctx.escrow().dispute_settle() (admin).
Handler Signature Updates: Release & AdminSettle
src/app/release.rs, src/app/admin_settle.rs
release_action removes ln_client parameter; calls settle_seller_hold_invoice(ctx, ...) instead. admin_settle_action removes ln_client parameter, uses ctx.escrow() for settlement, and creates LndConnector internally before bond resolution. Both now derive escrow from context.
Cashu Escrow Action: AddCashuEscrow Handler
src/app/add_cashu_escrow.rs
New add_cashu_escrow_action handles seller's escrow lock submission: validates order is WaitingPayment, verifies sender is seller, extracts CashuLockProof, derives {P_B, P_S, P_M} from trade keys, verifies token against mint and exact order amount, atomically transitions order from WaitingPayment to Active via db::update_order_cashu_escrow, publishes status, and notifies both parties. cashu_reason helper maps Cashu errors to CantDoReason.
Action Dispatch & Routing Updates
src/app.rs
Adds pub mod add_cashu_escrow submodule and use add_cashu_escrow_action import. Updates handle_message_action to call release_action and admin_settle_action without ln_client. Extends run_cashu dispatcher with Action::AddCashuEscrow arm routing to add_cashu_escrow_action with error propagation.
Application Boot: Mode-Specific Escrow Initialization
src/main.rs
Cashu mode: connects to mint, creates CashuBackend escrow, includes Cashu client in AppContext. Lightning mode: creates LightningBackend escrow wrapper, includes in AppContext. Both start scheduler with escrow backend dependency.
Take Buy/Sell Actions: Cashu Mode Branching
src/app/take_buy.rs, src/app/take_sell.rs
When Settings::is_cashu_enabled(), both actions call show_cashu_escrow_request(...) and return early, bypassing hold-invoice flow. Otherwise, existing hold-invoice/status-update logic applies.
Supporting Changes: RPC, Add Invoice, Bond Flow
src/rpc/service.rs, src/app/add_invoice.rs, src/app/bond/flow.rs
RPC admin handlers (call_admin_cancel, call_admin_settle, call_admin_add_solver, call_admin_take_dispute) create escrow backend and pass to AppContext::new. add_invoice_action passes ctx.escrow() to show_hold_invoice. resume_take_after_bond creates fresh LightningBackend for hold-invoice display.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • MostroP2P/mostro#760: Both PRs refactor escrow handling behind an EscrowBackend abstraction, changing core handler signatures and updating src/escrow.rs; the main PR's escrow-context and Cashu integration build directly on that seam.
  • MostroP2P/mostro#758: Both PRs touch Cashu execution path and routing in src/app.rs (via run_cashu), relying on Settings::is_cashu_enabled()-driven branching; the main PR's Cashu escrow action wiring builds on that foundation.
  • MostroP2P/mostro#761: The main PR's add_cashu_escrow_action relies on db::update_order_cashu_escrow for CAS escrow-lock and status advance, which is exactly what PR #761 adds at the DB layer.

Suggested reviewers

  • AndreaDiazCorreia

Poem

🐰 A rabbit hops through escrow seams,
Where Cashu tokens lock the dreams,
From mutable chaos, context born,
Escrow backends wake at morn—
Lightning strikes and Cashu's mint,
Now abstracted without a hint!

🚥 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 clearly and specifically describes the main change: implementing Track A of the Cashu escrow architecture with 2-of-3 escrow lock end-to-end functionality.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cashu-track-a-lock

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.

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

ℹ️ 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/main.rs
if Settings::is_cashu_enabled() {
tracing::info!("Starting in Cashu escrow mode (LND not required)");
let escrow: Arc<dyn escrow::EscrowBackend> = Arc::new(cashu::CashuBackend::new());
let ctx = AppContext::new(
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 Attach the configured Cashu client before starting the event loop

When [cashu].enabled = true, this branch constructs AppContext and immediately calls run_cashu, but AppContext::new initializes cashu_client to None. A repo-wide search shows that CashuClient::connect and with_cashu_client are never called. Consequently, every seller submission reaches add_cashu_escrow_action and fails with AddCashuEscrow dispatched without a Cashu client, so no Cashu order can advance from WaitingPayment to Active. Connect to the configured mint URL and attach the resulting client before entering run_cashu.

Useful? React with 👍 / 👎.

Comment thread src/cashu/mod.rs
.map(|s| cdk::dhke::hash_to_curve(s.as_bytes()))
.collect::<Result<Vec<PublicKey>, _>>()
.map_err(|e| Error::Token(format!("proof Y: {e}")))?;
let states = self.check_state(ys).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 Authenticate Cashu proofs before activating the order

Once the startup wiring is fixed, a malicious seller can submit a fabricated token and make the buyer send fiat without locking mint-backed funds. This path checks only the declared amount, P2PK secret structure, configured mint URL, and /v1/checkstate; as the comment on check_state itself notes, that endpoint proves only that secrets are unspent and does not authenticate mint signatures. An attacker can choose previously unseen secrets with the expected 2-of-3 condition, arbitrary proof points, and the required amount; an honest mint reports those unknown secrets as unspent, and the order is accepted. Require DLEQ-backed issuance verification or another mint-authenticating validation before persisting the escrow and transitioning to Active.

Useful? React with 👍 / 👎.

Two P1 issues from the Codex review of #765:

1. Attach the Cashu client before the event loop. The Cashu boot branch
   built AppContext but never called CashuClient::connect / with_cashu_client,
   so cashu_client stayed None and every AddCashuEscrow failed with
   "dispatched without a Cashu client" — no order could reach Active. main now
   connects the configured mint and attaches the client, panicking if the mint
   is unreachable (mirroring how Lightning mode refuses to boot without LND).

2. Authenticate proofs as mint-issued before activating the order.
   verify_escrow_token relied on /v1/checkstate, which reports any unknown
   secret as Unspent — a seller could fabricate proofs with the right 2-of-3
   condition and amount but no mint signature, pass every check, and trick the
   buyer into sending fiat against worthless ecash. Now verify the NUT-12 DLEQ
   proof of every proof (verify_token_dleq) before the unspent check.

cargo fmt + clippy --all-targets --all-features -D warnings clean; 388 unit
tests + cashu mint integration test pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@grunch
Copy link
Copy Markdown
Member Author

grunch commented Jun 1, 2026

@codex review

@grunch
Copy link
Copy Markdown
Member Author

grunch commented Jun 1, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 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

@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: f532ef062c

ℹ️ 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/cashu/mod.rs
Comment on lines +182 to +187
// 3: the locked amount must equal the order amount exactly.
let value = token
.value()
.map_err(|e| Error::Token(format!("token value: {e}")))?
.to_u64();
if value != expected_amount {
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 Require sat-denominated escrow tokens

When the configured mint exposes more than one currency unit, this accepts a token whose numeric value matches order.amount without checking that its unit is sat. Cashu tokens carry an explicit unit, and keysets may use units such as msat; for example, a 100,000-sat order can be advanced to Active with a valid 100,000-msat token worth only 100 sats. Verify the parsed token unit before persisting the lock.

Useful? React with 👍 / 👎.

Comment thread src/cashu/mod.rs
@@ -59,19 +61,29 @@ impl CashuClient {
Ok(info) => {
if !info.nuts.nut11.supported {
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 Check every required mint capability before booting

When a mint advertises NUT-11 but not NUT-07 or NUT-12, Cashu mode boots successfully even though every AddCashuEscrow request later fails: verify_escrow_token unconditionally calls /v1/checkstate and requires a DLEQ proof. These are separate optional Cashu extensions, so accepting a NUT-11-only mint leaves all taken orders stuck in WaitingPayment. Reject startup unless the mint also advertises the capabilities required by the lock path.

Useful? React with 👍 / 👎.

Comment thread src/cashu/mod.rs
Comment on lines 240 to 254
pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
let keysets = self.client.get_mint_keys().await.map_err(|e| Error::Client(cdk::error::Error::from(e)))?;
let keysets = self.client.get_mint_keys().await.map_err(Error::Client)?;

match token {
Token::TokenV3(token_v3) => {
let proofs = token_v3.token.iter().flat_map(|t| t.proofs.clone()).collect::<Vec<_>>();
let proofs = token_v3
.token
.iter()
.flat_map(|t| t.proofs.clone())
.collect::<Vec<_>>();
for proof in proofs {
let keyset = keysets.iter().find(|k| ShortKeysetId::from(k.id) == proof.keyset_id)
let keyset = keysets
.iter()
.find(|k| ShortKeysetId::from(k.id) == proof.keyset_id)
.ok_or_else(|| Error::Token("Unknown keyset".into()))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Load keys for inactive keysets when verifying DLEQ

When the mint rotates keysets after an escrow token is created, this fetches only /v1/keys and reports Unknown keyset for the still-valid token. The Cashu NUT-01 specification states that /v1/keys returns only active keysets while proofs from inactive keysets remain accepted as inputs; keys for a specific active or inactive keyset must be requested via /v1/keys/{keyset_id}. Resolve each proof's keyset ID through that endpoint so a rotation does not prevent a seller from locking valid funds.

Useful? React with 👍 / 👎.

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app.rs (1)

204-265: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject AddCashuEscrow explicitly outside Cashu mode.

In the normal run() path this action falls into the default arm and returns Ok(()), so unsupported Cashu-lock requests get silently ignored instead of returning InvalidAction.

Suggested fix
         Action::PayInvoice => Err(MostroError::MostroCantDo(CantDoReason::InvalidAction).into()),
+        Action::AddCashuEscrow => {
+            Err(MostroError::MostroCantDo(CantDoReason::InvalidAction).into())
+        }
         Action::LastTradeIndex => last_trade_index(ctx, msg, event, my_keys)
             .await
             .map_err(|e| e.into()),
🤖 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.rs` around lines 204 - 265, handle_message_action_no_ln currently
falls through Cashu-specific requests like Action::AddCashuEscrow to the
wildcard and returns Ok(()), silently ignoring them; update
handle_message_action_no_ln to explicitly match Action::AddCashuEscrow and
return Err(MostroError::MostroCantDo(CantDoReason::InvalidAction).into()) so
Cashu-lock requests are rejected when not in Cashu mode. Locate the match in
handle_message_action_no_ln and add a branch for Action::AddCashuEscrow that
returns the InvalidAction error (mirroring how Action::PayInvoice is handled).
src/app/take_sell.rs (1)

166-222: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip LN invoice validation on the Cashu path.

validate_invoice() still runs before the Cashu-mode branch, so TakeSell can fail on an irrelevant Lightning invoice even though the flow immediately switches to show_cashu_escrow_request and never uses payment_request.

Suggested fix
-    // Validate invoice and get payment request if present
-    // NOW dev_fee is set correctly for proper validation
-    let payment_request = validate_invoice(&msg, &order).await?;
-
     let trade_index = match msg.get_inner_message_kind().trade_index {
         Some(trade_index) => trade_index,
         None => {
@@
     if Settings::is_cashu_enabled() {
         show_cashu_escrow_request(my_keys, &event.sender, &seller_pubkey, order, request_id)
             .await?;
         return Ok(());
     }
+
+    // Validate invoice only on the LN path.
+    let payment_request = validate_invoice(&msg, &order).await?;
 
     // If payment request is not present, update order status to waiting buyer invoice
     if payment_request.is_none() {
         update_order_status(&mut order, my_keys, pool, request_id).await?;
🤖 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/take_sell.rs` around lines 166 - 222, The code currently calls
validate_invoice(&msg, &order).await? before checking
Settings::is_cashu_enabled(), causing Cashu-mode takes to fail on irrelevant LN
invoice validation; change the flow to skip validate_invoice when cashu is
enabled by either: (a) moving the call to validate_invoice so it runs only after
the cashu/bond branches (i.e. after checking bond_required and
Settings::is_cashu_enabled()), or (b) wrapping it in if
!Settings::is_cashu_enabled() { let payment_request = validate_invoice(&msg,
&order).await?; } and ensuring payment_request (used by bond::request_taker_bond
and later non-bond logic) is conditionally available when needed; update
references to payment_request accordingly and keep show_cashu_escrow_request,
update_user_trade_index, and bond::request_taker_bond logic unchanged.
🧹 Nitpick comments (1)
src/util.rs (1)

886-968: 💤 Low value

Unused variable locked_amount.

locked_amount is assigned on line 918 but never used afterward. The comment explains the intent (the value add_cashu_escrow_action validates against), but the variable itself is dead code in this function—the assignment to new_order.amount on line 941 could use order.amount directly.

♻️ Suggested fix
-    let locked_amount = order.amount;
-
     order.status = Status::WaitingPayment.to_string();
     ...
-    new_order.amount = locked_amount;
+    new_order.amount = order.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 `@src/util.rs` around lines 886 - 968, The function show_cashu_escrow_request
declares locked_amount but never uses it; remove the dead variable and instead
set new_order.amount directly from order.amount (or use locked_amount
consistently if you prefer) so the value validated by add_cashu_escrow_action is
preserved; update the assignment at new_order.amount to use order.amount and
delete the locked_amount binding to eliminate the unused-variable lint.
🤖 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 `@src/cashu/mod.rs`:
- Around line 208-219: The code currently only checks that returned states are
Unspent but doesn't verify the mint returned as many state entries as requests;
after creating ys and calling self.check_state(ys) ensure that
states.states.len() equals the number of requested proofs (e.g., secrets.len()
or ys.len()) and return an Err (e.g., Error::Token with a clear message like
"mismatched states length" or "missing state entries") if it is shorter, before
iterating to check State::Unspent; reference the variables/functions ys,
self.check_state, and states.states/State::Unspent to locate where to add this
length check.

---

Outside diff comments:
In `@src/app.rs`:
- Around line 204-265: handle_message_action_no_ln currently falls through
Cashu-specific requests like Action::AddCashuEscrow to the wildcard and returns
Ok(()), silently ignoring them; update handle_message_action_no_ln to explicitly
match Action::AddCashuEscrow and return
Err(MostroError::MostroCantDo(CantDoReason::InvalidAction).into()) so Cashu-lock
requests are rejected when not in Cashu mode. Locate the match in
handle_message_action_no_ln and add a branch for Action::AddCashuEscrow that
returns the InvalidAction error (mirroring how Action::PayInvoice is handled).

In `@src/app/take_sell.rs`:
- Around line 166-222: The code currently calls validate_invoice(&msg,
&order).await? before checking Settings::is_cashu_enabled(), causing Cashu-mode
takes to fail on irrelevant LN invoice validation; change the flow to skip
validate_invoice when cashu is enabled by either: (a) moving the call to
validate_invoice so it runs only after the cashu/bond branches (i.e. after
checking bond_required and Settings::is_cashu_enabled()), or (b) wrapping it in
if !Settings::is_cashu_enabled() { let payment_request = validate_invoice(&msg,
&order).await?; } and ensuring payment_request (used by bond::request_taker_bond
and later non-bond logic) is conditionally available when needed; update
references to payment_request accordingly and keep show_cashu_escrow_request,
update_user_trade_index, and bond::request_taker_bond logic unchanged.

---

Nitpick comments:
In `@src/util.rs`:
- Around line 886-968: The function show_cashu_escrow_request declares
locked_amount but never uses it; remove the dead variable and instead set
new_order.amount directly from order.amount (or use locked_amount consistently
if you prefer) so the value validated by add_cashu_escrow_action is preserved;
update the assignment at new_order.amount to use order.amount and delete the
locked_amount binding to eliminate the unused-variable lint.
🪄 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: 8ce906e2-0fe4-42ce-b004-f45aecc40591

📥 Commits

Reviewing files that changed from the base of the PR and between c445946 and f532ef0.

📒 Files selected for processing (14)
  • src/app.rs
  • src/app/add_cashu_escrow.rs
  • src/app/add_invoice.rs
  • src/app/admin_settle.rs
  • src/app/bond/flow.rs
  • src/app/context.rs
  • src/app/release.rs
  • src/app/take_buy.rs
  • src/app/take_sell.rs
  • src/cashu/mod.rs
  • src/escrow.rs
  • src/main.rs
  • src/rpc/service.rs
  • src/util.rs

Comment thread src/cashu/mod.rs
Comment on lines +208 to +219
let ys = secrets
.iter()
.map(|s| cdk::dhke::hash_to_curve(s.as_bytes()))
.collect::<Result<Vec<PublicKey>, _>>()
.map_err(|e| Error::Token(format!("proof Y: {e}")))?;
let states = self.check_state(ys).await?;
if states
.states
.iter()
.any(|s| s.state != cdk::nuts::State::Unspent)
{
return Err(Error::Token("one or more proofs are not unspent".into()));
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed if /checkstate returns fewer entries than requested.

This only rejects when a returned state is not Unspent. If the mint responds with a shorter states vector, the missing proofs are treated as implicitly valid and the escrow can be accepted with some proofs never checked.

Suggested fix
-        let states = self.check_state(ys).await?;
+        let expected_states = ys.len();
+        let states = self.check_state(ys).await?;
+        if states.states.len() != expected_states {
+            return Err(Error::Token(format!(
+                "checkstate returned {} states for {expected_states} proofs",
+                states.states.len()
+            )));
+        }
         if states
             .states
             .iter()
             .any(|s| s.state != cdk::nuts::State::Unspent)
         {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let ys = secrets
.iter()
.map(|s| cdk::dhke::hash_to_curve(s.as_bytes()))
.collect::<Result<Vec<PublicKey>, _>>()
.map_err(|e| Error::Token(format!("proof Y: {e}")))?;
let states = self.check_state(ys).await?;
if states
.states
.iter()
.any(|s| s.state != cdk::nuts::State::Unspent)
{
return Err(Error::Token("one or more proofs are not unspent".into()));
let ys = secrets
.iter()
.map(|s| cdk::dhke::hash_to_curve(s.as_bytes()))
.collect::<Result<Vec<PublicKey>, _>>()
.map_err(|e| Error::Token(format!("proof Y: {e}")))?;
let expected_states = ys.len();
let states = self.check_state(ys).await?;
if states.states.len() != expected_states {
return Err(Error::Token(format!(
"checkstate returned {} states for {expected_states} proofs",
states.states.len()
)));
}
if states
.states
.iter()
.any(|s| s.state != cdk::nuts::State::Unspent)
{
return Err(Error::Token("one or more proofs are not unspent".into()));
🤖 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/cashu/mod.rs` around lines 208 - 219, The code currently only checks that
returned states are Unspent but doesn't verify the mint returned as many state
entries as requests; after creating ys and calling self.check_state(ys) ensure
that states.states.len() equals the number of requested proofs (e.g.,
secrets.len() or ys.len()) and return an Err (e.g., Error::Token with a clear
message like "mismatched states length" or "missing state entries") if it is
shorter, before iterating to check State::Unspent; reference the
variables/functions ys, self.check_state, and states.states/State::Unspent to
locate where to add this length check.

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