Skip to content

fix: block paying own lightning invoice - #1292

Open
jvsena42 wants to merge 13 commits into
masterfrom
fix/901-block-own-invoice
Open

jvsena42 wants to merge 13 commits into
masterfrom
fix/901-block-own-invoice

Conversation

@jvsena42

@jvsena42 jvsena42 commented Sep 16, 2026

Copy link
Copy Markdown
Member

Fixes #901
Related to #1275

This PR blocks paying a Lightning invoice created by the wallet's own node, showing a dedicated error instead of the misleading "Insufficient Spending" toast.

Description

  • Adds a payee check when a Lightning invoice is scanned, deep linked or pasted: if the invoice's payee node ID matches the local node ID, a "Cannot Pay Own Invoice" error toast shows and no Send sheet, QuickPay or Confirm screen opens, because LDK cannot route a payment to itself.
  • Runs the check before QuickPay, the usable-channels wait and the spending balance check, so QuickPay can never try to pay the wallet's own invoice.
  • Waits up to 15s for the node ID while the node is still starting, so the guard also works right after launch.
  • Remembers the node ID once the node has run, so the guard still fires when the node is later stopped, failing to restart or slow to start; the remembered ID is cleared only on storage wipe. The ID derives from the mnemonic and passphrase alone, and a wallet wipe is the only path to another mnemonic, so it always belongs to the active wallet. When the node has never run in this process the ID is unknown and the payment is not blocked.
  • Skips an own Lightning invoice inside a unified bitcoin: URI, so the send falls back to on-chain when there are enough savings.
  • Shows the self-payment error instead of "Insufficient Savings" when a unified URI carries an own invoice and savings are zero, because the savings error hides the real blocker.
  • Adds English strings for the new error and a changelog fragment.

Out of Scope

  • AppViewModel unified URI with own invoice: this PR falls back to an on-chain send when savings exist, which differs from verification step 4 of [Bug]: Self-payment on Lightning shows misleading "Insufficient Spending" error #901 (toast for unified URIs). The Lightning part cannot be paid, while the on-chain part is a valid self-send.
  • AppViewModel unified URI with own invoice and savings above zero but below the amount: still shows "Insufficient Savings" instead of the self-payment error. Only the zero-savings case is covered; follow-up.
  • AppViewModel.estimateLightningRoutingFeesIfNeeded / estimateMaxAmountRoutingFee: step 4 of [Bug]: Self-payment on Lightning shows misleading "Insufficient Spending" error #901 (mapping LDK's "Cannot generate a route to ourselves" to the self-payment toast) is not implemented. The payee check before these calls covers the case when the node ID is available.
  • LightningRepo.awaitNodeId: in a process where the node has never run, no node ID was ever observed, so the guard is skipped and the existing LDK route error behaviour remains. Persisting the ID across process restarts needs per-wallet scoping in the cache store and is a follow-up. While the node is Starting and the ID is not yet known, a scan can wait up to 15s with no extra feedback.
  • Localization: only English strings are added; translations follow the usual flow.
  • iOS parity: bitkit-ios was not checked; a matching own-invoice guard there is a follow-up.

Design

N/A — no design available.

The toast has no design. The on-chain fallback for a unified URI uses the standard Amount and Confirm screens; the resulting self-send activity is designed in Wallet Self Send with the note Self Send. Nothing was broadcast during testing, so those activity rows were not compared.

Preview

Recordings and screenshots from the regtest emulator on this branch (commit 2b2a3a6).

Own BOLT11 deep link, QuickPay off: toast shows and the app stays on Home.

own-bolt11-toast.mp4

Own BOLT11 deep link, QuickPay on: toast only, QuickPay does not open.

own-bolt11-quickpay-on-toast.mp4

Own unified URI deep link: falls back to the on-chain Amount and Confirm screens.

own-unified-onchain.mp4

Regression, foreign invoice with QuickPay on: QuickPay still runs; the payment then fails on purpose because the invoice has a fake route hint (no funds moved).

regression-foreign-quickpay.mp4
Own BOLT11, QuickPay off Own BOLT11, QuickPay on
Own BOLT11 toast Own BOLT11 QuickPay on toast
Own unified URI, Amount Own unified URI, Confirm details
Own unified onchain amount Own unified confirm details
Regression: foreign invoice QuickPay Regression: expected route failure
Foreign QuickPay Foreign payment failed
Figma: Wallet Self Send Figma: Self Send note
Figma Wallet Self Send Figma Self Send note

