Skip to content
Draft
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bc24a88
feat(cashu): implement Track B - release happy path
a1denvalu3 May 30, 2026
924f1b6
feat(cashu): send P_M signature to buyer on release
a1denvalu3 May 30, 2026
bab00e4
feat(bond): Phase 4.5 — re-prompt winner for payout invoice on paymen…
grunch Jun 2, 2026
828114b
feat(bond): Phase 5 — maker bond (non-range) + dispute slash (#767)
grunch Jun 9, 2026
eed5ede
docs: document daemon event kinds (#769)
ermeme[bot] Jun 9, 2026
c7db272
feat(price): Phase 1 — Yadio provider + PriceManager wiring (#753)
grunch Jun 11, 2026
af8f05d
feat(bond): Phase 6 — range-order maker bond with proportional slashe…
grunch Jun 12, 2026
c610c34
fix(price): repair test-only price seeding broken by #753/#770 merge …
grunch Jun 12, 2026
be1bd5a
feat(bond): Phase 7 — maker timeout slash (#775)
grunch Jun 15, 2026
1d6c5ba
docs(bond): Phase 8 — public config exposure + operator docs (#777)
grunch Jun 16, 2026
14299b1
feat(price): Phase 2 — direct backup quoters + multi-source aggregati…
grunch Jun 16, 2026
ed10678
Update CHANGELOG for version 0.17.5
grunch Jun 16, 2026
5b07be2
chore: Release mostro version 0.17.5
grunch Jun 16, 2026
ba86102
feat(transport): Phase 1 — wire protocol v2 (NIP-44 direct) into most…
grunch Jun 16, 2026
68a125e
feat(bond): notify slashed party on dispute slash (#768) (#779)
Catrya Jun 16, 2026
23b759b
feat(transport): Phase 2 — anti-spam gates for protocol v2 (#780)
grunch Jun 17, 2026
4c8259b
feat(transport): log active transport at mostrod startup (#781)
grunch Jun 17, 2026
5801afe
fix(nip33): rename info tag protocol_versions -> protocol_version (#782)
grunch Jun 17, 2026
82f1923
feat(price): Phase 3 — El Toque fiat-cross provider (CUP/MLC) (#778)
grunch Jun 18, 2026
4a6722a
fix: surface InvalidOrderId to clients as CantDo(NotFound) (#752)
AndreaDiazCorreia Jun 19, 2026
6802c97
feat(transport): stamp inner protocol version to match active transpo…
grunch Jun 19, 2026
f64e93b
ci(mutation): run as scheduled audit + opt-in, not on every push to m…
grunch Jun 19, 2026
2514856
fix(cashu): harden release happy path and fix CI blockers
grunch Jun 24, 2026
ee9532e
Merge remote-tracking branch 'origin/main' into feature/cashu-track-b
grunch Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 114 additions & 29 deletions src/app/release.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::app::bond;
use crate::app::context::AppContext;
use crate::app::dispute::close_dispute_after_user_resolution;
use crate::config::settings::Settings;
use crate::escrow::EscrowBackend;
use crate::lightning::LndConnector;
use crate::lnurl::resolv_ln_address;
Expand Down Expand Up @@ -189,35 +190,117 @@ pub async fn release_action(
.get_next_trade_key()
.map_err(MostroInternalErr)?;

// Settle seller hold invoice
settle_seller_hold_invoice(event, ln_client, Action::Released, false, &order).await?;
// Update order event with status SettledHoldInvoice
order = update_order_event(my_keys, Status::SettledHoldInvoice, &order)
.await
.map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;

// Persist the status change to DB before calling do_payment.
// do_payment spawns async tasks that capture an Order copy; without this
// explicit write the settled-hold-invoice status only lived in memory and
// was persisted as a side-effect of the full-row writes in
// check_failure_retries / payment_success (now replaced by targeted updates).
let result =
sqlx::query("UPDATE orders SET status = ?, event_id = ? WHERE id = ? AND status IN (?, ?)")
.bind(&order.status)
.bind(&order.event_id)
.bind(order.id)
.bind(Status::FiatSent.to_string())
.bind(Status::Dispute.to_string())
.execute(pool)
let is_cashu = Settings::is_cashu_enabled() && order.cashu_escrow_token.is_some();

if is_cashu {
// Cashu flow: skip lightning invoice settlement and go straight to Success.
// Update order event with status Success
order = update_order_event(my_keys, Status::Success, &order)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
.map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;

let result =
sqlx::query("UPDATE orders SET status = ?, event_id = ? WHERE id = ? AND status IN (?, ?)")
.bind(&order.status)
.bind(&order.event_id)
.bind(order.id)
.bind(Status::FiatSent.to_string())
.bind(Status::Dispute.to_string())
.execute(pool)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;

if result.rows_affected() == 0 {
tracing::warn!(
"Order {} not transitioned to success: status changed concurrently",
order.id
);
return Ok(());
}

if result.rows_affected() == 0 {
tracing::warn!(
"Order {} not transitioned to settled-hold-invoice: status changed concurrently",
order.id
);
return Ok(());
// Send PurchaseCompleted message to buyer
enqueue_order_msg(
None,
Some(order.id),
Action::PurchaseCompleted,
None,
buyer_pubkey,
None,
)
.await;

// Generate and send PM signatures to the buyer "just in case" the seller forgot
// to send their own signature to the buyer via NIP-59 DM.
let mut pm_signatures = Vec::new();
let token_str = order.cashu_escrow_token.as_ref().unwrap();
if let Ok(token) = cdk::nuts::Token::from_str(token_str) {
let secrets = token.token_secrets();
if let Ok(p_m_secret) = cdk::nuts::nut01::SecretKey::from_str(&my_keys.secret_key().to_secret_hex()) {
for secret in secrets {
let msg = secret.to_bytes();
if let Ok(sig) = p_m_secret.sign(&msg) {
pm_signatures.push(mostro_core::message::CashuProofSignature::new(
secret.to_string(),
sig.to_string(),
));
}
}
}
}

if !pm_signatures.is_empty() {
enqueue_order_msg(
request_id,
Some(order.id),
Action::CashuPmSignature,
Some(Payload::CashuSignatures(pm_signatures)),
buyer_pubkey,
None,
).await;
}

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

Log errors during Cashu signature generation to aid debugging.

The nested if let Ok(...) chains silently discard errors from token parsing, key conversion, and signing. If any step fails, the buyer may receive no CashuPmSignature (or partial signatures), and there's no diagnostic trail. Since P_M signatures are critical for the buyer to redeem the Cashu escrow, failures here should be logged.

Proposed fix with error logging
         let token_str = order.cashu_escrow_token.as_ref().unwrap();
-        if let Ok(token) = cdk::nuts::Token::from_str(token_str) {
-            let secrets = token.token_secrets();
-            if let Ok(p_m_secret) = cdk::nuts::nut01::SecretKey::from_str(&my_keys.secret_key().to_secret_hex()) {
-                for secret in secrets {
-                    let msg = secret.to_bytes();
-                    if let Ok(sig) = p_m_secret.sign(&msg) {
-                        pm_signatures.push(mostro_core::message::CashuProofSignature::new(
-                            secret.to_string(),
-                            sig.to_string(),
-                        ));
+        match cdk::nuts::Token::from_str(token_str) {
+            Ok(token) => {
+                let secrets = token.token_secrets();
+                match cdk::nuts::nut01::SecretKey::from_str(&my_keys.secret_key().to_secret_hex()) {
+                    Ok(p_m_secret) => {
+                        for secret in secrets {
+                            let msg = secret.to_bytes();
+                            match p_m_secret.sign(&msg) {
+                                Ok(sig) => {
+                                    pm_signatures.push(mostro_core::message::CashuProofSignature::new(
+                                        secret.to_string(),
+                                        sig.to_string(),
+                                    ));
+                                }
+                                Err(e) => {
+                                    tracing::warn!("Order {}: failed to sign Cashu secret: {}", order.id, e);
+                                }
+                            }
+                        }
                     }
+                    Err(e) => {
+                        tracing::warn!("Order {}: failed to parse P_M secret key: {}", order.id, e);
+                    }
                 }
             }
+            Err(e) => {
+                tracing::warn!("Order {}: failed to parse Cashu escrow token: {}", order.id, e);
+            }
         }
📝 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 mut pm_signatures = Vec::new();
let token_str = order.cashu_escrow_token.as_ref().unwrap();
if let Ok(token) = cdk::nuts::Token::from_str(token_str) {
let secrets = token.token_secrets();
if let Ok(p_m_secret) = cdk::nuts::nut01::SecretKey::from_str(&my_keys.secret_key().to_secret_hex()) {
for secret in secrets {
let msg = secret.to_bytes();
if let Ok(sig) = p_m_secret.sign(&msg) {
pm_signatures.push(mostro_core::message::CashuProofSignature::new(
secret.to_string(),
sig.to_string(),
));
}
}
}
}
if !pm_signatures.is_empty() {
enqueue_order_msg(
request_id,
Some(order.id),
Action::CashuPmSignature,
Some(Payload::CashuSignatures(pm_signatures)),
buyer_pubkey,
None,
).await;
}
let mut pm_signatures = Vec::new();
let token_str = order.cashu_escrow_token.as_ref().unwrap();
match cdk::nuts::Token::from_str(token_str) {
Ok(token) => {
let secrets = token.token_secrets();
match cdk::nuts::nut01::SecretKey::from_str(&my_keys.secret_key().to_secret_hex()) {
Ok(p_m_secret) => {
for secret in secrets {
let msg = secret.to_bytes();
match p_m_secret.sign(&msg) {
Ok(sig) => {
pm_signatures.push(mostro_core::message::CashuProofSignature::new(
secret.to_string(),
sig.to_string(),
));
}
Err(e) => {
tracing::warn!("Order {}: failed to sign Cashu secret: {}", order.id, e);
}
}
}
}
Err(e) => {
tracing::warn!("Order {}: failed to parse P_M secret key: {}", order.id, e);
}
}
}
Err(e) => {
tracing::warn!("Order {}: failed to parse Cashu escrow token: {}", order.id, e);
}
}
if !pm_signatures.is_empty() {
enqueue_order_msg(
request_id,
Some(order.id),
Action::CashuPmSignature,
Some(Payload::CashuSignatures(pm_signatures)),
buyer_pubkey,
None,
).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/release.rs` around lines 234 - 260, The token parsing / key
conversion / signing currently swallows errors; update the block that builds
pm_signatures so each fallible operation logs failures (include the offending
token_str/order.id and request_id for context): log errors from
cdk::nuts::Token::from_str(order.cashu_escrow_token), from
cdk::nuts::nut01::SecretKey::from_str(my_keys.secret_key().to_secret_hex()), and
from p_m_secret.sign(&msg) before continuing, and still push any successful
CashuProofSignature::new entries into pm_signatures; use the existing logging
framework (e.g., tracing::error! or the module's logger) and ensure logs mention
functions/symbols like Token::from_str, SecretKey::from_str, p_m_secret.sign,
and CashuProofSignature so failures are diagnosable.


// Send dm to buyer to rate counterpart
enqueue_order_msg(
request_id,
Some(order.id),
Action::Rate,
None,
buyer_pubkey,
None,
)
.await;
} else {
// Lightning flow
// Settle seller hold invoice
settle_seller_hold_invoice(event, ln_client, Action::Released, false, &order).await?;
// Update order event with status SettledHoldInvoice
order = update_order_event(my_keys, Status::SettledHoldInvoice, &order)
.await
.map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;

// Persist the status change to DB before calling do_payment.
// do_payment spawns async tasks that capture an Order copy; without this
// explicit write the settled-hold-invoice status only lived in memory and
// was persisted as a side-effect of the full-row writes in
// check_failure_retries / payment_success (now replaced by targeted updates).
let result =
sqlx::query("UPDATE orders SET status = ?, event_id = ? WHERE id = ? AND status IN (?, ?)")
.bind(&order.status)
.bind(&order.event_id)
.bind(order.id)
.bind(Status::FiatSent.to_string())
.bind(Status::Dispute.to_string())
.execute(pool)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;

if result.rows_affected() == 0 {
tracing::warn!(
"Order {} not transitioned to settled-hold-invoice: status changed concurrently",
order.id
);
return Ok(());
}
}

// If there was an active dispute on this order, close it since the seller
Expand Down Expand Up @@ -276,8 +359,10 @@ pub async fn release_action(
// does not block trade finalization.
bond::release_bonds_for_order_or_warn(pool, order.id, "release_action").await;

// Finally we try to pay buyer's invoice
let _ = do_payment(ctx, order, request_id).await;
if !is_cashu {
// Finally we try to pay buyer's invoice
let _ = do_payment(ctx, order, request_id).await;
}

Ok(())
}
Expand Down