Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
94 changes: 94 additions & 0 deletions rust/src/api/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,11 @@ pub async fn take_order(
// Subscribe to d-tag K38383 updates for this specific order so we
// receive status changes (pending → in-progress → waiting-payment …).
subscribe_single_order(&order_id).await;
// Retaking within the grace window: drop the canceled order's deferred
// session so this take gets a fresh one under its own trade key.
crate::mostro::session::session_manager()
.resolve_deferred_removal(&order_id)
.await;
// Create a session so the chat API can look up keys immediately.
let _ = crate::mostro::session::session_manager()
.create_session(
Expand Down Expand Up @@ -1831,6 +1836,16 @@ async fn dispatch_mostro_message(
if let Some(order_id) = &kind.id {
let oid = order_id.to_string();
order_book().remove_order(&oid).await;
// Read before the sync below overwrites it with Canceled.
let prev_status = match crate::db::app_db::db() {
Some(db) => db
.get_trade_by_order_id(&oid)
.await
.ok()
.flatten()
.map(|t| t.order.status),
None => None,
};
// Sync the Canceled status into the trade DB so My Trades
// reflects the cancellation immediately.
if let Some(db) = crate::db::app_db::db() {
Expand All @@ -1846,6 +1861,7 @@ async fn dispatch_mostro_message(
log::warn!("[orders] failed to sync Canceled status for {oid}: {e}");
}
}
apply_cancel_cleanup(&oid, prev_status.as_ref()).await;
}
}
// Seller receives BuyerTookOrder → peer is buyer_trade_pubkey.
Expand Down Expand Up @@ -2151,6 +2167,9 @@ async fn dispatch_mostro_message(
log::info!(
"[orders] gift-wrap BondSlashed: order={order_id} amount={amount_sats} cause={cause:?}"
);
crate::mostro::session::session_manager()
.resolve_deferred_removal(&order_id)
.await;
crate::api::bond::emit_bond_slashed(crate::api::types::BondSlashedEvent {
event_id: event_id.to_string(),
order_id,
Expand All @@ -2167,6 +2186,22 @@ async fn dispatch_mostro_message(
}
}

/// A cancel that may be followed by a `bond-slashed` keeps its session alive
/// for the grace period, so the trailing notice still decrypts.
async fn apply_cancel_cleanup(order_id: &str, prev_status: Option<&OrderStatus>) {
use crate::mostro::session::{
cancel_cleanup, defer_session_removal, session_manager, CancelCleanup,
BOND_SLASH_GRACE_SECS,
};
match cancel_cleanup(prev_status) {
CancelCleanup::Keep => {}
CancelCleanup::Immediate => session_manager().remove_session(order_id).await,
CancelCleanup::Defer => {
defer_session_removal(order_id.to_string(), BOND_SLASH_GRACE_SECS).await;
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
Outdated
}
}
}

/// Maps a `mostro_core::order::Status` to the local [`OrderStatus`] enum.
/// Map a daemon action to the order status it implies, for messages that
/// carry no explicit status payload (action-only progression replies).
Expand Down Expand Up @@ -3729,6 +3764,65 @@ mod tests {
assert!(session.shared_key.is_none());
}

// ── Cancel cleanup ────────────────────────────────────────────────────────

async fn session_for_cleanup() -> String {
let order_id = uuid::Uuid::new_v4().to_string();
session_manager()
.create_session(
order_id.clone(),
TradeRole::Buyer,
0,
dummy_order_info(&order_id),
)
.await
.expect("create_session");
order_id
}

#[tokio::test]
async fn a_committed_cancel_keeps_the_session_for_the_slash_notice() {
let order_id = session_for_cleanup().await;

apply_cancel_cleanup(&order_id, Some(&OrderStatus::WaitingBuyerInvoice)).await;

assert!(session_manager().get_session(&order_id).await.is_some());
}

#[tokio::test]
async fn a_pending_cancel_drops_the_session_at_once() {
let order_id = session_for_cleanup().await;

apply_cancel_cleanup(&order_id, Some(&OrderStatus::Pending)).await;

assert!(session_manager().get_session(&order_id).await.is_none());
}

#[tokio::test]
async fn a_disputed_cancel_keeps_the_session_for_the_admin_chat() {
let order_id = session_for_cleanup().await;

apply_cancel_cleanup(&order_id, Some(&OrderStatus::Dispute)).await;

assert!(session_manager().get_session(&order_id).await.is_some());
}

#[tokio::test]
async fn a_trailing_bond_slashed_settles_the_deferred_session() {
let order_id = session_for_cleanup().await;

apply_cancel_cleanup(&order_id, Some(&OrderStatus::WaitingPayment)).await;
assert!(session_manager().get_session(&order_id).await.is_some());

assert!(
session_manager()
.resolve_deferred_removal(&order_id)
.await,
"the cancel must have left a deferred removal for the notice to settle"
);
assert!(session_manager().get_session(&order_id).await.is_none());
}

// ── Peer-pubkey resolution ────────────────────────────────────────────────

/// on_peer_pubkey_received with no session for the order is a graceful no-op.
Expand Down
Loading
Loading