QA Notes

Manual Tests

Create the own invoice from Receive → Edit → 1000 sats → QR Code, and hand it to the app as a deep link (adb shell am start -a android.intent.action.VIEW -d "lightning:<invoice>" to.bitkit.dev) or by scanning.

  • 1a. QuickPay off → scan/deep link own LN invoice: "Cannot Pay Own Invoice" toast, no Send sheet or Confirm opens.
    • 1b. QuickPay on → scan/deep link own LN invoice: same toast, QuickPay does not open.
    • 1c. Send → Paste own LN invoice: same toast, no Amount or Confirm.
  • 2a. Savings > amount → scan/deep link own unified QR: Send Amount opens with source Savings → Continue → Send Confirm shows own on-chain address.
    • 2b. Savings = 0 → scan/paste own unified QR: "Cannot Pay Own Invoice" toast, no "Insufficient Savings".
  • 3. Cold launch → deep link own LN invoice while node is starting: toast shows once the node is running, QuickPay/Confirm never opens.
  • 4a. regression: Scan foreign LN invoice with QuickPay on: QuickPay pays it.
    • 4b. regression: Scan foreign LN invoice with QuickPay off: Send Confirm opens and payment succeeds.
  • 5. regression: Scan foreign unified QR with savings = 0: "Insufficient Savings" toast still shows when LN cannot be used.

Journey covering tests 1a, 1b and 2a, committed at journeys/send/own-invoice-guard.xml (row added to the suites and cross-platform tables in journeys/README.md). Inline for review:

own-invoice-guard.xml
diff --git a/journeys/send/own-invoice-guard.xml b/journeys/send/own-invoice-guard.xml
new file mode 100644
index 000000000..d429b74ce
--- /dev/null
+++ b/journeys/send/own-invoice-guard.xml
@@ -0,0 +1,33 @@
+<journey name="own invoice guard">
+  <description>
+    Paying a Lightning invoice created by this wallet is rejected with a "Cannot Pay Own Invoice"
+    toast before QuickPay or the Confirm screen, and a unified QR carrying an own invoice falls back
+    to an on-chain send. Precondition: onboarded dev wallet, node running, a usable spending channel
+    and a savings balance. The toast never reaches `android layout`; assert it from a screenshot
+    taken within ~2s of the deep link. The unified URI has to be quoted twice: `adb shell` joins its
+    arguments and hands them to the device shell without re-escaping, so quotes written bare on the
+    host are eaten there and the device shell splits the URI at the `&amp;` before `lightning=`,
+    leaving an on-chain-only URI. Wrap the whole remote command in double quotes and the URI in
+    single quotes so one level survives to the device. Android only for now: `bitkit-ios` has no
+    own-invoice guard.
+  </description>
+  <actions>
+    <action>Tap Receive on the home screen (tag "Receive")</action>
+    <action>Tap Edit (tag "SpecifyInvoiceButton"), tap the amount field (tag "ReceiveNumberPadTextField"), enter 1000 with "N1" and "N000", tap Continue (tag "ReceiveNumberPadSubmit"), then tap QR Code (tag "ShowQrReceive")</action>
+    <action>Read the unified `bitcoin:` URI from the QR element's content-desc in `android layout`, and take the `lightning=` value as the own invoice; never type it</action>
+    <action>Press the device back button until the wallet overview is visible</action>
+    <action>Run `adb shell am start -a android.intent.action.VIEW -d "lightning:&lt;own invoice&gt;" to.bitkit.dev`</action>
+    <action>Verify from a screenshot that an error toast titled "Cannot Pay Own Invoice" is visible (tag "SelfPaymentToast")</action>
+    <action>Verify that the wallet overview is still visible and no Send sheet, QuickPay screen or Confirm screen opened</action>
+    <action>Run `adb shell "am start -a android.intent.action.VIEW -d '&lt;unified bitcoin: URI&gt;' to.bitkit.dev"` — outer double quotes, inner single quotes, so the device shell receives the whole URI as one word (see the description)</action>
+    <action>Verify that the send amount screen (tag "send_amount_screen") is visible with 1 000 prefilled and the SAVINGS source selected; without the guard the `lightning=` param would route this to a Lightning send instead</action>
+    <action>Verify that the app log contains "Skipped own lightning invoice in unified URI, defaulting to onchain"</action>
+    <action>Tap Continue (tag "ContinueAmount"), then tap Show Details (tag "SendConfirmToggleDetails")</action>
+    <action>Verify that the Confirm screen shows FROM Savings and TO the wallet's own on-chain address (tag "ReviewUri"); do not swipe to pay</action>
+    <action>Press the device back button until the wallet overview is visible</action>
+    <action>Open Settings ▸ QuickPay (tag "QuickpaySettings"), turn QuickPay on, and return to the wallet overview</action>
+    <action>Run `adb shell am start -a android.intent.action.VIEW -d "lightning:&lt;own invoice&gt;" to.bitkit.dev`</action>
+    <action>Verify from a screenshot that the "Cannot Pay Own Invoice" toast is visible and the QuickPay screen did not open</action>
+    <action>Open Settings ▸ QuickPay and turn QuickPay back off if it was off before this journey</action>
+  </actions>
+</journey>

