Skip to content
11 changes: 8 additions & 3 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<Unit> = lightningService.syncFlow()

Expand Down
10 changes: 6 additions & 4 deletions app/src/main/java/to/bitkit/services/LightningService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<ULong> {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/to/bitkit/ui/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
Expand Down
10 changes: 6 additions & 4 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ class QuickPayViewModel @Inject constructor(
bolt11: String,
amount: ULong? = null,
): Result<PaymentId> {
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 ->
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}
Expand All @@ -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) }
}
Expand Down
13 changes: 11 additions & 2 deletions app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down