From 0d2d233050096a6d915c075d4d762f574ee81728 Mon Sep 17 00:00:00 2001 From: benk10 Date: Sat, 24 Oct 2026 23:38:01 +0200 Subject: [PATCH 1/2] fix: show ordinary Lightning results without Paykit waits --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 7 +- .../viewmodels/AppViewModelSendFlowTest.kt | 70 +++++++++++++++++++ .../next/ordinary-paykit-waits.fixed.md | 1 + .../wallet/ordinary-during-paykit-outage.xml | 20 ++++++ 4 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 changelog.d/next/ordinary-paykit-waits.fixed.md create mode 100644 journeys/wallet/ordinary-during-paykit-outage.xml diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index ef470a7fd4..94648b955d 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -6228,7 +6228,7 @@ class AppViewModel @Inject constructor( Logger.info("Lightning payment pending", context = TAG) pendingPaymentRepo.track(pendingHash) preserveContactPaymentContext(pendingHash) - refreshIncomingPaykitPaymentRequests() + if (incomingPaymentRequest != null) refreshIncomingPaykitPaymentRequests() setSendEffect(SendEffect.NavigateToPending(pendingHash, displayAmountSats.toLong())) lnurlComment?.let { activityRepo.setLightningMessageIfEmpty(paymentHash, it) } return@onFailure @@ -6261,14 +6261,11 @@ class AppViewModel @Inject constructor( isPaymentRequest: Boolean, ): Boolean = when (error) { is PaymentPendingException -> false + else if !isPaymentRequest -> true is LightningPaymentFailedError -> { paykitPaymentProofRepo.failLightningPayment(paymentHash) true } - else if !isPaymentRequest -> { - paykitPaymentProofRepo.failLightningPayment(paymentHash) - true - } else -> paykitPaymentProofRepo.failLightningPayment(paymentHash, error) } diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index ea979a6e41..0e41372e5a 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -12898,6 +12898,76 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertEquals(1, dispatched) } + @Test + fun `ordinary lightning failure is shown while unrelated proof cleanup is unavailable`() = test { + val bolt11 = "lnbcrt1ordinaryfailure" + val cleanup = CompletableDeferred() + balanceState.value = BalanceState(maxSendLightningSats = 100_000u) + whenever(lightningRepo.payInvoice(bolt11 = bolt11, sats = null)) + .thenReturn(Result.failure(NodeException.PaymentSendingFailed("no route"))) + whenever(paykitPaymentProofRepo.failLightningPayment(any())).doSuspendableAnswer { + cleanup.await() + Result.success(Unit) + } + setSendState( + SendUiState( + address = bolt11, + amount = 1000u, + payMethod = SendMethod.LIGHTNING, + decodedInvoice = lightningInvoice(bolt11, amountSats = 1000u), + ), + ) + try { + sut.sendEffect.test { + sut.setSendEvent(SendEvent.PayConfirmed) + advanceUntilIdle() + assertTrue(awaitItem() is SendEffect.NavigateToError) + verify(paykitPaymentProofRepo, never()).failLightningPayment(any()) + verify(lightningRepo).payInvoice(bolt11 = bolt11, sats = null) + } + } finally { + cleanup.complete(Unit) + } + } + + @Test + fun `ordinary lightning pending state is shown while unrelated proof reconciliation is unavailable`() = test { + val bolt11 = "lnbcrt1ordinarypending" + val paymentHash = "pending_hash" + val reconciliation = CompletableDeferred() + var submitted = false + isPaykitEnabled.value = true + pubkyPublicKey.value = testPublicKey + runCurrent() + balanceState.value = BalanceState(maxSendLightningSats = 100_000u) + whenever(lightningRepo.payInvoice(bolt11 = bolt11, sats = null)).doSuspendableAnswer { + submitted = true + Result.failure(PaymentPendingException(paymentHash)) + } + whenever(paykitPaymentProofRepo.reconcile()).doSuspendableAnswer { + if (submitted) reconciliation.await() + } + setSendState( + SendUiState( + address = bolt11, + amount = 1000u, + payMethod = SendMethod.LIGHTNING, + decodedInvoice = lightningInvoice(bolt11, amountSats = 1000u), + ), + ) + try { + sut.sendEffect.test { + sut.setSendEvent(SendEvent.PayConfirmed) + advanceUntilIdle() + assertEquals(SendEffect.NavigateToPending(paymentHash, 1000), awaitItem()) + verify(pendingPaymentRepo).track(paymentHash) + verify(lightningRepo).payInvoice(bolt11 = bolt11, sats = null) + } + } finally { + reconciliation.complete(Unit) + } + } + @Test fun `private lightning contact payment consumes private list before send`() = test { val bolt11 = "lnbcrt1privatecontact" diff --git a/changelog.d/next/ordinary-paykit-waits.fixed.md b/changelog.d/next/ordinary-paykit-waits.fixed.md new file mode 100644 index 0000000000..684bbe2d28 --- /dev/null +++ b/changelog.d/next/ordinary-paykit-waits.fixed.md @@ -0,0 +1 @@ +Ordinary Lightning payment errors and pending results no longer wait for unrelated Paykit proof maintenance. diff --git a/journeys/wallet/ordinary-during-paykit-outage.xml b/journeys/wallet/ordinary-during-paykit-outage.xml new file mode 100644 index 0000000000..d1b3f042ec --- /dev/null +++ b/journeys/wallet/ordinary-during-paykit-outage.xml @@ -0,0 +1,20 @@ + + + Use disposable test-network wallets with synthetic funds and compatible service fault controls. + Delay public endpoint publication, endpoint cleanup and an unrelated retained Paykit proof while + leaving wallet-node and VSS services available. Never use live funds. iOS requires public sharing + for the receive case, and a retained authenticated identity with Paykit UI disabled for the startup + case. Android must have a retained proof for the unrelated request. These controls are not currently + wired into the full app journey; native component coverage must not be reported as this execution. + Android already prepares receive data independently; iOS already presents ordinary Lightning pending + results independently. Both platforms must keep genuine payment-request proof requirements. + + + Edit an ordinary receive amount and note, then show its QR while public endpoint publication is delayed; verify the QR contains the current amount and note without waiting for Pubky + Close and reopen receive, restore Pubky access and verify current public endpoints reconcile without restoring a superseded invoice or a signed-out identity + With pending order or swap recovery and disabled Paykit UI, start the wallet while endpoint cleanup is delayed; verify unrelated recovery watchers start + Hold unrelated proof maintenance, send a controlled ordinary Lightning invoice that fails and verify its error appears without waiting for Paykit; verify no automatic second payment is created + Repeat with a controlled pending Lightning result and verify the original payment hash is tracked and shown as pending; do not submit another payment + Release the service faults, restart and verify retained Paykit work reconciles; a real Paykit request must still require authorization and proof bookkeeping + + From 3b95acd85998a118e0b14802aee0e172a4893636 Mon Sep 17 00:00:00 2001 From: benk10 Date: Sat, 24 Oct 2026 23:39:44 +0200 Subject: [PATCH 2/2] docs: associate changelog with PR 1684 --- .../next/{ordinary-paykit-waits.fixed.md => 1684.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{ordinary-paykit-waits.fixed.md => 1684.fixed.md} (100%) diff --git a/changelog.d/next/ordinary-paykit-waits.fixed.md b/changelog.d/next/1684.fixed.md similarity index 100% rename from changelog.d/next/ordinary-paykit-waits.fixed.md rename to changelog.d/next/1684.fixed.md