Automated Checks

  • Unit tests added in AppViewModelSendFlowTest.kt: own LN invoice scan shows the self-payment toast without QuickPay or canSend; foreign invoice still uses QuickPay; missing node ID does not block; scans wait for a starting node ID before QuickPay and before Confirm; unified URI with own invoice falls back to on-chain; foreign unified invoice keeps Lightning; manual input of own invoice shows the toast; own unified invoice with zero savings shows the toast on scan and manual input; foreign unified invoice with zero savings keeps the Insufficient Savings toast.
  • Unit tests added in LightningRepoTest.kt: awaitNodeId returns null without waiting when the node cannot run, waits for a starting node and returns its ID, returns null on timeout, returns the remembered ID after the node stops, and returns it when a restart after a stop fails and leaves ErrorStarting; getLastKnownNodeId returns the remembered ID while the node is not running and null after a storage wipe.
  • Local runs: just compile, just test (2612 tests, 0 failed) and just lint pass.

jvsena42 and others added 2 commits September 16, 2026 19:37
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42 jvsena42 self-assigned this Sep 16, 2026
@greptile-apps

greptile-apps Bot commented Sep 16, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

The PR should not merge as a complete self-payment fix until own invoices are also rejected when the Lightning node is stopped, fails to start, or exceeds the node-ID wait timeout.

Findings

  1. P1 Unavailable ID Bypasses Guard

Summary

This PR adds detection of Lightning invoices issued by the local node, prevents normal scan and manual-entry send flows from paying them, and falls back to the on-chain target for unified Bitcoin URIs. It also introduces bounded node-ID waiting, dedicated customer-facing error text, regression tests, and a changelog entry.

  • Compares decoded invoice payee identities with the local Lightning node identity before QuickPay, channel readiness, and balance checks.
  • Falls back to on-chain sending when a unified URI contains a detected local Lightning invoice.
  • Adds coverage for direct, unified, manual, QuickPay, startup-wait, and zero-savings paths.
  • The guard still becomes permissive whenever the node identity is unavailable, leaving stopped and sufficiently slow startup states uncovered.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Decode Lightning invoice] --> B[Await local node ID]
    B -->|ID available| C{Payee equals local node?}
    C -->|Yes, direct invoice| D[Show Cannot Pay Own Invoice]
    C -->|Yes, unified URI| E[Discard Lightning option]
    E --> F[Continue with on-chain target]
    C -->|No| G[QuickPay or normal send checks]
    B -->|Stopped, failed, or timed out| H[Return null]
    H --> G
Loading

Reviews (1) · Last reviewed commit: "fix: wait for node id and flag own unifi..."

Comment thread app/src/main/java/to/bitkit/repositories/LightningRepo.kt Outdated
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Regtest APK

Built from fd2ef02 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Approved — no HIGH/MEDIUM findings at head 5a907e097. (Posted as a comment because GitHub does not allow approving your own PR.)

