Skip to content

BugFix: users can cancel an order cooperatively at the same time#733

Open
Matobi98 wants to merge 14 commits intolnp2pBot:mainfrom
Matobi98:issue583
Open

BugFix: users can cancel an order cooperatively at the same time#733
Matobi98 wants to merge 14 commits intolnp2pBot:mainfrom
Matobi98:issue583

Conversation

@Matobi98
Copy link
Contributor

@Matobi98 Matobi98 commented Feb 2, 2026

Description

Fix race condition when processing simultaneous cooperative cancellations: if buyer and seller send /cancel order-id at nearly the same time both flags (buyer_cooperativecancel, seller_cooperativecancel) were set but the order remained in Active status.

Now the operation uses an atomic update/lock so when both confirm the order transitions to cancelled and bot replies remain consistent.

Summary by CodeRabbit

  • Refactor
    • Cooperative cancellation flow now relies on refreshed order state throughout, re-checks both parties' cancel flags to prevent race conditions, and persists the finalized cancellation before sending notifications and events.
  • Bug Fixes
    • Duplicate or concurrent cancellation requests are detected and handled gracefully by asking callers to wait, avoiding unintended or stale state mutations.

…nction and update the cancelOrder command to utilize it.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Introduces setCooperativeCancelFlag(orderId, role) and refactors cancelOrder to use the atomic update result (updateOrder) instead of in-memory mutation; treats a null return as duplicate, re-checks both flags for races, and sets status to CANCELED when both parties requested cancellation.

Changes

Cohort / File(s) Summary
Cooperative Cancel Refactor
bot/commands.ts
Adds helper setCooperativeCancelFlag(orderId, role) using findOneAndUpdate; cancelOrder now uses returned updateOrder (null → duplicate), re-checks cooperative flags to avoid races, sets status = 'CANCELED' when both requested, and uses updateOrder for messaging/events instead of mutating the original order.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • Catrya
  • grunch

Poem

🐇 I nudged a flag with careful paw,
Two callers tapped and kept to law,
updateOrder now has the final say,
We save, emit, and hop away,
Hooray — two cancels paved the way. 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'BugFix: users can cancel an order cooperatively at the same time' directly summarizes the main change: fixing a race condition in cooperative order cancellation when both parties act simultaneously.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Caution

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

⚠️ Outside diff range comments (1)
bot/commands.ts (1)

734-758: ⚠️ Potential issue | 🟠 Major

Emit orderUpdated with the freshly saved order to avoid stale event payloads.

updateOrder is persisted with status = 'CANCELED', but the event is emitted with the pre-update order, so listeners may see ACTIVE and miss the cancellation. Emit updateOrder instead (or re-fetch) to keep downstream state consistent.

🛠️ Proposed fix
-      logger.info('cancelOrder => OrderEvents.orderUpdated(order);');
-      OrderEvents.orderUpdated(order);
+      logger.info('cancelOrder => OrderEvents.orderUpdated(updateOrder);');
+      OrderEvents.orderUpdated(updateOrder);

Copy link
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.

Good approach using findOneAndUpdate to make the flag-setting atomic — that's the right direction for fixing this race condition. However, there are several issues that need to be addressed before this can be merged:

1. Stale event emitted (Bug)

Line 754: OrderEvents.orderUpdated(order) still emits the original order object (stale, status still active) instead of updateOrder (which has status = CANCELED). This means downstream listeners will receive incorrect data. Should be:

OrderEvents.orderUpdated(updateOrder);

CodeRabbit also flagged this.

2. Race condition not fully solved

The current fix makes the flag-setting atomic, but the check + cancel is still two separate operations. Consider this scenario:

  1. Buyer calls setCooperativeCancelFlag → sets buyer_cooperativecancel = true, gets back order with seller_cooperativecancel = false
  2. Seller calls setCooperativeCancelFlag → sets seller_cooperativecancel = true, gets back order with buyer_cooperativecancel = true
  3. Seller's code sees both flags true → cancels the order ✅
  4. Buyer's code sees only their flag true → sends "waiting for counterparty" message ❌

Buyer gets a stale "waiting" message even though the order was already cancelled by seller's execution. The truly atomic fix would be to do the full cancel logic inside findOneAndUpdate with a condition that checks both flags, or re-read the order after setting the flag.

