Skip to content

Conversation

@ToRyVand
Copy link

@ToRyVand ToRyVand commented Dec 20, 2025

🐛 Problem

When a seller shows a hold invoice to a buyer, the inline keyboard buttons disappear, leaving the seller without options to mark the order as pending or cancel the operation. This creates a poor UX where sellers are stuck waiting for payment with no way to reset the order back to PENDING status.

Current Flow (Broken):

  1. Buyer takes a BUY order
  2. Seller receives notification and clicks "Continue"
  3. Bot shows hold invoice QR code
  4. BUG: Buttons disappear
  5. Seller cannot mark order as pending or cancel

✅ Solution

Added a "Mark as pending" button alongside the cancel button after the hold invoice is displayed.

New Flow:

  1. Buyer takes a BUY order
  2. Seller receives notification and clicks "Continue"
  3. Bot shows hold invoice QR code
  4. FIX: Bot shows buttons: [Mark as pending] [Cancel]
  5. Seller can mark order as pending, which:
    • Cancels the hold invoice
    • Resets order status to PENDING
    • Clears hash, secret, and taken_at fields
    • Republishes the order
    • Notifies the seller

🔧 Changes Made

Files Modified (5 total):

  1. locales/en.yaml - Added English translation

    mark_as_pending: Mark as pending
  2. locales/es.yaml - Added Spanish translation

    mark_as_pending: Marcar como pendiente
  3. bot/messages.ts - Modified showHoldInvoiceMessage()

    • Added optional orderId parameter
    • Added inline keyboard with two buttons after QR code display
    • Buttons: "Mark as pending" and "Cancel"
  4. bot/commands.ts - Created markAsPending() function

    • Validates order exists and is in WAITING_PAYMENT status
    • Cancels the hold invoice (with error handling for already expired invoices)
    • Resets order to PENDING status
    • Clears invoice-related fields (hash, secret, taken_at)
    • Sends confirmation message to seller
    • Deletes the message with buttons
  5. bot/start.ts - Registered action handler

    • Added handler for markPendingBtn_[orderId] callback
    • Uses regex to extract orderId: /^markPendingBtn_([0-9a-f]{24})$/
    • Calls markAsPending() with extracted orderId

📊 Stats

  • +98 additions, -2 deletions
  • 5 files changed
  • 1 commit (clean, focused commit)

🎯 Implementation Details

Pattern Followed

This implementation follows the same pattern as the existing cancelShowHoldInvoice function:

  • Similar validation flow
  • Same error handling approach
  • Consistent state management

Why orderId is Optional

Made orderId optional in showHoldInvoiceMessage() to maintain backward compatibility with existing code that calls this function without the parameter.

Error Handling

The cancelHoldInvoice() call is wrapped in try-catch because the invoice might already be expired or canceled. This is not critical—the important part is resetting the order state in the database.


✅ Testing

  • TypeScript compilation successful
  • Followed existing code patterns from the project
  • Validated against similar functionality (cancelShowHoldInvoice)

Note: Attempted local testing but encountered dependency issues with canvas (pixman-1). The code compiles successfully and follows established patterns in the codebase.


🤔 Questions for Reviewers

  1. Should I add validation to check if ctx.from.id === order.seller_id?
  2. Would you prefer a test case for this functionality?
  3. Is the error handling approach for cancelHoldInvoice() acceptable?

📚 Related


✨ Checklist

  • Code follows project style
  • Translations added (EN + ES)
  • Follows existing patterns
  • Single, focused commit
  • References issue number
  • Maintainer review pending

Summary by CodeRabbit

  • New Features

    • Users can mark orders as pending via a new interactive button in payment messages.
    • A new flow lets users revert a waiting-for-payment order back to pending and republishes the related order message.
  • Localization

    • Added English and Spanish translations for the new "Mark as pending" action.

✏️ Tip: You can customize this high-level summary in your review settings.

…ld invoice