Checked: Traced every send entry at the PR head: main scanner and Send-sheet scanner (onScanResult), Send > Paste (onPasteClick), deep links (processDeeplink), clipboard auto-read and ShopWebView payment intents, manual address input (validateAddressWithFeedback plus AddressContinue, which re-runs launchScan), and Paykit contact payments (openContactPayment). All of them funnel through handleScan -> handleDecodedScan -> onScanLightning / onScanOnchain, and the own-invoice check runs there before handleQuickPayIfApplicable, waitForUsableChannels and canSend, so QuickPay (including the quickPayRepo.hasOpen re-entry) cannot pick up an own invoice. The unified bitcoin: path is covered in extractViableLightningInvoice, which now checks isOwnInvoice before the optimistic NodeLifecycleState.Stopped branch, so a remembered node id makes a stopped node fall back to on-chain rather than keep the own invoice. False positives: bitkit-core fills payeeNodeId via recover_payee_pub_key for every valid BOLT11 (src/modules/scanner/implementation.rs:278), so the compare is against the signature-recovered key, hex vs LDK's hex node id with ignoreCase; lastKnownNodeId is only ever written from lightningService.nodeId while Running and cleared in wipeStorage (called by WipeWalletUseCase.wipeLocal), and walletIndex is always 0, so it cannot carry another wallet's id into a fresh wallet. LNURL-pay resolves its invoice at pay time (fetchLnurlInvoice) and is not guarded, but that is pre-existing and ends in the LDK route error rather than a wrong payment. Timeout paths (awaitNodeId returning null) fail open to the pre-PR behaviour, which the author accepted in the greptile thread. No iOS twin PR found. One LOW (restart failure drops the remembered node id, so the guard fails open) is posted as a reply on the existing LightningRepo thread.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Approved — no HIGH/MEDIUM findings at head d3910ff20. (Posted as a comment because GitHub does not allow approving your own PR.)

Checked: Follow-up on d3910ff: the earlier LOW (remembered node id dropped on restart) is fixed. Follow-up at d3910ff resolves the LOW posted on the LightningRepo thread (4046157262). setup() (LightningRepo.kt:294-311) no longer touches lastKnownNodeId, so a stop followed by a failed start (ErrorStarting via 364-377 or 431-434) keeps the remembered id and awaitNodeId() (1669-1672) / getLastKnownNodeId() (1667) still return it; the new test 'awaitNodeId should return last known id when restart after stop fails' reproduces exactly that sequence (start, stop, setup throws, ErrorStarting) and fails without the change because executeWhenNodeRunning fails fast on ErrorStarting.canRun() == false. The id is now written only from lightningService.nodeId while Running (1655-1657, 1672) and cleared only in wipeStorage() (827), which now runs before lightningService.wipeStorage so a failing native wipe still forgets it; when stopLocked() fails, mapCatching is skipped and the id is retained, but WipeWalletUseCase.wipeLocal returns before keychain.wipe() on that failure (WipeWalletUseCase.kt:91), so the mnemonic is unchanged and the retained id is still the active wallet's. Every mnemonic write is createWallet/restoreWallet (WalletRepo.kt:437-475), reachable only from the startup routes that MainActivity shows when !walletExists (MainActivity.kt:144), or the RN migration which only runs when no native mnemonic exists; every wipe entry (Settings reset, recovery mode, dev settings, ResetAndRestore) funnels through WipeWalletUseCase -> lightningRepo.wipeStorage, and start() refuses while isWiping (350) so no node can observe the old id between the LDK wipe and the keychain wipe. walletIndex is always 0 and only selects the storage path (LightningService.setup/build derive entropy from the keychain mnemonic), and the field is in-memory so a process restart resets it. The existing wipe test still passes with the reordered clear. No other lines changed in the delta, so the previously traced send entry paths in AppViewModel are unaffected.

@jvsena42
jvsena42 requested a review from ovitrif September 18, 2026 13:26
jvsena42 and others added 2 commits September 18, 2026 10:35
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Approved — no blocking HIGH/MEDIUM findings at head 8dd6b1dee. (Posted as a comment because GitHub does not allow approving your own PR.)

Inline: MEDIUM (journey, non-blocking) — Unified URI deep link is cut at the first &, so lightning= never reaches the app

