Skip to content

handle slow payments to buyer#844

Open
Luquitasjeffrey wants to merge 1 commit into
mainfrom
handle_delayed_payment
Open

handle slow payments to buyer#844
Luquitasjeffrey wants to merge 1 commit into
mainfrom
handle_delayed_payment

Conversation

@Luquitasjeffrey

@Luquitasjeffrey Luquitasjeffrey commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened Lightning Network payment processing to prevent duplicate payments and improve recovery from service restarts.
    • Enhanced payment status detection to reliably identify confirmed payments and prevent redundant payment attempts.
    • Improved invoice update flows to detect pre-existing confirmed payments and skip unnecessary processing.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

New payment status helpers getPaymentStatus, isConfirmedPayment, and isPendingOrConfirmed are added to ln/pay_request.ts and re-exported via ln/index.ts. These replace the prior isPendingPayment-only checks across bot/scenes.ts, bot/modules/community/scenes.ts, and jobs/pending_payments.ts to prevent duplicate payments when a payment is already confirmed after a restart.

Changes

Payment Status Hardening and Double-Pay Prevention

Layer / File(s) Summary
New payment status helpers
ln/pay_request.ts, ln/index.ts
getPaymentStatus centralizes getPayment querying and returns a {is_confirmed, is_pending, payment} struct. isConfirmedPayment and isPendingOrConfirmed are derived from it. isPendingPayment is reimplemented on top of getPaymentStatus. payToBuyer is updated to call isPendingOrConfirmed. All new helpers are re-exported from ln/index.ts.
Scene invoice update guards
bot/scenes.ts, bot/modules/community/scenes.ts
addInvoicePHIWizard gains a confirmed-payment heal path that sets order.status to SUCCESS before rejecting the invoice update request; the pending-only gate is replaced with isPendingOrConfirmed. The community earnings-withdrawal wizard similarly switches its gate from isPendingPayment to isPendingOrConfirmed.
Pending payments job hardening
jobs/pending_payments.ts
attemptPendingPayments replaces isPendingPayment flight checks with getPaymentStatus-based guards: skips if buyer invoice is pending, heals to SUCCESS if already confirmed, scans other unexpired pending rows for the same order before incrementing attempts. sellerUser.save() is now awaited. attemptCommunitiesPendingPayments uses getPaymentStatus for early confirmed/pending routing before invoking payRequest.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • lnp2pBot/bot#667: Modifies the same attemptPendingPayments and attemptCommunitiesPendingPayments functions in jobs/pending_payments.ts to change retry scheduling, backoff, and error handling for pending payment flows.
  • lnp2pBot/bot#831: Modifies ln/pay_request.ts (payToBuyer) and jobs/pending_payments.ts (attemptPendingPayments) to adjust when pending payments are skipped or marked paid, directly overlapping with the same code paths changed here.
  • lnp2pBot/bot#835: Modifies community earnings withdrawal handling in addEarningsInvoiceWizard and attemptCommunitiesPendingPayments, adjusting guards and status handling to prevent incorrect duplicate payout behavior in the same functions.

Suggested reviewers

  • grunch
  • mostronatorcoder

Poem