- Added 'mark_as_pending' translation in en.yaml and es.yaml
- Modified showHoldInvoiceMessage to accept orderId and display buttons
- Created markAsPending function to handle marking order as pending
- Added markPendingBtn action handler in start.ts
- The button appears alongside cancel button after invoice is shown to seller
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 20, 2025

Walkthrough

Adds a "mark as pending" flow: new exported markAsPending command, a Telegram action handler, updates to hold-invoice message to include order-specific inline buttons, and new localization keys in English and Spanish. Changes target order state transitions from WAITING_PAYMENT to PENDING.

Changes

Cohort / File(s) Summary
Command logic & export
bot/commands.ts
Added and exported markAsPending(ctx, orderId): validates WAITING_PAYMENT and seller identity, cancels hold invoices, resets order to PENDING (clearing related fields), republishes buy/sell message, saves order, and sends success/error messages.
Bot action wiring
bot/start.ts
Imported markAsPending and added a Telegraf action handler matching markPendingBtn_<24-hex-chars> that invokes markAsPending(ctx, id) with userMiddleware.
Hold-invoice message UI
bot/messages.ts
Extended showHoldInvoiceMessage(...) signature with optional orderId?: string. When provided, sends an additional message with inline buttons (mark as pending, cancel) whose callback_data include the order ID.
Localization
locales/en.yaml, locales/es.yaml
Added mark_as_pending translation key (EN/ES). Restored check_solvers_last_warning in Spanish.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Review order-state transition and field-clearing logic in markAsPending() for correctness and edge cases.
  • Verify hold-invoice cancellation and error handling paths.
  • Confirm callback_data format and ctx.match parsing in the new action handler.
  • Ensure showHoldInvoiceMessage message flow and keyboard generation correctly include orderId and do not alter existing flows.

Possibly related PRs

Suggested reviewers

  • Catrya
  • grunch

Poem

🐰 A tiny hop, a gentle nudge,
Buttons ready, queues unclogged,
Hold invoices let go, not stray —
Orders bounce back into play.
✨🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses issue #556 about incorrect creator information in purchase orders; however, the implementation focuses on adding a 'Mark as pending' button without directly fixing the creator display bug described in the linked issue. Review whether the PR implementation actually resolves the creator display issue (#556) or if additional changes are needed to correct purchase order creator information in /checkorder output.
Out of Scope Changes check ⚠️ Warning The PR introduces changes to add 'Mark as pending' functionality (messages.ts, commands.ts, start.ts, locale files), which is only tangentially related to issue #556's core problem of incorrect creator display in /checkorder output. Clarify the relationship between the 'Mark as pending' feature and the creator display bug, or refocus the PR to directly address the stated objective of correcting creator information.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding a 'Mark as pending' button after showing a hold invoice, which aligns with the primary objective of the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 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: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 875ede4 and fb5202f.

📒 Files selected for processing (5)
  • bot/commands.ts (3 hunks)
  • bot/messages.ts (2 hunks)
  • bot/start.ts (2 hunks)
  • locales/en.yaml (1 hunks)
  • locales/es.yaml (1 hunks)
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to models/order*.ts : Enforce Order status lifecycle transitions: PENDING → ACTIVE → FIAT_SENT → COMPLETED; include DISPUTE, CANCELED_BY_ADMIN, EXPIRED, CANCELED
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to models/pendingpayment*.ts : Model PendingPayment as a queue for failed payment retries
📚 Learning: 2025-08-14T18:53:14.640Z
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to models/order*.ts : Enforce Order status lifecycle transitions: PENDING → ACTIVE → FIAT_SENT → COMPLETED; include DISPUTE, CANCELED_BY_ADMIN, EXPIRED, CANCELED

Applied to files:

  • bot/commands.ts
📚 Learning: 2025-08-14T18:53:14.640Z
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to jobs/**/*.ts : Implement background jobs for pending payments, order expiration/cleanup, community earnings, node health, and solver availability

Applied to files:

  • bot/commands.ts
📚 Learning: 2025-08-14T18:53:14.640Z
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to models/pendingpayment*.ts : Model PendingPayment as a queue for failed payment retries