Checked: Round 4 (0a83c99, new journey; the finding was re-checked against the journey text at head): Follow-up on 0a83c99 (own invoice guard journey). Every testTag the journey names exists at head: Receive (TabBar), SpecifyInvoiceButton (ReceiveQrScreen), ReceiveNumberPadTextField / ReceiveNumberPadSubmit / ShowQrReceive (EditInvoiceScreen), N1 and N000 (NumberPad, INTEGER pad under the default BITCOIN display), send_amount_screen and ContinueAmount (SendAmountScreen), SendConfirmToggleDetails and ReviewUri (SendConfirmScreen), QuickpaySettings (SettingsScreen); the toast title matches other__pay_self_invoice_title and the log line matches AppViewModel.kt:2187. The QR's content-desc is the raw QR content (QrCodeImage), and the default AUTO tab carries the bip21 with lightning= when a usable channel exists. The BOLT11 deep link steps would fail if the guard were reverted: without the isOwnInvoice check in onScanLightning the scan goes to handleQuickPayIfApplicable / canSend and opens Confirm or QuickPay (1000 sats is under the default $5 QuickPay threshold), so the toast and 'overview still visible' assertions fail. No step completes a payment: the Confirm step says not to swipe, and at head the QuickPay step is stopped by the guard. No bitkit-ios twin exists; the skip is recorded in the cross-platform table and the suite row is added. One MEDIUM on the unified-URI deep link quoting.

Comment thread journeys/send/own-invoice-guard.xml Outdated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Approved — no blocking HIGH/MEDIUM findings at head cb6531022. (Posted as a comment because GitHub does not allow approving your own PR.)

Checked: Follow-up: Reviewed cb65310 only. The new step adb shell "am start -a android.intent.action.VIEW -d '<unified bitcoin: URI>' to.bitkit.dev" hands the host shell one argument that still carries the single quotes, so the device sh -c sees the URI (including ?, & and lightning=) as one word and the package argument survives. The follow-up assertion matches the code: extractViableLightningInvoice (AppViewModel.kt:2175-2208) drops an own lightning= param via isOwnInvoice() and logs "Skipped own lightning invoice in unified URI, defaulting to onchain" at AppViewModel.kt:2187, so the amount screen opens on-chain with SAVINGS; without that check the usable-channel path returns canSend true and the URI would route to Lightning, as the step now says. The two lightning:<invoice> steps keep bare double quotes, which is safe for a bech32 bolt11.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ✅ Approve


Review: diff 8 files.

Findings:
N/A

Audit:
Audited - no findings.

Coverage:
QA: journeys and manual tests await green CI checks


Reviewed by claude-opus-5-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

ovitrif

This comment was marked as resolved.

@ovitrif

ovitrif commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

@jvsena42 conflicts and e2e red

@jvsena42

Copy link
Copy Markdown
Member Author

Both points are covered centrally in #1275 (comment): the conflict is the shared journeys/README.md row that every PR in this batch touches, and a merge of current master is running across all of them; the red e2e shards failed in setup (snapshot load, adb exit 1, app never reaching Home), not in an assertion.

I will confirm the result on this PR once the merge lands and CI re-runs on the new head.

ovi-reviewer[bot]

This comment was marked as resolved.

ovi-reviewer[bot]

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
Member Author

You were right — it is the missing companion e2e branch, not the change.