🐇 Hop, hop, no double-pay today!
The rabbit checked if coins had flown away,
getPaymentStatus sniffs the confirmed trail,
isPendingOrConfirmed won't let healing fail,
Each order healed, each invoice sealed with care —
No sats sent twice! The ledger's cleared and fair. 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'handle slow payments to buyer' accurately reflects the main objective of the PR, which adds defense-in-depth logic to prevent double-paying and handle already-confirmed or pending payments across multiple modules.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch handle_delayed_payment

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: one or more packages not found in the registry.


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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@jobs/pending_payments.ts`:
- Around line 86-131: The condition at the start of the confirmed payment block
uses AND logic which means if is_confirmed is true but payment is falsy, the
entire block is skipped and execution falls through to re-payment logic.
Instead, change the primary skip condition to check only is_confirmed, then nest
the notification messages (toAdminChannelPendingPaymentSuccessMessage,
toBuyerPendingPaymentSuccessMessage, and rateUserMessage) inside a separate
check for when payment is present. This prevents re-payment of confirmed
invoices when payment data is missing. Apply the same fix pattern to the
attemptCommunitiesPendingPayments function which has the identical
vulnerability.

In `@ln/pay_request.ts`:
- Around line 245-256: The catch block in getPaymentStatus is returning a
safe-looking response for all errors, including unexpected ones like transient
connectivity issues, which can mask real failures and cause callers to unsafely
proceed with payment. Modify the error handling to only return the
is_confirmed/is_pending false response for legitimate "not found" cases (errors
containing SentPaymentNotFound, PaymentNotFound, or 404), and rethrow all other
unrecognized errors so that callers like payToBuyer and attemptPendingPayments
can properly abort before attempting payment instead of treating an unknown
error state as safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5e68f4c3-ad4c-43c0-bf7e-ce206d4b7095

📥 Commits

Reviewing files that changed from the base of the PR and between fdd5b2b and 34178f8.

📒 Files selected for processing (5)
  • bot/modules/community/scenes.ts
  • bot/scenes.ts
  • jobs/pending_payments.ts
  • ln/index.ts
  • ln/pay_request.ts

Comment thread jobs/pending_payments.ts
Comment on lines +86 to +131
const currentStatus = await getPaymentStatus(pending.payment_request);

// If already confirmed, process the SUCCESS routine immediately!
if (currentStatus.is_confirmed && currentStatus.payment) {
const payment = currentStatus.payment;
order.status = 'SUCCESS';
order.routing_fee = payment.fee;
pending.paid = true;
pending.paid_at = new Date();
const buyerUser = await User.findOne({ _id: order.buyer_id });
if (buyerUser === null) throw Error('buyerUser was not found in DB');
const i18nCtx: I18nContext = await getUserI18nContext(buyerUser);
buyerUser.trades_completed++;
await buyerUser.save();
const sellerUser = await User.findOne({ _id: order.seller_id });
if (sellerUser === null) throw Error('sellerUser was not found in DB');
sellerUser.trades_completed++;
await sellerUser.save();
logger.info(
`Invoice with hash: ${pending.hash} already paid, processing SUCCESS routine`,
);
await messages.toAdminChannelPendingPaymentSuccessMessage(
bot,
buyerUser,
order,
pending,
payment,
i18nCtx,
);
await messages.toBuyerPendingPaymentSuccessMessage(
bot,
buyerUser,
order,
payment,
i18nCtx,
);
await messages.rateUserMessage(bot, buyerUser, order, i18nCtx);
continue;
}

if (currentStatus.is_pending) {
logger.info(
`Order ${order._id}: current payment is already in-flight, skipping retry without incrementing attempts`,
);
continue;
}

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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

is_confirmed && payment can fall through to a re-pay.

If getPaymentStatus ever returns is_confirmed: true with a falsy payment, the block at Line 89 is skipped, the is_pending block at Line 126 is also false, and execution continues to payRequest — re-paying an already-confirmed invoice. Base the skip decision on is_confirmed alone, and only run the notification routine when payment is present. The same pattern exists in attemptCommunitiesPendingPayments at Line 285.

🛡️ Suggested guard
-      if (currentStatus.is_confirmed && currentStatus.payment) {
-        const payment = currentStatus.payment;
+      if (currentStatus.is_confirmed) {
         order.status = 'SUCCESS';
-        order.routing_fee = payment.fee;
         pending.paid = true;
         pending.paid_at = new Date();
-        const buyerUser = await User.findOne({ _id: order.buyer_id });
+        const payment = currentStatus.payment;
+        if (!payment) {
+          logger.warning(
+            `Order ${order._id}: confirmed without payment details, marking SUCCESS and skipping retry`,
+          );
+          continue;
+        }
+        order.routing_fee = payment.fee;
+        const buyerUser = await User.findOne({ _id: order.buyer_id });
🤖 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 `@jobs/pending_payments.ts` around lines 86 - 131, The condition at the start
of the confirmed payment block uses AND logic which means if is_confirmed is
true but payment is falsy, the entire block is skipped and execution falls
through to re-payment logic. Instead, change the primary skip condition to check
only is_confirmed, then nest the notification messages
(toAdminChannelPendingPaymentSuccessMessage,
toBuyerPendingPaymentSuccessMessage, and rateUserMessage) inside a separate
check for when payment is present. This prevents re-payment of confirmed
invoices when payment data is missing. Apply the same fix pattern to the
attemptCommunitiesPendingPayments function which has the identical
vulnerability.

Comment thread ln/pay_request.ts
Comment on lines 245 to 256
} catch (error: any) {
const message = error.toString();
logger.error(`isPendingPayment catch error: ${message}`);
return false;
const errStr = error.toString();
if (
errStr.includes('SentPaymentNotFound') ||
errStr.includes('PaymentNotFound') ||
errStr.includes('404')
) {
return { is_confirmed: false, is_pending: false, payment: undefined };
}
logger.error(`getPaymentStatus error: ${errStr}`);
return { is_confirmed: false, is_pending: false, payment: undefined };
}

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.

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🌐 Web query:

ln-service getPayment error codes for payment not found versus connectivity errors

💡 Result:

ln-service getPayment: “payment not found” vs connectivity errors 1) Payment not found (i.e., the payment hash exists in your request to getPayment, but lnd can’t return it) - For ln-service’s getPayment, the library returns payment lookup results in a structured shape (success/failed/pending) rather than throwing a “connectivity” error. The API doc describes getPayment as a past-payment lookup returning fields like failed/is_pending/is_confirmed and a payment object when found. [1] - In the underlying lnd/router tracking model, “no route can be found” is represented as a payment failure reason no-route (FailureReasonNoRoute = 2). [2] - Note: lnd’s payment failure reasons like TIMEOUT, NO_ROUTE, etc. are not “connectivity errors”; they are in-band payment outcome/failure reasons. [2] Practical mapping you can use: - If getPayment returns a response that indicates a failed payment with a failure reason such as NO_ROUTE or TIMEOUT, treat it as “payment exists but failed / can’t find a route”. [2] - If lnd returns a gRPC NotFound-style response for the lookup itself, treat it as “payment not found” (not connectivity). (lnd has been changing/standardizing NotFound handling for lookups in general; see that LookupInvoice was fixed to return codes.NotFound for missing entities.) [3] 2) Connectivity errors (i.e., you cannot reach lnd / gRPC channel fails) - ln-service documents debugging by enabling gRPC verbosity/tracing via GRPC_VERBOSITY and GRPC_TRACE to inspect the underlying gRPC calls; this is the usual way you identify connectivity/transport problems vs in-band payment results. [4] Practical mapping you can use: - If you see gRPC transport/channel errors (e.g., inability to connect, inactive RPC) rather than a payment/failure result payload, classify it as connectivity. This is distinct from payment failure reasons like NO_ROUTE. [4][5] Gaps / what I could not conclusively map - Your request asks for “ln-service getPayment error codes … for payment not found versus connectivity errors”. The official ln-service getPayment documentation snippet available from the sources I retrieved does not enumerate explicit numeric error codes for “payment not found” vs “connectivity”. It only shows the success/failure/pending return structure and general debugging guidance. [1][4] - The lnrpc PaymentFailureReason enum provides failure reasons for payment outcomes (e.g., NO_ROUTE) but not a dedicated “payment not found” code. [2] If you want, share the exact error object you’re getting from ln-service getPayment (stack + error fields, or the gRPC code/message). I can then tell you which category it is (payment-not-found vs transport) and what specific code/message to branch on.

Citations:


🏁 Script executed:

# Find and examine the pay_request.ts file
fd -t f "pay_request.ts" | head -5

Repository: lnp2pBot/bot

Length of output: 168


🏁 Script executed:

# Get the file structure and size
wc -l ln/pay_request.ts 2>/dev/null || find . -name "pay_request.ts" -exec wc -l {} \;

Repository: lnp2pBot/bot

Length of output: 172


🏁 Script executed:

# Use ast-grep to map the getPaymentStatus function
ast-grep outline ln/pay_request.ts --match getPaymentStatus 2>/dev/null || ast-grep outline . --match getPaymentStatus 2>/dev/null | head -20

Repository: lnp2pBot/bot

Length of output: 245


🏁 Script executed:

# Read the getPaymentStatus function and surrounding code (lines 230-270)
sed -n '230,270p' ln/pay_request.ts

Repository: lnp2pBot/bot

Length of output: 1414


🏁 Script executed:

# Search for where getPayment is called and defined
rg "getPayment" ln/pay_request.ts -B2 -A5 | head -50

Repository: lnp2pBot/bot

Length of output: 1489


🏁 Script executed:

# Look for the payToBuyer function to see how it handles getPaymentStatus errors
rg "payToBuyer" ln/pay_request.ts -B3 -A10 | head -60

Repository: lnp2pBot/bot

Length of output: 1483


🏁 Script executed:

# Check lightning library imports and see if there's documentation or other usage
rg "from 'lightning'" -A5 | head -30

Repository: lnp2pBot/bot

Length of output: 150


🏁 Script executed:

# Search for other error handling patterns with getPayment or similar ln-service calls
rg "getPayment|payViaPaymentRequest" --type ts -B2 -A5 | head -80

Repository: lnp2pBot/bot

Length of output: 150


🏁 Script executed:

# Check if there are any tests or examples showing getPayment error behavior
fd -t f "\.test\.ts|\.spec\.ts|test|spec" -x grep -l "getPayment" {} \; 2>/dev/null | head -5

Repository: lnp2pBot/bot

Length of output: 150


🏁 Script executed:

# Look for package.json to see the lightning library version
cat package.json | grep -A2 -B2 "lightning"

Repository: lnp2pBot/bot

Length of output: 513


🌐 Web query:

lightning npm library getPayment function error handling throws exceptions v10

💡 Result:

I wasn’t able to find a definitive, official “lightning v10 npm getPayment” error-handling section (or an official changelog entry) that explicitly documents which errors are thrown by a specific “getPayment” function in a “lightning” npm package version 10. The closest authoritative material I found concerns the underlying “ln-service” library’s getPayment API and general error-handling guidance for Lightning/WebLN usage. What I can say with sourced support: 1) The commonly referenced getPayment in this ecosystem is provided by ln-service (Alex Bosworth). Its docs describe getPayment as “Lookup details about a past payment” but the snippet I retrieved does not include documented throw/error semantics for getPayment itself. [1] 2) In Lightning/WebLN-style APIs, errors during an API call are delivered by throwing an exception, and the recommended approach is to wrap calls in try/catch. [2] 3) In a separate Lightning API context (LND router payment tracking), failure reasons are represented as enumerated fields (e.g., TIMEOUT, NO_ROUTE, INCORRECT_PAYMENT_DETAILS, etc.), which suggests the preferred pattern is to treat payment failures as returned status/failure fields rather than (or in addition to) exceptions. [3] 4) A concrete example from a codebase shows an implementation pattern: call getPayment inside try/catch, and treat a specific “payment not found” case as non-fatal (return null), otherwise rethrow. [4] Practical guidance to handle “getPayment” exceptions reliably (based on the sourced patterns above): - Always use try/catch around await getPayment(...), because some Lightning API surfaces throw exceptions on failure conditions. [2] - If your runtime indicates “not found” as an exception (example: mapping a 404 + specific error code to null), translate that to a non-exceptional result for your caller; otherwise rethrow or convert to a domain error. [4] - Prefer inspecting returned payment status/failure reason fields (where your library exposes them) for terminal payment outcomes instead of assuming every payment failure is thrown as an exception. [3] If you share the exact npm package name (is it “lightning” by lightning/?? vs “ln-service”?) and the exact import path plus the code that calls getPayment (and the observed error message/stack), I can do a second targeted search to identify the precise documented throw behavior for that specific v10 implementation.

Citations:


🏁 Script executed:

# Check if there are any comments or inline documentation in the code about error handling
rg "getPayment|connectivity|transient" ln/pay_request.ts -B3 -A3

Repository: lnp2pBot/bot

Length of output: 1444


🏁 Script executed:

# Look for other try/catch patterns in the codebase to understand error handling philosophy
rg "try \{" ln/ -A15 | head -100

Repository: lnp2pBot/bot

Length of output: 5642


Replace fail-open error handling with rethrow on unrecognized errors to prevent false-safe interpretations.

The catch handler at lines 254–255 returns {is_confirmed: false, is_pending: false} for all errors: both legitimate "not found" cases (correct for fresh invoices) and any unexpected errors (e.g. transient connectivity issues). This fail-open behavior is unsafe because every caller (payToBuyer, attemptPendingPayments, etc.) interprets all-false as "safe to proceed to payment"—even if getPayment threw due to a transient LND outage while a payment was actually in-flight or confirmed. This defeats the guard's purpose.

Adopt fail-closed behavior: rethrow unrecognized errors so existing try/catch blocks in callers abort before attempting payment, rather than treating an unknown state as safe.

Suggested fail-closed handling
  } catch (error: any) {
    const errStr = error.toString();
    if (
      errStr.includes('SentPaymentNotFound') ||
      errStr.includes('PaymentNotFound') ||
      errStr.includes('404')
    ) {
      return { is_confirmed: false, is_pending: false, payment: undefined };
    }
    logger.error(`getPaymentStatus error: ${errStr}`);
-   return { is_confirmed: false, is_pending: false, payment: undefined };
+   // Unexpected error: fail closed so callers do not treat an unknown state
+   // as "safe to pay" and re-attempt an in-flight/confirmed payment.
+   throw error;
  }
🤖 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 `@ln/pay_request.ts` around lines 245 - 256, The catch block in
getPaymentStatus is returning a safe-looking response for all errors, including
unexpected ones like transient connectivity issues, which can mask real failures
and cause callers to unsafely proceed with payment. Modify the error handling to
only return the is_confirmed/is_pending false response for legitimate "not
found" cases (errors containing SentPaymentNotFound, PaymentNotFound, or 404),
and rethrow all other unrecognized errors so that callers like payToBuyer and
attemptPendingPayments can properly abort before attempting payment instead of
treating an unknown error state as safe.

@knocte

knocte commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

What does is_confirmed mean? are we introducing on-chain (non-lightning) payments? If the answer is no, the word "confirmed" might be measleading (i.e. "confirmations").

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Strict review

Direction is correct and it fixes two real bugs along the way:

  • Good fix: the old attemptCommunitiesPendingPayments did if (isPending) return; — a return that aborted the whole community payout batch, not just that item. Now it's continue.
  • Good fix: attempts++ is no longer incremented on every poll while a payment is in-flight; it's incremented only right before an actual pay attempt.
  • sellerUser.save()await sellerUser.save().

I don't see a critical fund-loss bug: for the same payment_request, LND's payment-hash idempotency backstops the check (a second payViaPaymentRequest on an in-flight/settled hash fails rather than double-paying). Build (tsc --noEmit), eslint, and the test suite (133 passing) are all green locally. That said, a few things should be addressed before merge — details inline.

Summary

  • 🟠 The "healing" branches mark the order SUCCESS but skip buyer notification, trades_completed, rating and routing_fee, unlike the full success routine — inconsistent completed-order state.
  • 🟡 getPaymentStatus fails open on unknown/transient LND errors (returns all-false → caller proceeds to pay). For a double-pay guard this should fail closed (treat unknown error as pending/skip).
  • 🟡 Not-found detection via error.toString().includes('404' | 'SentPaymentNotFound') is fragile; inspect the structured error instead.
  • 🟡 payment?: any drops SDK typing; the success routine is now triplicated and the heal branches are already diverging from it (see first point).
  • 🟡 N+1 LND RPCs per job run; previousPendingPayments query isn't filtered.
  • 🟢 No tests for the new, fund-sensitive logic.

Verdict: request changes. The must-haves before merge are (1) unify notification/reputation in the heal branches and (2) make getPaymentStatus fail-closed on unknown errors. The rest are recommended but non-blocking.

Comment thread ln/pay_request.ts
return { is_confirmed: false, is_pending: false, payment: undefined };
}
logger.error(`getPaymentStatus error: ${errStr}`);
return { is_confirmed: false, is_pending: false, payment: undefined };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fail-open on unknown errors. Any error that isn't *NotFound/404 (e.g. 503, gRPC down, RPC timeout) returns {is_confirmed:false, is_pending:false}, so the caller proceeds to pay. Since the whole point of this PR is preventing a double pay after restart, this should fail closed: on an unknown error, treat the payment as pending (skip and retry next run) rather than free-to-pay. The same-hash case is covered by LND idempotency, but the /setinvoice new-invoice vector relies on isConfirmedPayment(order.buyer_invoice) here, and a transient failure exactly during a restart+settle attack would open it.

Comment thread ln/pay_request.ts
if (
errStr.includes('SentPaymentNotFound') ||
errStr.includes('PaymentNotFound') ||
errStr.includes('404')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fragile error matching. The pay/skip gate depends on string-matching error.toString(). The lightning lib returns errors as arrays like [404, 'SentPaymentNotFound', {...}]; this works today only because toString() flattens them, and includes('404') can also false-positive on other codes containing 404. Prefer inspecting the structured error (error[0] === 404 / error[1]).

Comment thread ln/pay_request.ts
interface PaymentStatus {
is_confirmed: boolean;
is_pending: boolean;
payment?: any;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

payment?: any drops the SDK's type for the payment object (the fields fee, id, secret are read downstream). Consider typing it from getPayment's return. Also: the success routine (set status, trades_completed++, admin/buyer messages, rate) is now triplicated (here-driven branches in the job + community). Extracting a helper would stop these copies from diverging — which they already are (see the heal branches).

Comment thread jobs/pending_payments.ts
logger.info(
`Order ${order._id}: original buyer invoice already confirmed, marking SUCCESS and skipping retry`,
);
continue;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Healing branch doesn't notify or credit. When order.buyer_invoice is already confirmed the buyer did receive the sats, but here we only set status='SUCCESS' + pending.paid=true — no buyer message, no trades_completed++ for either party, no routing_fee, no rateUserMessage. Same for the previousPendingPayments-confirmed branch below and the /setinvoice heal in bot/scenes.ts. Only the currentStatus.is_confirmed branch runs the full routine. Result: "phantom" completed orders with no reputation/rating and an uninformed buyer. Run the same success routine when healing by confirmation.

Comment thread jobs/pending_payments.ts
_id: { $ne: pending._id },
order_id: order._id,
is_invoice_expired: false,
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

N+1 LND RPCs per job run: this now does getPaymentStatus(order.buyer_invoice) + one per previousPendingPayments + one for the current request, for every pending, every 5 min. Acceptable at current scale but worth noting. This query also doesn't filter paid:false/attempts, so it scans more rows than needed.

Comment thread bot/scenes.ts
if (alreadyPaid) {
order.status = 'SUCCESS';
await order.save();
return await messages.invoiceAlreadyUpdatedMessage(ctx);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same consistency gap as the job: heals to SUCCESS but the buyer (who is here precisely because they think they weren't paid) gets invoiceAlreadyUpdatedMessage, which is confusing. If the original invoice is confirmed, tell them they were already paid and credit the success routine.

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