Skip to content

Comments

Don't prevent detection when paying to self#625

Merged
chedieck merged 1 commit intoPayButton:masterfrom
Fabcien:self_payment
Feb 20, 2026
Merged

Don't prevent detection when paying to self#625
chedieck merged 1 commit intoPayButton:masterfrom
Fabcien:self_payment

Conversation

@Fabcien
Copy link
Collaborator

@Fabcien Fabcien commented Feb 20, 2026

The amount detection is actually an exact match, computing the transaction amount by summing the recipient output and subtracting the sender inputs to get the (out - in) difference.

When sending to self this computed value is zero so the amount is not matched (the tx is not even processed).

This also fixes the edge case of a tx sending more coins to the recipient, e.g. via an extra output.

Closes #301.

Note that the exact same issue exists on server side.

Test Plan:
yarn test

Try sending to yourself and check detection now works.

Summary by CodeRabbit

  • Bug Fixes
    • Improved transaction amount validation to accept amounts equal to or greater than expected amounts, rather than requiring exact matches.
    • Corrected transaction amount calculation to use total output values for more accurate transaction processing.

The amount detection is actually an exact match, computing the transaction amount by summing the recipient output and subtracting the sender inputs to get the (out - in) difference.

When sending to self this computed value is zero so the amount is not matched (the tx is not even processed).

This also fixes the edge case of a tx sending more coins to the recipient, e.g. via an extra output.

Test Plan:
yarn test

Try sending to yourself and check detection now works.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

The changes fix a payment validation issue where onSuccess callbacks weren't triggering for payments involving inputs and outputs from the same wallet. The amount calculation now uses total outputs instead of net (output minus input), and validation uses greater-than-or-equal comparison instead of strict equality.

Changes

Cohort / File(s) Summary
Amount Calculation & Validation
react/lib/util/chronik.ts, react/lib/util/validate.ts
Modified amount calculation to use total outputs instead of net output-input. Updated validation logic to check if received amount is greater-than-or-equal to expected amount instead of strictly equal.
Validation Tests
react/lib/tests/util/validate.test.ts
Refactored test case to verify true when received amount matches or exceeds expected amount, and added separate test to verify false when received amount is less than expected.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested labels

bug

Poem

🐰 A wallet pays itself, the quirk unfolds,
Where inputs and outputs both take hold,
Net gave false, but gross amounts ring true,
Greater-equal checks now see it through,
Payment fires where silence reigned before! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Don't prevent detection when paying to self' clearly and concisely summarizes the main change: fixing transaction detection for self-payments.
Description check ✅ Passed The description covers the problem (exact match prevention for self-payments), the solution (changed to less-than-or-equal comparison), references the linked issue (#301), and includes a test plan.
Linked Issues check ✅ Passed The PR addresses issue #301 by fixing onSuccess detection for self-payments through logic changes in validate.ts and chronik.ts that enable transactions with matching input/output addresses.
Out of Scope Changes check ✅ Passed All changes directly address the self-payment detection issue: modifying amount calculation logic, adjusting validation comparisons, and updating corresponding tests.
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 docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

@Klakurka Klakurka requested a review from chedieck February 20, 2026 15:34
@Klakurka Klakurka added the bug Something isn't working label Feb 20, 2026
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)
react/lib/util/chronik.ts (1)

116-138: ⚠️ Potential issue | 🟡 Minor

Transaction.amount may be inflated for self-payments that include change back to the same address.

The fix correctly reports totalOutput instead of the previous totalOutput − totalInput (which produced zero for self-payments). For the common self-payment scenario, however, an UTXO-spending transaction often sends change back to the same address:

  • Input: 1000 XEC from Alice
  • Output 1: 100 XEC to Alice ← intended payment
  • Output 2: 895 XEC to Alice ← change

With the new logic, totalOutput = 995 XEC is recorded in Transaction.amount, not 100 XEC. The isLessThanOrEqualTo comparison in validate.ts still triggers onSuccess correctly (100 ≤ 995), but the amount surfaced to merchant callbacks and the UI will be inflated. This is an inherent limitation of the totalOutput approach when change can't be distinguished from the intentional payment in a self-payment.

Consider documenting this behaviour (e.g., a code comment on getTransactionAmountAndData noting that for self-payments the amount reflects the sum of all outputs to the address, including change).

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

In `@react/lib/util/chronik.ts` around lines 116 - 138,
getTransactionAmountAndData currently sums all outputs to the given address
(totalOutput) and therefore will report an inflated amount for self-payments
that also return change to the same address; add a clear inline comment above
the getTransactionAmountAndData function explaining that Transaction.amount
reflects the sum of all outputs to the address (including change) in
self-payments, describe the example case (payment + change) briefly, and note
this limitation as intentional/current behavior and a future improvement area
(e.g., require input ownership data to separate change from payment).
🧹 Nitpick comments (1)
react/lib/tests/util/validate.test.ts (1)

148-174: result1 and result2 can be const.

Neither variable is reassigned after its initializer; let overstates mutability.