3. Missing duplicate cancel guard

The original code had: if (order[initiatorCooperativeCancelProperty]) return shouldWaitCooperativeCancelMessage(...) — this prevented a user from requesting cancel twice. The new code removed this guard. Now if a user sends /cancel twice, findOneAndUpdate will succeed both times (idempotent $set: true), but the user won't get the "you already requested this" feedback.

4. Silent failure on null

Line 720: if (!updateOrder) return; — if the order is not found, the function silently returns with no feedback to the user. Should at least log a warning or send an error message.

5. Minor: simplify setCooperativeCancelFlag

The propName construction can be a ternary:

const propName = role === 'buyer' ? 'buyer_cooperativecancel' : 'seller_cooperativecancel';

No need for let + empty string + if/else block.

Copy link
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 (1)
bot/commands.ts (1)

700-711: ⚠️ Potential issue | 🔴 Critical

Remove unused counterParty local to unblock lint.

counterParty is assigned but never read, and this is currently failing CI lint.

Suggested fix
-    let counterPartyUser, initiator: 'buyer' | 'seller', counterParty;
+    let counterPartyUser, initiator: 'buyer' | 'seller';
@@
-      counterParty = 'seller';
@@
-      counterParty = 'buyer';

As per coding guidelines, "Follow ESLint Standard plus TypeScript rules and address warnings instead of disabling them".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` around lines 700 - 711, The local variable counterParty is
declared and assigned but never used; remove it to satisfy linting: delete
counterParty from the variable declaration (currently alongside counterPartyUser
and initiator) and remove the two assignments that set counterParty = 'seller' /
'buyer' inside the initiatorUser conditional; keep counterPartyUser and
initiator (and their assignments) intact, and run a quick search for any other
references to counterParty to ensure nothing else depends on it before
committing.
🧹 Nitpick comments (1)
bot/commands.ts (1)

768-770: Drop the code-like info log line (or downgrade to debug).

logger.info('cancelOrder => OrderEvents.orderUpdated(updateOrder);'); looks like debug artifact and adds noise in production logs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` around lines 768 - 770, The info-level log "cancelOrder =>
OrderEvents.orderUpdated(updateOrder);" is a debug artifact and should be
removed or downgraded; update the cancelOrder flow to either delete the
logger.info(...) call or change it to logger.debug(...) so production logs
aren't noisy, keeping the subsequent call to
OrderEvents.orderUpdated(updateOrder) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bot/commands.ts`:
- Around line 28-36: The current TOCTOU comes from setting
buyer_cooperativecancel / seller_cooperativecancel after reading order state;
make the update atomic by including the guard in the DB query: in
setCooperativeCancelFlag use Order.findOneAndUpdate with a filter that ensures
the target property is not already true (e.g. { _id: orderId, [propName]: { $ne:
true } }), and perform a single $set update returning the new doc ({ new: true
}); if the call returns null the flag was already set so upstream logic should
treat it as a duplicate/failed request. Apply the same pattern to the other
similar block referenced (lines ~715-730).

---

Outside diff comments:
In `@bot/commands.ts`:
- Around line 700-711: The local variable counterParty is declared and assigned
but never used; remove it to satisfy linting: delete counterParty from the
variable declaration (currently alongside counterPartyUser and initiator) and
remove the two assignments that set counterParty = 'seller' / 'buyer' inside the
initiatorUser conditional; keep counterPartyUser and initiator (and their
assignments) intact, and run a quick search for any other references to
counterParty to ensure nothing else depends on it before committing.

---

Nitpick comments:
In `@bot/commands.ts`:
- Around line 768-770: The info-level log "cancelOrder =>
OrderEvents.orderUpdated(updateOrder);" is a debug artifact and should be
removed or downgraded; update the cancelOrder flow to either delete the
logger.info(...) call or change it to logger.debug(...) so production logs
aren't noisy, keeping the subsequent call to
OrderEvents.orderUpdated(updateOrder) intact.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8edb00b and 4bcbf8a.

📒 Files selected for processing (1)
  • bot/commands.ts

Copy link
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.

♻️ Duplicate comments (1)
bot/commands.ts (1)

28-36: ⚠️ Potential issue | 🟠 Major

Make cooperative-cancel duplicate protection atomic in the DB query.

The stale pre-check (hasRequestedCancel) plus _id-only update is still TOCTOU. Concurrent /cancel requests can both pass and produce inconsistent duplicate responses. Move the guard into findOneAndUpdate and treat null as duplicate/out-of-state.

Suggested fix
 const setCooperativeCancelFlag = async (
   orderId: string,
   role: 'buyer' | 'seller',
 ): Promise<IOrder | null> => {
   const propName =
     role === 'buyer' ? 'buyer_cooperativecancel' : 'seller_cooperativecancel';
-  const update = { [propName]: true };
-  return await Order.findOneAndUpdate({ _id: orderId }, update, { new: true });
+  return await Order.findOneAndUpdate(
+    {
+      _id: orderId,
+      status: { $in: ['ACTIVE', 'FIAT_SENT', 'DISPUTE'] },
+      [propName]: { $ne: true },
+    },
+    { $set: { [propName]: true } },
+    { new: true },
+  );
 };
@@
-    // Check if user already requested cancel (duplicate guard)
-    const hasRequestedCancel =
-      initiator === 'buyer'
-        ? order.buyer_cooperativecancel
-        : order.seller_cooperativecancel;
-    if (hasRequestedCancel) {
-      return await messages.shouldWaitCooperativeCancelMessage(ctx, user);
-    }
     const updateOrder = await setCooperativeCancelFlag(order._id, initiator);
-    // Handle null case with user feedback
+    // Null => duplicate click or order is no longer cooperative-cancelable.
     if (!updateOrder) {
-      logger.warn(
-        `Order ${orderId} not found when setting cooperative cancel flag`,
-      );
-      return ctx.reply(ctx.i18n.t('generic_error')); // Opción segura: respondemos un error genérico desde i18n
+      return await messages.shouldWaitCooperativeCancelMessage(ctx, user);
     }
#!/bin/bash
# Read-only verification: confirms whether the atomic guard is in query and stale pre-check is removed.
python - <<'PY'
from pathlib import Path

text = Path('bot/commands.ts').read_text()

start = text.find('const setCooperativeCancelFlag')
end = text.find('const waitPayment')
fn = text[start:end]

print("has_status_filter:", "status: { $in:" in fn)
print("has_flag_ne_guard:", "$ne: true" in fn)
print("uses_$set:", "$set" in fn)
print("stale_hasRequestedCancel_present:", "const hasRequestedCancel" in text)
PY

Also applies to: 713-728

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` around lines 28 - 36, Change setCooperativeCancelFlag to
perform an atomic guarded update: include the guard for current order status and
that the buyer_cooperativecancel/seller_cooperativecancel flag is not already
true inside the findOneAndUpdate query (use $ne: true) and use $set to set the
flag; remove the separate pre-check (e.g., any hasRequestedCancel boolean or
function) so there is no TOCTOU, and treat a null return from findOneAndUpdate
as a duplicate/out-of-state response. Ensure the change is applied to the other
identical block noted (around lines referenced) so both buyer and seller paths
use the same atomic query.
🧹 Nitpick comments (1)
bot/commands.ts (1)