Applied to files:

  • bot/commands.ts
📚 Learning: 2025-08-14T18:53:14.640Z
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to bot/start.ts : Implement bot initialization, command registration, and scheduled job setup in bot/start.ts

Applied to files:

  • bot/start.ts
📚 Learning: 2025-08-14T18:53:14.640Z
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to bot/**/*.ts : Use custom Telegraf context types (MainContext, CommunityContext) in handlers and modules

Applied to files:

  • bot/start.ts
📚 Learning: 2025-08-14T18:53:14.640Z
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to bot/modules/*/{commands,actions,messages,scenes,index}.ts : Structure each feature module with commands.ts, actions.ts, messages.ts, scenes.ts, and index.ts

Applied to files:

  • bot/start.ts
📚 Learning: 2025-08-14T18:53:14.640Z
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to bot/middleware/**/*.ts : Maintain middleware chain order: User validation → Admin checking → Context enhancement → Command routing

Applied to files:

  • bot/start.ts
🧬 Code graph analysis (3)
bot/commands.ts (6)
bot/modules/community/communityContext.ts (1)
  • CommunityContext (18-22)
models/index.ts (2)
  • Order (9-9)
  • User (9-9)
util/index.ts (1)
  • getUserI18nContext (615-615)
ln/hold_invoice.ts (1)
  • cancelHoldInvoice (67-67)
jobs/cancel_orders.ts (1)
  • bot (11-124)
bot/modules/orders/commands.ts (1)
  • ctx (113-183)
locales/es.yaml (1)
jobs/check_solvers.ts (1)
  • community (27-75)
bot/start.ts (4)
bot/middleware/user.ts (1)
  • userMiddleware (9-19)
bot/modules/community/communityContext.ts (1)
  • CommunityContext (18-22)
bot/commands.ts (1)
  • markAsPending (942-942)
bot/modules/dispute/index.ts (1)
  • bot (7-15)
🔇 Additional comments (8)
locales/en.yaml (1)

672-672: LGTM! Translation key added correctly.

The new mark_as_pending translation key is clear and concise, properly supporting the new "Mark as pending" button functionality.

locales/es.yaml (1)

669-669: LGTM! Spanish translation is correct.

The translation "Marcar como pendiente" accurately conveys the "Mark as pending" action in Spanish.

bot/commands.ts (2)

461-461: LGTM! Correct parameter addition.

Passing order._id to showHoldInvoiceMessage enables the new inline keyboard with "Mark as pending" and "Cancel" buttons.


942-942: Verify authorization before deploying.

Ensure the authorization check is added to markAsPending before this export is used in production, as the current implementation has a critical security vulnerability.

bot/start.ts (2)

43-43: Import added correctly.

The markAsPending import follows the existing pattern for other command functions.


851-860: Action handler correctly structured, but depends on fixing authorization in markAsPending.

The regex pattern /^markPendingBtn_([0-9a-f]{24})$/ properly validates MongoDB ObjectId format, and the handler correctly extracts the orderId. However, this handler will be unsafe until the authorization check is added to the markAsPending function (see bot/commands.ts review comment).

bot/messages.ts (2)

482-482: LGTM! Optional parameter maintains backward compatibility.

Making orderId optional ensures existing calls to showHoldInvoiceMessage continue to work without modification.


513-531: Reconsider non-null assertion on ctx.from.

Line 515 uses ctx.from!.id with a non-null assertion. While telegraf's Context type marks ctx.from as optional and the non-null assertion bypasses TypeScript's safety checks, callback queries typically guarantee ctx.from is defined. If you want defensive programming, add an explicit guard; otherwise, the assertion is acceptable for this handler context.

