BugFix: users can cancel an order cooperatively at the same time#733
BugFix: users can cancel an order cooperatively at the same time#733Matobi98 wants to merge 14 commits intolnp2pBot:mainfrom
Conversation
…nction and update the cancelOrder command to utilize it.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughIntroduces Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorEmit
orderUpdatedwith the freshly saved order to avoid stale event payloads.
updateOrderis persisted withstatus = 'CANCELED', but the event is emitted with the pre-updateorder, so listeners may seeACTIVEand miss the cancellation. EmitupdateOrderinstead (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);
There was a problem hiding this comment.
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:
- Buyer calls
setCooperativeCancelFlag→ setsbuyer_cooperativecancel = true, gets back order withseller_cooperativecancel = false - Seller calls
setCooperativeCancelFlag→ setsseller_cooperativecancel = true, gets back order withbuyer_cooperativecancel = true - Seller's code sees both flags true → cancels the order ✅
- 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.
…d duplicate guard and error handling
There was a problem hiding this comment.
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 | 🔴 CriticalRemove unused
counterPartylocal to unblock lint.
counterPartyis 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.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
bot/commands.ts (1)
28-36:⚠️ Potential issue | 🟠 MajorMake cooperative-cancel duplicate protection atomic in the DB query.
The stale pre-check (
hasRequestedCancel) plus_id-only update is still TOCTOU. Concurrent/cancelrequests can both pass and produce inconsistent duplicate responses. Move the guard intofindOneAndUpdateand treatnullas 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) PYAlso 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).
…vide correct user feedback.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
bot/commands.ts (1)
35-39:⚠️ Potential issue | 🟠 MajorRemaining TOCTOU:
setCooperativeCancelFlagdoes not guard against invalid order statuses.The status check at lines 694–701 is a separate read from the
findOneAndUpdatehere. If the order transitions out ofACTIVE/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-nullupdateOrderreturned, 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: PassupdateOrderinstead of staleordertoinitCooperativeCancelMessage.The
elsebranch usesupdateOrderfor the counterparty message but the staleorderfor the initiator message. For consistency, and to avoid passing an object whose state diverges from the DB after the atomic update, passupdateOrderhere 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: UseupdateOrder.hashfor consistency.The
cancelHoldInvoicecall still references the staleorder.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 onupdateOrder.♻️ 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).
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
bot/commands.ts (4)
765-765:⚠️ Potential issue | 🟡 MinorRemove debug log artifact.
This
logger.infoechoes 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 | 🟠 MajorMissing
statusfilter insetCooperativeCancelFlagleaves a TOCTOU gap.The status guard at lines 694–701 validates the order is in a cancellable state, but
setCooperativeCancelFlagonly filters on_idand the flag itself — not onstatus. A concurrent operation that transitions the order toCANCELED,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 | 🟡 MinorUse
updateOrder.hashinstead of staleorder.hash.
orderis the pre-update snapshot;updateOrderis the fresh document returned fromfindOneAndUpdate. Usingorder.hashhere 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
initCooperativeCancelMessagereceives the staleorderobject; passupdateOrder.All other messaging calls in this function were updated to use
updateOrder, but line 768 still passes the pre-updateordersnapshot.- 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.
…ta, recording the canceller, and persisting changes.
… or dispute statuses to avoid race condition.
Description
Fix race condition when processing simultaneous cooperative cancellations: if buyer and seller send
/cancel order-idat nearly the same time both flags (buyer_cooperativecancel,seller_cooperativecancel) were set but the order remained inActivestatus.Now the operation uses an atomic update/lock so when both confirm the order transitions to
cancelledand bot replies remain consistent.Summary by CodeRabbit