766-768: Remove the hardcoded debug-style info log line.

logger.info('cancelOrder => OrderEvents.orderUpdated(updateOrder);'); is noisy and not operationally useful.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` around lines 766 - 768, Remove the noisy hardcoded debug log
by deleting the logger.info('cancelOrder =>
OrderEvents.orderUpdated(updateOrder);') call in the cancelOrder flow; ensure
the code still emits the fresh event via OrderEvents.orderUpdated(updateOrder)
(or, if a log is required, replace it with a concise, contextual message that
includes meaningful runtime details such as orderId and status).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@bot/commands.ts`:
- Around line 28-36: Change setCooperativeCancelFlag to perform an atomic
guarded update: include the guard for current order status and that the
buyer_cooperativecancel/seller_cooperativecancel flag is not already true inside
the findOneAndUpdate query (use $ne: true) and use $set to set the flag; remove
the separate pre-check (e.g., any hasRequestedCancel boolean or function) so
there is no TOCTOU, and treat a null return from findOneAndUpdate as a
duplicate/out-of-state response. Ensure the change is applied to the other
identical block noted (around lines referenced) so both buyer and seller paths
use the same atomic query.

---

Nitpick comments:
In `@bot/commands.ts`:
- Around line 766-768: Remove the noisy hardcoded debug log by deleting the
logger.info('cancelOrder => OrderEvents.orderUpdated(updateOrder);') call in the
cancelOrder flow; ensure the code still emits the fresh event via
OrderEvents.orderUpdated(updateOrder) (or, if a log is required, replace it with
a concise, contextual message that includes meaningful runtime details such as
orderId and status).

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4bcbf8a and 579db9d.

