Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
70 changes: 70 additions & 0 deletions app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit>()
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<Unit>()
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"
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1684.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ordinary Lightning payment errors and pending results no longer wait for unrelated Paykit proof maintenance.
20 changes: 20 additions & 0 deletions journeys/wallet/ordinary-during-paykit-outage.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<journey name="ordinary wallet during Paykit outage">
<description>
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.
</description>
<actions>
<action>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</action>
<action>Close and reopen receive, restore Pubky access and verify current public endpoints reconcile without restoring a superseded invoice or a signed-out identity</action>
<action>With pending order or swap recovery and disabled Paykit UI, start the wallet while endpoint cleanup is delayed; verify unrelated recovery watchers start</action>
<action>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</action>
<action>Repeat with a controlled pending Lightning result and verify the original payment hash is tracked and shown as pending; do not submit another payment</action>
<action>Release the service faults, restart and verify retained Paykit work reconciles; a real Paykit request must still require authorization and proof bookkeeping</action>
</actions>
</journey>
Loading