Skip to content
Merged
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
3 changes: 3 additions & 0 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4153,6 +4153,7 @@ class AppViewModel @Inject constructor(
Logger.info("Found amount in invoice, proceeding with payment", context = TAG)

navigateToSendRoute(fromMainScanner, SendRoute.Confirm, SendEffect.NavigateToConfirm)
estimateLightningRoutingFeesIfNeeded()
return
}
Logger.info("No amount found in invoice, proceeding to enter amount", context = TAG)
Expand Down Expand Up @@ -5548,6 +5549,7 @@ class AppViewModel @Inject constructor(
}

private suspend fun estimateLightningRoutingFeesIfNeeded() {
val generation = sendStateGeneration
val currentState = _sendUiState.value
if (currentState.payMethod != SendMethod.LIGHTNING) return
val decodedInvoice = currentState.decodedInvoice ?: return
Expand All @@ -5560,6 +5562,7 @@ class AppViewModel @Inject constructor(

feeResult.onSuccess { fee ->
_sendUiState.update {
if (generation != sendStateGeneration) return@update it
it.copy(
lightningFeeSats = fee.toLong(),
lastLightningFee = fee.toLong(),
Expand Down
95 changes: 93 additions & 2 deletions app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,9 @@ class AppViewModelSendFlowTest : BaseUnitTest() {
whenever { lightningRepo.getFeeRateForSpeed(any(), anyOrNull()) }
.thenReturn(Result.success(2u))
whenever(lightningRepo.canSend(any())).thenReturn(true)
whenever { lightningRepo.estimateRoutingFees(any()) }.thenReturn(Result.failure(Exception("No route")))
whenever { lightningRepo.estimateRoutingFeesForAmount(any(), any()) }
.thenReturn(Result.failure(Exception("No route")))
whenever(toastManager.currentToast).thenReturn(toastState)
}

Expand Down Expand Up @@ -5285,14 +5288,99 @@ class AppViewModelSendFlowTest : BaseUnitTest() {
}

@Test
fun `fixed lightning invoice has valid amount`() = test {
fun `fixed lightning invoice opens confirmation before its routing fee returns`() = test {
val bolt11 = "lnbcrt1fixedamount"
val estimateStarted = CompletableDeferred<Unit>()
val feeEstimate = CompletableDeferred<Result<ULong>>()
stubLightningScan(bolt11 = bolt11, amountSats = 1_000uL)
whenever(lightningRepo.estimateRoutingFees(bolt11)).doSuspendableAnswer {
estimateStarted.complete(Unit)
feeEstimate.await()
}
sut.setIsAuthenticated(true)

sut.onScanResult(bolt11)
estimateStarted.await()
advanceUntilIdle()
val pendingSheet = sut.currentSheet.value
val pendingState = sut.sendUiState.value
feeEstimate.complete(Result.success(2uL))
advanceUntilIdle()

assertTrue(sut.sendUiState.value.isAmountInputValid)
assertEquals(Sheet.Send(SendRoute.Confirm), pendingSheet)
assertTrue(pendingState.isAmountInputValid)
assertEquals(1_000uL, pendingState.amount)
assertNull(pendingState.lightningFeeSats)
assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value)
assertEquals(2L, sut.sendUiState.value.lightningFeeSats)
}

@Test
fun `fixed lightning invoice remains reviewable when routing fee estimation fails`() = test {
val bolt11 = "lnbcrt1unavailablefee"
stubLightningScan(bolt11 = bolt11, amountSats = 1_000uL)
sut.setIsAuthenticated(true)

sut.onScanResult(bolt11)
advanceUntilIdle()

assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value)
assertEquals(1_000uL, sut.sendUiState.value.amount)
assertNull(sut.sendUiState.value.lightningFeeSats)
}

@Test
fun `new scan replaces an invoice with pending routing fee estimation`() = test {
val firstInvoice = "lnbcrt1pendingfee"
val latestInvoice = "lnbcrt1latestfee"
val estimateStarted = CompletableDeferred<Unit>()
stubLightningScan(bolt11 = firstInvoice, amountSats = 1_000uL)
stubLightningScan(bolt11 = latestInvoice, amountSats = 2_000uL)
whenever(lightningRepo.estimateRoutingFees(firstInvoice)).doSuspendableAnswer {
estimateStarted.complete(Unit)
awaitCancellation()
}
whenever(lightningRepo.estimateRoutingFees(latestInvoice)).thenReturn(Result.success(3uL))
sut.setIsAuthenticated(true)

sut.onScanResult(firstInvoice)
estimateStarted.await()
advanceUntilIdle()
assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value)
sut.onScanResult(latestInvoice)
advanceUntilIdle()

assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value)
assertEquals(latestInvoice, sut.sendUiState.value.decodedInvoice?.bolt11)
assertEquals(2_000uL, sut.sendUiState.value.amount)
assertEquals(3L, sut.sendUiState.value.lightningFeeSats)
}

@Test
fun `reset send flow ignores a pending invoice routing fee and confirmation`() = test {
val bolt11 = "lnbcrt1pendingfee"
val estimateStarted = CompletableDeferred<Unit>()
val feeEstimate = CompletableDeferred<Result<ULong>>()
stubLightningScan(bolt11 = bolt11, amountSats = 1_000uL)
whenever(lightningRepo.estimateRoutingFees(bolt11)).doSuspendableAnswer {
estimateStarted.complete(Unit)
feeEstimate.await()
}
sut.setIsAuthenticated(true)

sut.onScanResult(bolt11)
estimateStarted.await()
val newSheet = Sheet.Send(SendRoute.Recipient)
sut.showSheet(newSheet)
sut.resetSendState()
advanceUntilIdle()
val newState = sut.sendUiState.value

feeEstimate.complete(Result.success(2uL))
advanceUntilIdle()

assertEquals(newState, sut.sendUiState.value)
assertEquals(newSheet, sut.currentSheet.value)
}

@Test
Expand Down Expand Up @@ -7430,6 +7518,8 @@ class AppViewModelSendFlowTest : BaseUnitTest() {
balanceState.value = BalanceState(maxSendLightningSats = 100_000u)
stubLightningScan(bolt11 = bolt11, amountSats = 0u)
whenever(lightningRepo.canSend(request.amountSats)).thenReturn(true)
whenever(lightningRepo.estimateRoutingFeesForAmount(bolt11, request.amountSats))
.thenReturn(Result.success(4uL))
val privateContext = stubOpenedPaymentRequest(request, bolt11)
whenever(paykitPaymentRequestRepo.refresh(any())).thenReturn(Result.success(Unit))

Expand All @@ -7441,6 +7531,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() {

assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value)
assertEquals(request.amountSats, sut.sendUiState.value.amount)
assertEquals(4L, sut.sendUiState.value.lightningFeeSats)
assertTrue(sut.sendUiState.value.isAmountInputValid)
assertTrue(sut.sendUiState.value.isPaymentRequest)
assertEquals(
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1461.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed missing fee estimates when reviewing Lightning invoices with a set amount.
19 changes: 19 additions & 0 deletions journeys/send/lightning-fee-review.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<journey name="lightning fee review">
<description>
Verifies that fixed-amount Lightning invoices and Paykit requests show an available numeric
routing-fee estimate before payment approval. Preconditions: two owned regtest wallets linked
as contacts, usable Spending channels with a nonzero route fee, and QuickPay disabled on the
sending wallet. Keep both apps running. Review and cancel each payment without submitting it.
</description>
<actions>
<action>On the receiving wallet, create a fresh 1,000-sat Lightning invoice from Receive and copy its complete invoice</action>
<action>Open that invoice on the sending wallet with a lightning: deep link</action>
<action>Verify confirmation shows 1,000 sats without requiring amount entry</action>
<action>Open Show Details and verify Spending, the recipient invoice and a numeric estimated fee</action>
<action>Dismiss the send review without swiping to pay and verify no outgoing payment was created</action>
<action>On the receiving wallet, create a fresh 1,000-sat payment request for the sending contact with Spending as its only accepted payment method</action>
<action>On the sending wallet, open the received payment request and tap Pay if it has not opened automatically</action>
<action>Open Show Details and verify confirmation shows 1,000 sats, Spending, the receiving contact and a numeric estimated fee</action>
<action>Dismiss the send review without swiping to pay and verify the request remains unpaid</action>
</actions>
</journey>
Loading