diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index a786fee65..b085ababd 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -62,7 +62,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -private const val SYNC_TIMEOUT_MS = 10_000L +private const val SYNC_TIMEOUT_MS = 15_000L @Singleton class LightningRepo @Inject constructor( @@ -682,8 +682,13 @@ class LightningRepo @Inject constructor( } } - fun canSend(amountSats: ULong): Boolean = - _lightningState.value.nodeLifecycleState.isRunning() && lightningService.canSend(amountSats) + suspend fun canSend(amountSats: ULong, fallbackToCachedBalance: Boolean = true): Boolean { + return if (!_lightningState.value.nodeLifecycleState.isRunning() && fallbackToCachedBalance) { + amountSats <= (cacheStore.data.first().balance?.maxSendLightningSats ?: 0u) + } else { + lightningService.canSend(amountSats) + } + } fun getSyncFlow(): Flow = lightningService.syncFlow() diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index b0b87dabf..aaa4cb0d3 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -445,11 +445,13 @@ class LightningService @Inject constructor( .getOrElse { e -> throw LdkError(e as NodeException) } return ServiceQueue.LDK.background { - when (sats != null) { - true -> node.bolt11Payment().sendUsingAmount(bolt11Invoice, sats * 1000u, null) - else -> node.bolt11Payment().send(bolt11Invoice, null) + runCatching { + when (sats != null) { + true -> node.bolt11Payment().sendUsingAmount(bolt11Invoice, sats * 1000u, null) + else -> node.bolt11Payment().send(bolt11Invoice, null) + } } - } + }.getOrThrow() } suspend fun estimateRoutingFees(bolt11: String): Result { diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index d40f4b9b8..7e67db80a 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -1,5 +1,6 @@ package to.bitkit.ui +import android.app.NotificationManager import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent @@ -70,10 +71,11 @@ class MainActivity : FragmentActivity() { initNotificationChannel() initNotificationChannel( - // TODO EXTRACT TO Strings + // TODO Transifex id = CHANNEL_ID_NODE, name = "Lightning node notification", desc = "Channel for LightningNodeService", + importance = NotificationManager.IMPORTANCE_LOW ) startForegroundService(Intent(this, LightningNodeService::class.java)) installSplashScreen() diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 1bb53ae38..3f37ac78e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -294,6 +294,7 @@ private fun SendAmountNodeRunning( PrimaryButton( text = stringResource(R.string.common__continue), enabled = uiState.isAmountInputValid, + isLoading = uiState.isLoading, onClick = { onEvent(SendEvent.AmountContinue(uiState.amountInput)) }, modifier = Modifier.testTag("ContinueAmount") ) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index ff9c8804c..3e4ead614 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -351,7 +351,7 @@ class AppViewModel @Inject constructor( } } - private fun onAmountChange(value: String) { + private fun onAmountChange(value: String) = viewModelScope.launch { _sendUiState.update { it.copy( amountInput = value, @@ -405,7 +405,7 @@ class AppViewModel @Inject constructor( } } - private fun onPaymentMethodSwitch() { + private fun onPaymentMethodSwitch() = viewModelScope.launch { val nextPaymentMethod = when (_sendUiState.value.payMethod) { SendMethod.ONCHAIN -> SendMethod.LIGHTNING SendMethod.LIGHTNING -> SendMethod.ONCHAIN @@ -437,8 +437,10 @@ class AppViewModel @Inject constructor( return } + _sendUiState.update { it.copy(isLoading = true) } refreshOnchainSendIfNeeded() estimateLightningRoutingFeesIfNeeded() + _sendUiState.update { it.copy(isLoading = false) } setSendEffect(SendEffect.NavigateToConfirm) } @@ -451,7 +453,7 @@ class AppViewModel @Inject constructor( setSendEffect(SendEffect.NavigateToConfirm) } - private fun validateAmount( + private suspend fun validateAmount( value: String, payMethod: SendMethod = _sendUiState.value.payMethod, ): Boolean { @@ -826,7 +828,7 @@ class AppViewModel @Inject constructor( return false } - private fun resetAmountInput() { + private fun resetAmountInput() = viewModelScope.launch { _sendUiState.update { state -> state.copy( amountInput = state.amount.toString(), diff --git a/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt index 2b9f696ed..cad84d55a 100644 --- a/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt @@ -73,7 +73,11 @@ class QuickPayViewModel @Inject constructor( bolt11: String, amount: ULong? = null, ): Result { - val hash = lightningRepo.payInvoice(bolt11 = bolt11, sats = amount).getOrThrow() + val hash = lightningRepo.payInvoice(bolt11 = bolt11, sats = amount) + .onFailure { exception -> + return Result.failure(exception) + } + .getOrDefault("") // Wait until matching payment event is received val result = ldkNodeEventBus.events.watchUntil { event -> diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index d966ccdbc..c5ee7f7e4 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -163,7 +164,9 @@ class WalletViewModel @Inject constructor( walletRepo.syncNodeAndWallet() .onFailure { error -> Logger.error("Failed to refresh state: ${error.message}", error) - ToastEventBus.send(error) + if (error !is TimeoutCancellationException) { + ToastEventBus.send(error) + } } } } @@ -174,7 +177,9 @@ class WalletViewModel @Inject constructor( walletRepo.syncNodeAndWallet() .onFailure { error -> Logger.error("Failed to refresh state: ${error.message}", error) - ToastEventBus.send(error) + if (error !is TimeoutCancellationException) { + ToastEventBus.send(error) + } } _uiState.update { it.copy(isRefreshing = false) } } diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index f5c891933..413b92603 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -22,12 +22,14 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyBlocking import org.mockito.kotlin.whenever import org.mockito.kotlin.wheneverBlocking +import to.bitkit.data.AppCacheData import to.bitkit.data.CacheStore import to.bitkit.data.SettingsData import to.bitkit.data.SettingsStore import to.bitkit.data.dto.TransactionMetadata import to.bitkit.data.keychain.Keychain import to.bitkit.ext.createChannelDetails +import to.bitkit.models.BalanceState import to.bitkit.models.CoinSelectionPreference import to.bitkit.models.ElectrumServer import to.bitkit.models.LnPeer @@ -286,8 +288,15 @@ class LightningRepoTest : BaseUnitTest() { } @Test - fun `canSend should return false when node is not running`() = test { - assertFalse(sut.canSend(1000uL)) + fun `canSend should use cached outbound when node is not running`() = test { + val cacheData = AppCacheData( + balance = BalanceState( + maxSendLightningSats = 2000uL + ) + ) + whenever(cacheStore.data).thenReturn(flowOf(cacheData)) + + assert(sut.canSend(1000uL, fallbackToCachedBalance = true)) } @Test