Comment on lines 586 to 644
const markAsPending = async (ctx: CommunityContext, orderId: string) => {
try {
ctx.deleteMessage();
ctx.scene.leave();

const order = await Order.findOne({ _id: orderId });
if (!order) return;

const user = await User.findOne({ _id: order.seller_id });
if (!user) return;

const i18nCtx = await getUserI18nContext(user);

// Only allow marking as pending for orders in WAITING_PAYMENT status
if (order.status !== 'WAITING_PAYMENT') {
return await messages.genericErrorMessage(ctx, user, i18nCtx);
}

// Cancel the hold invoice
if (order.hash) {
await cancelHoldInvoice({ hash: order.hash });
}

const buyerUser = await User.findOne({ _id: order.buyer_id });
if (!buyerUser) throw new Error('buyerUser was not found');

logger.info(
`Seller Id ${user.id} marked Order Id: ${order._id} as pending, republishing to the channel`,
);

// Reset order to PENDING status
order.taken_at = null;
order.status = 'PENDING';

if (!!order.min_amount && !!order.max_amount) {
order.fiat_amount = undefined;
}

if (order.price_from_api) {
order.amount = 0;
order.fee = 0;
order.hash = null;
order.secret = null;
}

if (order.type === 'buy') {
order.seller_id = null;
await messages.publishBuyOrderMessage(ctx, buyerUser, order, i18nCtx);
} else {
order.buyer_id = null;
await messages.publishSellOrderMessage(ctx, user, order, i18nCtx);
}

await order.save();
await messages.successCancelOrderMessage(ctx, user, order, i18nCtx);
} catch (error) {
logger.error(error);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

CRITICAL: Missing authorization check exposes security vulnerability.

The markAsPending function does not verify that the caller (ctx.from.id) is the seller of the order (order.seller_id). This allows any authenticated user to mark any other user's order as pending, potentially disrupting legitimate transactions.

Additionally, when the seller marks an order as pending, the buyer is not notified. This differs from cancelShowHoldInvoice (lines 519-528), which sends notifications to both parties and could leave the buyer confused about the order status change.

🔎 Recommended authorization check and buyer notification

Add authorization validation and buyer notification:

 const markAsPending = async (ctx: CommunityContext, orderId: string) => {
   try {
     ctx.deleteMessage();
     ctx.scene.leave();
 
     const order = await Order.findOne({ _id: orderId });
     if (!order) return;
 
     const user = await User.findOne({ _id: order.seller_id });
     if (!user) return;
 
     const i18nCtx = await getUserI18nContext(user);
 
+    // Verify that the caller is the seller
+    if (ctx.from && String(ctx.from.id) !== user.tg_id) {
+      return await messages.notAuthorized(ctx, user.tg_id);
+    }
+
     // Only allow marking as pending for orders in WAITING_PAYMENT status
     if (order.status !== 'WAITING_PAYMENT') {
       return await messages.genericErrorMessage(ctx, user, i18nCtx);
     }
 
     // Cancel the hold invoice
     if (order.hash) {
       await cancelHoldInvoice({ hash: order.hash });
     }
 
     const buyerUser = await User.findOne({ _id: order.buyer_id });
     if (!buyerUser) throw new Error('buyerUser was not found');
 
     logger.info(
       `Seller Id ${user.id} marked Order Id: ${order._id} as pending, republishing to the channel`,
     );
 
     // Reset order to PENDING status
     order.taken_at = null;
     order.status = 'PENDING';
 
     if (!!order.min_amount && !!order.max_amount) {
       order.fiat_amount = undefined;
     }
 
     if (order.price_from_api) {
       order.amount = 0;
       order.fee = 0;
       order.hash = null;
       order.secret = null;
     }
 
     if (order.type === 'buy') {
       order.seller_id = null;
       await messages.publishBuyOrderMessage(ctx, buyerUser, order, i18nCtx);
     } else {
       order.buyer_id = null;
       await messages.publishSellOrderMessage(ctx, user, order, i18nCtx);
     }
 
     await order.save();
     await messages.successCancelOrderMessage(ctx, user, order, i18nCtx);
+    // Notify the buyer that the order has been reset
+    const i18nCtxBuyer = await getUserI18nContext(buyerUser);
+    await messages.counterPartyCancelOrderMessage(ctx, buyerUser, order, i18nCtxBuyer);
   } catch (error) {
     logger.error(error);
   }
 };

Committable suggestion skipped: line range outside the PR's diff.

Verify that the user clicking the button is the seller of the order
before allowing them to mark it as pending. This prevents unauthorized
users from manipulating orders that don't belong to them.

Fixes security issue identified by CodeRabbit review.
@ToRyVand
Copy link
Author

Thanks @coderabbitai for catching the security issue! 🙏

I've added the authorization check in commit 67f4850. Now the markAsPending function verifies that the user clicking the button is actually the seller of the order before allowing the action.

The check compares order.seller_id with ctx.from.id and returns an error if they don't match.

Ready for another review! ✅

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 20, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the markAsPending function implementation from bot/commands.ts
cat bot/commands.ts | grep -A 50 "export const markAsPending"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the specific commit to see what was changed
git log --oneline -20 | head -20

Length of output: 890


🏁 Script executed:

#!/bin/bash
# Show the diff for the specific commit if it exists
git show 67f4850 --stat 2>/dev/null || echo "Commit not found in local history"

Length of output: 513


🏁 Script executed:

#!/bin/bash
# Read the entire markAsPending function to see the full implementation
rg -A 60 "export const markAsPending" bot/commands.ts

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for markAsPending function without assuming export const
rg -n "markAsPending" bot/commands.ts -A 3 -B 3

Length of output: 318


🏁 Script executed:

#!/bin/bash
# Show the full diff of the authorization commit
git show 67f4850

Length of output: 1128


🏁 Script executed:

#!/bin/bash
# Get a larger context around the function definition
rg -n "async.*markAsPending" bot/commands.ts -A 50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Just cat the entire commands.ts file to see the implementation
cat bot/commands.ts | tail -100

Length of output: 2443


@ToRyVand Excellent work! 🎉 The authorization check looks perfect!

The implementation is solid:

  • ✅ Correctly compares order.seller_id with ctx.from.id using .toString() to handle ObjectId vs number type differences
  • ✅ Uses optional chaining for safe property access
  • ✅ Positioned correctly—fails fast on authorization before any business logic
  • ✅ Returns a generic error message (good security practice)

The security issue is now properly addressed. The function will prevent unauthorized users from manipulating orders that don't belong to them.

The only remaining suggestion from my earlier review would be adding unit tests for this function, particularly testing:

  • Authorized seller can mark as pending ✓
  • Unauthorized user gets rejected ✓
  • Invalid order status handling ✓

But that's optional and can be done in a follow-up if you prefer. The core functionality and security are now in place! 👍


🧠 Learnings used
Learnt from: CR
Repo: lnp2pBot/bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-14T18:53:14.640Z
Learning: Applies to models/order*.ts : Enforce Order status lifecycle transitions: PENDING → ACTIVE → FIAT_SENT → COMPLETED; include DISPUTE, CANCELED_BY_ADMIN, EXPIRED, CANCELED

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Copy link
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

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

Hi @ToRyVand , thanks for your contribution!
However, I don't see how this PR can resolve issue #556 I don't see the connection. Could you explain it better please?

@ToRyVand
Copy link
Author

Hi @Catrya, you're absolutely right - I apologize for the confusion.

This PR does NOT fix issue #556 (the /checkorder creator bug).

While exploring the codebase, I found a different UX issue: after showing a hold invoice, the inline keyboard buttons disappear, leaving sellers unable to mark orders as pending when buyers don't pay.

This PR fixes that button issue instead.

Should I:

  1. Create a new issue for this problem and update the PR description?
  2. Close this PR and work on the actual Incorrect information about the creator of a purchase order is displayed #556?

@knocte
Copy link
Collaborator

knocte commented Dec 23, 2025

Create a new issue for this problem and update the PR description?

Yes

Close this PR

No

work on the actual 556 issue

Sounds good.

@Catrya
Copy link
Member

Catrya commented Dec 23, 2025

Hi @ToRyVand did you test the PR locally and verify that the bot works correctly?

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.

3 participants