diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 91d20b209..f496bf1fe 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -2786,6 +2786,7 @@ class AppViewModel @Inject constructor( return } + _sendUiState.update { it.copy(isAmountInputValid = true) } navigateToSendRoute(fromMainScanner, SendRoute.Confirm, SendEffect.NavigateToConfirm) refreshOnchainSendIfNeeded() estimateLightningRoutingFeesIfNeeded() diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index c4af4a99c..2fc439cca 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -9,12 +9,16 @@ import android.net.Uri import android.nfc.NfcAdapter import androidx.core.net.toUri import app.cash.turbine.test +import com.synonym.bitkitcore.AddressType import com.synonym.bitkitcore.FeeRates import com.synonym.bitkitcore.LightningActivity import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.LnurlPayData import com.synonym.bitkitcore.NetworkType +import com.synonym.bitkitcore.OnChainInvoice import com.synonym.bitkitcore.Scanner +import com.synonym.bitkitcore.ValidationResult +import com.synonym.bitkitcore.validateBitcoinAddress import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.CancellationException @@ -38,9 +42,10 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.lightningdevkit.ldknode.Event -import org.lightningdevkit.ldknode.SpendableUtxo import org.lightningdevkit.ldknode.PaymentFailureReason +import org.lightningdevkit.ldknode.SpendableUtxo import org.lightningdevkit.ldknode.TransactionDetails +import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.atLeast @@ -93,6 +98,7 @@ import to.bitkit.repositories.HealthRepo import to.bitkit.repositories.HwWalletRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LightningState +import to.bitkit.repositories.MethodId import to.bitkit.repositories.NodeEventUpdate import to.bitkit.repositories.PaykitPaymentRequest import to.bitkit.repositories.PaykitPaymentRequestCreation @@ -3715,6 +3721,67 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertTrue(request.id in surfacedPaykitPaymentRequestIds) } + @Test + fun `incoming onchain payment request enables confirm with its fixed amount and context`() = test { + val request = onchainPaymentRequest() + val paymentEndpoint = "bitcoin:$REGTEST_ADDRESS" + val privateContext = PrivatePaykitPaymentContext("bitkit/server", 7uL) + balanceState.value = BalanceState(maxSendOnchainSats = 100_000u) + whenever(coreService.decode(paymentEndpoint)).thenReturn( + Scanner.OnChain(onchainInvoice(amountSats = 0u)), + ) + sut.setIsAuthenticated(true) + + mockValidOnchainAddress().use { + sut.openContactPayment( + paymentRequest = paymentEndpoint, + publicKey = testPublicKey, + privatePaymentContext = privateContext, + incomingPaymentRequest = request, + ) + advanceUntilIdle() + + assertEquals(REGTEST_ADDRESS, sut.sendUiState.value.address) + assertEquals(paymentEndpoint, sut.sendUiState.value.addressInput) + assertEquals(request.amountSats, sut.sendUiState.value.amount) + assertTrue(sut.sendUiState.value.isAddressInputValid) + assertTrue(sut.sendUiState.value.isAmountInputValid) + assertEquals(SendMethod.ONCHAIN, sut.sendUiState.value.payMethod) + assertTrue(sut.sendUiState.value.isPaymentRequest) + assertEquals(Sheet.Send(SendRoute.Confirm), sut.currentSheet.value) + assertEquals( + ContactPaymentContext(testPublicKey, privateContext, request), + activeContactPaymentContext(), + ) + } + } + + @Test + fun `unaffordable incoming onchain payment request remains blocked`() = test { + val request = onchainPaymentRequest() + val paymentEndpoint = "bitcoin:$REGTEST_ADDRESS" + balanceState.value = BalanceState(maxSendOnchainSats = request.amountSats - 1u) + whenever(coreService.decode(paymentEndpoint)).thenReturn( + Scanner.OnChain(onchainInvoice(amountSats = request.amountSats)), + ) + whenever(formatMoneyValue(1u)).thenReturn("1 sat") + sut.setIsAuthenticated(true) + + mockValidOnchainAddress().use { + sut.openContactPayment( + paymentRequest = paymentEndpoint, + publicKey = testPublicKey, + incomingPaymentRequest = request, + ) + advanceUntilIdle() + + assertNull(sut.currentSheet.value) + assertEquals(REGTEST_ADDRESS, sut.sendUiState.value.address) + assertEquals(request.amountSats, sut.sendUiState.value.amount) + assertFalse(sut.sendUiState.value.isAmountInputValid) + } + } + @Test fun `outgoing payment request creation continues after its caller returns`() = test { val request = paymentRequest().copy(counterparty = "pubkyrecipient") @@ -4682,6 +4749,22 @@ class AppViewModelSendFlowTest : BaseUnitTest() { payeeNodeId = null, ) + private fun onchainInvoice(amountSats: ULong) = OnChainInvoice( + address = REGTEST_ADDRESS, + amountSatoshis = amountSats, + label = null, + message = null, + params = null, + ) + + private fun mockValidOnchainAddress() = Mockito.mockStatic( + Class.forName("com.synonym.bitkitcore.Bitkitcore_androidKt"), + ).apply { + `when` { validateBitcoinAddress(REGTEST_ADDRESS) }.thenReturn( + ValidationResult(REGTEST_ADDRESS, NetworkType.REGTEST, AddressType.P2WPKH), + ) + } + private suspend fun enablePublicPaykitSharing() { whenever(publicPaykitRepo.syncCurrentPublishedEndpoints(any(), any())).thenReturn(Result.success(Unit)) walletState.value = WalletState(onchainAddress = "bc1qtest") @@ -4859,6 +4942,10 @@ class AppViewModelSendFlowTest : BaseUnitTest() { acceptedPaymentEndpointIdentifiers = listOf("lightning_bolt11"), ) + private fun onchainPaymentRequest() = paymentRequest().copy( + acceptedPaymentEndpointIdentifiers = listOf(MethodId.P2wpkh.rawValue), + ) + private fun paymentRequestCreation( request: PaykitPaymentRequest, wasPublishedToActiveState: Boolean = true, diff --git a/changelog.d/next/1221.fixed.md b/changelog.d/next/1221.fixed.md new file mode 100644 index 000000000..45e3b9ed9 --- /dev/null +++ b/changelog.d/next/1221.fixed.md @@ -0,0 +1 @@ +Incoming on-chain payment requests can now be confirmed when the requested amount is available.