♻️ Proposed fix
-        let result1 = shouldTriggerOnSuccess(
+        const result1 = shouldTriggerOnSuccess(
           transaction,
           currency,
           price,
           false,
           false,
           expectedPaymentId,
           expectedAmount,
           expectedOpReturn
         );
 
         expect(result1).toBe(true);
 
         expectedAmount = resolveNumber('101.00');
 
-        let result2 = shouldTriggerOnSuccess(
+        const result2 = shouldTriggerOnSuccess(
           transaction,
           currency,
           price,
           false,
           false,
           expectedPaymentId,
           expectedAmount,
           expectedOpReturn
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react/lib/tests/util/validate.test.ts` around lines 148 - 174, The variables
result1 and result2 in the test block are declared with let but never
reassigned; change their declarations to const to reflect immutability where
shouldTriggerOnSuccess(...) is called for each assertion (keep expectedAmount as
let since it's reassigned before result2). Update the lines creating result1 and
result2 to use const to improve clarity and satisfy lint rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@react/lib/util/validate.ts`:
- Around line 37-42: In the block where randomSatoshis is truthy you replaced
exact matching with isLessThanOrEqualTo and also skip isPaymentIdValid,
weakening the per-session uniqueness guarantee; update validate.ts by adding a
concise comment next to the randomSatoshis / isLessThanOrEqualTo branch (and
near the isPaymentIdValid skip) that states this use of isLessThanOrEqualTo is
intentional, documents that uniqueness is preserved by the paymentId /
isPaymentIdValid logic (or by the TX_ADDED_TO_MEMPOOL event semantics) rather
than exact amount matching (isEqualTo), and notes the trade-off/risk of
coincidental higher-value transactions triggering onSuccess — or alternatively
re-enable isPaymentIdValid for randomSatoshis flows if you want strict
uniqueness.

---

Outside diff comments:
In `@react/lib/util/chronik.ts`:
- Around line 116-138: getTransactionAmountAndData currently sums all outputs to
the given address (totalOutput) and therefore will report an inflated amount for
self-payments that also return change to the same address; add a clear inline
comment above the getTransactionAmountAndData function explaining that
Transaction.amount reflects the sum of all outputs to the address (including
change) in self-payments, describe the example case (payment + change) briefly,
and note this limitation as intentional/current behavior and a future
improvement area (e.g., require input ownership data to separate change from
payment).

---

Nitpick comments:
In `@react/lib/tests/util/validate.test.ts`:
- Around line 148-174: The variables result1 and result2 in the test block are
declared with let but never reassigned; change their declarations to const to
reflect immutability where shouldTriggerOnSuccess(...) is called for each
assertion (keep expectedAmount as let since it's reassigned before result2).
Update the lines creating result1 and result2 to use const to improve clarity
and satisfy lint rules.

Comment on lines +37 to +42
isAmountValid = resolveNumber(value).isLessThanOrEqualTo(amount)
}else {
isAmountValid = false
}
} else {
isAmountValid = expectedAmount.isEqualTo(amount);
isAmountValid = expectedAmount.isLessThanOrEqualTo(amount);
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 | 🟡 Minor

isLessThanOrEqualTo weakens the uniqueness guarantee when randomSatoshis is truthy.

The comparison direction (expectedAmount ≤ receivedAmount) is correct and logically consistent with the PR goal. However, a subtle regression:

When randomSatoshis is truthy and non-zero, the isPaymentIdValid check is entirely skipped (lines 48–53). Previously, isEqualTo enforced an exact match on the salt-modified amount, which implicitly gave each payment session uniqueness. With isLessThanOrEqualTo, any concurrent transaction to the monitored address with amount >= expectedAmount would trigger onSuccess.

In practice the WebSocket handler only processes TX_ADDED_TO_MEMPOOL events (new transactions), so a replay of an older larger transaction is not possible at the protocol level. The risk is therefore a coincidental same-block unrelated transaction to the same address that exceeds the threshold — low probability, but the uniqueness guarantee that randomSatoshis was intended to provide is no longer enforced by the amount check alone.

Consider adding a brief comment explaining that isLessThanOrEqualTo is intentional and that uniqueness for randomSatoshis sessions is instead provided by … (paymentId, or document the trade-off).

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

In `@react/lib/util/validate.ts` around lines 37 - 42, In the block where
randomSatoshis is truthy you replaced exact matching with isLessThanOrEqualTo
and also skip isPaymentIdValid, weakening the per-session uniqueness guarantee;
update validate.ts by adding a concise comment next to the randomSatoshis /
isLessThanOrEqualTo branch (and near the isPaymentIdValid skip) that states this
use of isLessThanOrEqualTo is intentional, documents that uniqueness is
preserved by the paymentId / isPaymentIdValid logic (or by the
TX_ADDED_TO_MEMPOOL event semantics) rather than exact amount matching
(isEqualTo), and notes the trade-off/risk of coincidental higher-value
transactions triggering onSuccess — or alternatively re-enable isPaymentIdValid
for randomSatoshis flows if you want strict uniqueness.

@Klakurka Klakurka added this to the Phase 3 milestone Feb 20, 2026
@chedieck chedieck merged commit d49eb6c into PayButton:master Feb 20, 2026
3 checks passed
@Fabcien Fabcien deleted the self_payment branch February 22, 2026 09:15
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.

onSuccess doesn't fire when the payment has both an input and output w/ the same address (same wallet)

3 participants