The settings shard fails @settings_01 and @settings_06 with DrawerSettings still not displayed after 30000ms: the Background Payments intro sheet sits over Home and swallows the drawer tap. Eight other branches in this batch already have a companion branch in bitkit-e2e-tests carrying the best-effort dismiss (#234, #236, #237, #238, #239, #240, #241); fix/901-block-own-invoice had none, so the job resolved main without the fix.

Created it: synonymdev/bitkit-e2e-tests#242, same commit as the others. Re-ran the failed job so it picks the branch up.

ovi-reviewer[bot]

This comment was marked as resolved.

@jvsena42
jvsena42 requested a review from ovitrif September 18, 2026 20:19

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ✅ Approve


Reaudit: diff 1 file.

Findings:
N/A

Audit:
Already done in comment.

QA: emu-1 and emu-2 redroid Android 15 (API 35) on m5a-linux-2, exact-head dev E2E build e2e71e9

  1. passed: Own Lightning invoice was rejected with QuickPay off; Home remained visible and no payment screen opened.

    Test 1:
    **1a.** QuickPay off → scan/deep link own LN invoice: "Cannot Pay Own Invoice" toast, no Send sheet or…
    1.mp4
  2. passed: Own Lightning invoice was rejected with QuickPay on; QuickPay never opened.

    Test 2:
    **1b.** QuickPay on → scan/deep link own LN invoice: same toast, QuickPay does not open.
    2.mp4
  3. passed: Pasting the own invoice and cold-opening it during node startup both showed Cannot Pay Own Invoice without opening QuickPay or Confirm.

    Test 3:
    Cold launch → deep link own LN invoice while node is starting: toast shows once the node is running,…
    3.mp4
  4. passed: Own unified URI fell back to Savings on-chain send and Confirm showed the wallet's own address.

    Test 4:
    **2a.** Savings > amount → scan/deep link own unified QR: Send Amount opens with source Savings → Continue →…
    4.mp4
  5. passed: Zero-Savings own unified URI showed Cannot Pay Own Invoice, while a foreign unified URI showed Insufficient Savings.

    Test 5:
    `regression:` Scan foreign unified QR with savings = 0: "Insufficient Savings" toast still shows when LN…
    5.mp4
  6. passed: Foreign 2,000-sat invoice paid through QuickPay and reached Bitcoin Sent.

    Test 7:
    **4a.** `regression:` Scan foreign LN invoice with QuickPay on: QuickPay pays it.
    7.mp4
  7. passed: With QuickPay off, the foreign 2,000-sat invoice opened Confirm, paid after swipe, and reached Bitcoin Sent.

    Test 8:
    **4b.** `regression:` Scan foreign LN invoice with QuickPay off: Send Confirm opens and payment succeeds.
    8.mp4

Tip

Test 1 worth a journey:

  • Go to Receive
  • Tap Edit and enter 1 000 sats
  • Tap Continue and show the QR code
  • Read the own Lightning invoice from the unified QR
  • Return to Home
  • Open the own Lightning invoice as a deep link
  • Verify Cannot Pay Own Invoice appears
  • Verify Home remains visible with no Send or Confirm screen

Test 2 worth a journey:

  • Go to Settings
  • Open QuickPay
  • Enable QuickPay
  • Return to Home
  • Open the own Lightning invoice as a deep link
  • Verify Cannot Pay Own Invoice appears
  • Verify Home remains visible and QuickPay does not open

Test 3 worth a journey:

  • Go to Receive and select Spending
  • Copy the wallet's own Lightning invoice
  • Return to Home and tap Send
  • Tap Paste Invoice
  • Verify Cannot Pay Own Invoice appears with no Amount or Confirm screen
  • Force-stop Bitkit
  • Cold-open the own Lightning invoice as a deep link
  • Verify the toast appears after the node starts
  • Verify QuickPay and Confirm never open

Test 4 worth a journey:

  • Create a 1 000-sat unified receive QR
  • Return to Home
  • Open the own unified URI as a deep link
  • Verify Send Amount shows 1 000 from Savings
  • Tap Continue
  • Tap Show Details
  • Verify Confirm shows Savings and the wallet's own on-chain address
  • Return to Home without paying

Test 5 worth a journey:

  • Open an own unified URI on a wallet with zero Savings
  • Verify Cannot Pay Own Invoice appears
  • Verify Home remains visible with no payment screen
  • Open a foreign unified URI on the same wallet
  • Verify Insufficient Savings appears
  • Verify Home remains visible with no Send Amount or Confirm screen

Test 7 worth a journey:

  • Enable QuickPay on the funded wallet
  • Create a 2 000-sat Lightning invoice on a second wallet
  • Open the second wallet's invoice on the funded wallet
  • Verify QuickPay pays without confirmation
  • Verify Bitcoin Sent shows 2 000 sats

Test 8 worth a journey:

  • Disable QuickPay on the funded wallet
  • Create a fresh 2 000-sat Lightning invoice on a second wallet
  • Open the second wallet's invoice on the funded wallet
  • Verify Confirm shows 2 000 sats
  • Swipe to pay
  • Verify Bitcoin Sent shows 2 000 sats

Coverage:
Unit tests: 90% - New cases in AppViewModelSendFlowTest.kt and LightningRepoTest.kt cover every new guard and the remembered node id; the hardware-wallet arm of the zero-savings check has none.
QA: 7 of 7 manual tests passed


Reviewed by gpt-5.6-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

ovi-reviewer[bot]

This comment was marked as resolved.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ✅ Approve


Reaudit: diff 1 file.

Findings:
N/A

Audit:
Already done in comment.

QA:
Tested on emu-1 and emu-2 on SSH host m5a-linux-2
Tests 1-5, 7-8 already done in comment.

Coverage:
QA: 7 of 7 manual tests passed


Reviewed by claude-opus-5-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest · audit (author or owner)

jvsena42 and others added 2 commits September 21, 2026 07:08
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

[Bug]: Self-payment on Lightning shows misleading "Insufficient Spending" error

2 participants