📒 Files selected for processing (1)
  • bot/commands.ts

@Luquitasjeffrey
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 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
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: 2

♻️ Duplicate comments (1)
bot/commands.ts (1)

35-39: ⚠️ Potential issue | 🟠 Major

Remaining TOCTOU: setCooperativeCancelFlag does not guard against invalid order statuses.

The status check at lines 694–701 is a separate read from the findOneAndUpdate here. If the order transitions out of ACTIVE/FIAT_SENT/DISPUTE (e.g., completed, expired, or admin-cancelled) in the window between those two operations, the flag will still be set and a non-null updateOrder returned, driving cooperative-cancel messaging on an order that is no longer in a cancellable state.

The previous review explicitly suggested adding status: { $in: ['ACTIVE', 'FIAT_SENT', 'DISPUTE'] } to the filter — that part of the suggestion was not adopted.

🔒 Proposed fix
   return await Order.findOneAndUpdate(
-    { _id: orderId, [propName]: { $ne: true } },
+    {
+      _id: orderId,
+      status: { $in: ['ACTIVE', 'FIAT_SENT', 'DISPUTE'] },
+      [propName]: { $ne: true },
+    },
     { $set: { [propName]: true } },
     { new: true },
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` around lines 35 - 39, The TOCTOU remains because
setCooperativeCancelFlag calls Order.findOneAndUpdate without constraining
status; change the filter used in that update (the call in
setCooperativeCancelFlag that currently uses { _id: orderId, [propName]: { $ne:
true } }) to also require status: { $in: ['ACTIVE','FIAT_SENT','DISPUTE'] } so
the atomic findOneAndUpdate only succeeds for cancellable orders; keep the
existing propName check and update payload but add the status $in clause to the
query to prevent setting the flag on non-cancellable orders.
🧹 Nitpick comments (2)
bot/commands.ts (2)

768-768: Pass updateOrder instead of stale order to initCooperativeCancelMessage.

The else branch uses updateOrder for the counterparty message but the stale order for the initiator message. For consistency, and to avoid passing an object whose state diverges from the DB after the atomic update, pass updateOrder here too.

♻️ Proposed fix
-      await messages.initCooperativeCancelMessage(ctx, order);
+      await messages.initCooperativeCancelMessage(ctx, updateOrder);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` at line 768, The code passes the stale order object to
messages.initCooperativeCancelMessage; instead use the updated object returned
from the atomic update. Replace the call await
messages.initCooperativeCancelMessage(ctx, order) with await
messages.initCooperativeCancelMessage(ctx, updateOrder) so both the counterparty
and initiator messages receive the same, current order state (symbols:
initCooperativeCancelMessage, updateOrder, order).

736-736: Use updateOrder.hash for consistency.

The cancelHoldInvoice call still references the stale order.hash. While the hash doesn't change during this flow, using the stale object is inconsistent with the rest of the completion path, which operates entirely on updateOrder.

♻️ Proposed fix
-      if (order.hash) await cancelHoldInvoice({ hash: order.hash });
+      if (updateOrder.hash) await cancelHoldInvoice({ hash: updateOrder.hash });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` at line 736, Replace the stale reference to order.hash with
updateOrder.hash in the cancelHoldInvoice call: locate the call to
cancelHoldInvoice (currently using order.hash) and change it to use
updateOrder.hash so the completion path consistently uses the updated order
object (ensure cancelHoldInvoice is still awaited and behavior unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bot/commands.ts`:
- Line 765: Remove the debug artifact: delete the logger.info call that logs the
literal "cancelOrder => OrderEvents.orderUpdated(updateOrder);" in the
cancelOrder flow, or replace it with a meaningful, non-redundant message (or a
logger.debug) that reflects actual state (e.g., order id or status) before
invoking OrderEvents.orderUpdated(updateOrder); ensure the change touches the
logger.info call referencing that exact string in the cancelOrder
implementation.
- Around line 34-38: Update setCooperativeCancelFlag: in the
Order.findOneAndUpdate call inside setCooperativeCancelFlag replace the plain
query { _id: orderId, [propName]: { $ne: true } } with one that also filters on
the cancellable status values used earlier (e.g., include status: { $in:
[...cancellableStatuses] }) to prevent the TOCTOU race; you can leave the update
as a plain object (Mongoose 8 will $set it). Also replace any usage of the stale
order object after the update with updateOrder (use updateOrder.hash instead of
order.hash), remove or clarify the noisy logger.info('cancelOrder =>
OrderEvents.orderUpdated(updateOrder);') debug line, and pass updateOrder (not
order) into initCooperativeCancelMessage(ctx, ...) to ensure you use the updated
document.

---

Duplicate comments:
In `@bot/commands.ts`:
- Around line 35-39: The TOCTOU remains because setCooperativeCancelFlag calls
Order.findOneAndUpdate without constraining status; change the filter used in
that update (the call in setCooperativeCancelFlag that currently uses { _id:
orderId, [propName]: { $ne: true } }) to also require status: { $in:
['ACTIVE','FIAT_SENT','DISPUTE'] } so the atomic findOneAndUpdate only succeeds
for cancellable orders; keep the existing propName check and update payload but
add the status $in clause to the query to prevent setting the flag on
non-cancellable orders.

---

Nitpick comments:
In `@bot/commands.ts`:
- Line 768: The code passes the stale order object to
messages.initCooperativeCancelMessage; instead use the updated object returned
from the atomic update. Replace the call await
messages.initCooperativeCancelMessage(ctx, order) with await
messages.initCooperativeCancelMessage(ctx, updateOrder) so both the counterparty
and initiator messages receive the same, current order state (symbols:
initCooperativeCancelMessage, updateOrder, order).
- Line 736: Replace the stale reference to order.hash with updateOrder.hash in
the cancelHoldInvoice call: locate the call to cancelHoldInvoice (currently
using order.hash) and change it to use updateOrder.hash so the completion path
consistently uses the updated order object (ensure cancelHoldInvoice is still
awaited and behavior unchanged).

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 579db9d and eaa985b.

📒 Files selected for processing (1)
  • bot/commands.ts

Copy link
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

♻️ Duplicate comments (4)
bot/commands.ts (4)

765-765: ⚠️ Potential issue | 🟡 Minor

Remove debug log artifact.

This logger.info echoes its own source line as a string literal and is a debug artifact that should not reach production.

-     logger.info('cancelOrder => OrderEvents.orderUpdated(updateOrder);');
      OrderEvents.orderUpdated(updateOrder);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` at line 765, Remove the stray debug log line that prints the
source line literal in the cancelOrder flow: delete the logger.info('cancelOrder
=> OrderEvents.orderUpdated(updateOrder);') call so production logs no longer
contain this artifact; ensure no other similar literal debug logs remain near
the cancelOrder handler and where OrderEvents.orderUpdated(updateOrder) is
invoked.

34-38: ⚠️ Potential issue | 🟠 Major

Missing status filter in setCooperativeCancelFlag leaves a TOCTOU gap.

The status guard at lines 694–701 validates the order is in a cancellable state, but setCooperativeCancelFlag only filters on _id and the flag itself — not on status. A concurrent operation that transitions the order to CANCELED, COMPLETED, EXPIRED, etc. between the read and this update will still succeed in setting the flag on a terminal-state order.

🔧 Proposed fix
  return await Order.findOneAndUpdate(
-   { _id: orderId, [propName]: { $ne: true } },
+   {
+     _id: orderId,
+     [propName]: { $ne: true },
+     status: { $in: ['ACTIVE', 'FIAT_SENT', 'DISPUTE'] },
+   },
    update,
    { new: true },
  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` around lines 34 - 38, The update call in
setCooperativeCancelFlag currently only filters by _id and the flag (propName)
which creates a TOCTOU race versus the earlier status check; modify the
Order.findOneAndUpdate invocation used in setCooperativeCancelFlag to also
include the cancellable status condition (the same status predicate used in the
prior validation) in the query filter so the atomic update only succeeds when
the order's status is still cancellable; keep the existing { new: true } option
and propName/update logic intact.

736-736: ⚠️ Potential issue | 🟡 Minor

Use updateOrder.hash instead of stale order.hash.

order is the pre-update snapshot; updateOrder is the fresh document returned from findOneAndUpdate. Using order.hash here is inconsistent and could act on stale data if a concurrent update changed the hash.

-     if (order.hash) await cancelHoldInvoice({ hash: order.hash });
+     if (updateOrder.hash) await cancelHoldInvoice({ hash: updateOrder.hash });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` at line 736, In the post-update cleanup use the fresh
document returned by findOneAndUpdate instead of the pre-update snapshot:
replace references to order.hash with updateOrder.hash when calling
cancelHoldInvoice (the call near "if (order.hash) await cancelHoldInvoice({
hash: order.hash });"), ensuring you check updateOrder exists and its hash
(e.g., if (updateOrder && updateOrder.hash) await cancelHoldInvoice({ hash:
updateOrder.hash });).

768-768: ⚠️ Potential issue | 🟡 Minor

initCooperativeCancelMessage receives the stale order object; pass updateOrder.

All other messaging calls in this function were updated to use updateOrder, but line 768 still passes the pre-update order snapshot.

-     await messages.initCooperativeCancelMessage(ctx, order);
+     await messages.initCooperativeCancelMessage(ctx, updateOrder);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/commands.ts` at line 768, The call to
messages.initCooperativeCancelMessage is using the stale order variable; change
it to pass the updated order snapshot (updateOrder) instead. Locate the
invocation of messages.initCooperativeCancelMessage(ctx, order) and replace the
second argument with updateOrder so it aligns with other messaging calls that
were already switched to the updated object; keep ctx as the first parameter and
ensure updateOrder is in scope where the change is made.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bot/commands.ts`:
- Around line 736-766: The cooperative-cancel branch mutates updateOrder.status
= 'CANCELED' (and never sets updateOrder.canceled_by) but does not persist these
changes; locate the cooperative cancellation flow around
setCooperativeCancelFlag and the code that calls
messages.successCancelOrderMessage / okCooperativeCancelMessage /
refundCooperativeCancelMessage and before OrderEvents.orderUpdated(updateOrder)
persist the final state to the DB (either call updateOrder.save() on the
returned document or run a second findOneAndUpdate that sets status='CANCELED'
and canceled_by to the initiator) so the DB matches the emitted update and
include the same canceled_by logic used in the PENDING path.

---

Duplicate comments:
In `@bot/commands.ts`:
- Line 765: Remove the stray debug log line that prints the source line literal
in the cancelOrder flow: delete the logger.info('cancelOrder =>
OrderEvents.orderUpdated(updateOrder);') call so production logs no longer
contain this artifact; ensure no other similar literal debug logs remain near
the cancelOrder handler and where OrderEvents.orderUpdated(updateOrder) is
invoked.
- Around line 34-38: The update call in setCooperativeCancelFlag currently only
filters by _id and the flag (propName) which creates a TOCTOU race versus the
earlier status check; modify the Order.findOneAndUpdate invocation used in
setCooperativeCancelFlag to also include the cancellable status condition (the
same status predicate used in the prior validation) in the query filter so the
atomic update only succeeds when the order's status is still cancellable; keep
the existing { new: true } option and propName/update logic intact.
- Line 736: In the post-update cleanup use the fresh document returned by
findOneAndUpdate instead of the pre-update snapshot: replace references to
order.hash with updateOrder.hash when calling cancelHoldInvoice (the call near
"if (order.hash) await cancelHoldInvoice({ hash: order.hash });"), ensuring you
check updateOrder exists and its hash (e.g., if (updateOrder &&
updateOrder.hash) await cancelHoldInvoice({ hash: updateOrder.hash });).
- Line 768: The call to messages.initCooperativeCancelMessage is using the stale
order variable; change it to pass the updated order snapshot (updateOrder)
instead. Locate the invocation of messages.initCooperativeCancelMessage(ctx,
order) and replace the second argument with updateOrder so it aligns with other
messaging calls that were already switched to the updated object; keep ctx as
the first parameter and ensure updateOrder is in scope where the change is made.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eaa985b and 38a60ec.

📒 Files selected for processing (1)
  • bot/commands.ts

…ta, recording the canceller, and persisting changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants