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
35 changes: 29 additions & 6 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,9 @@ class LightningRepo @Inject constructor(
}

suspend fun connectToTrustedPeers(): Result<Unit> = executeWhenNodeRunning("connectToTrustedPeers") {
runCatching { lightningService.connectToTrustedPeers() }
runCatching { lightningService.connectToTrustedPeers() }.also {
syncState()
}
}

suspend fun connectPeer(peer: PeerDetails): Result<Unit> = executeWhenNodeRunning("connectPeer") {
Expand Down Expand Up @@ -762,11 +764,23 @@ class LightningRepo @Inject constructor(
bolt11: String,
sats: ULong? = null,
): Result<PaymentId> = executeWhenNodeRunning("payInvoice") {
waitForUsableChannels()
runCatching { lightningService.send(bolt11, sats) }.also {
syncState()
}
}

private suspend fun waitForUsableChannels() {
if (lightningService.channels?.any { it.isUsable } == true) return

Logger.info("Waiting for usable channels before sending payment", context = TAG)
syncState()

withTimeoutOrNull(CHANNELS_USABLE_TIMEOUT_MS) {
_lightningState.first { state -> state.channels.any { it.isUsable } }
} ?: Logger.warn("Timeout waiting for usable channels", context = TAG)
}

@Suppress("LongParameterList")
suspend fun sendOnChain(
address: Address,
Expand Down Expand Up @@ -944,12 +958,19 @@ class LightningRepo @Inject constructor(
}
}

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)
suspend fun canSend(amountSats: ULong, fallbackToCachedBalance: Boolean = true) = withContext(bgDispatcher) {
if (!_lightningState.value.nodeLifecycleState.canRun()) {
return@withContext false
}
if (_lightningState.value.nodeLifecycleState.isStarting() && fallbackToCachedBalance) {
return@withContext amountSats <= (cacheStore.data.first().balance?.maxSendLightningSats ?: 0u)
}
if (lightningService.channels == null) {
withTimeoutOrNull(CHANNELS_READY_TIMEOUT_MS) {
_lightningState.first { lightningService.channels != null }
}
}
return@withContext lightningService.canSend(amountSats)
}

fun getNodeId(): String? =
Expand Down Expand Up @@ -1135,6 +1156,8 @@ class LightningRepo @Inject constructor(
private const val LENGTH_CHANNEL_ID_PREVIEW = 10
private const val MS_SYNC_LOOP_DEBOUNCE = 500L
private const val SYNC_RETRY_DELAY_MS = 15_000L
private const val CHANNELS_READY_TIMEOUT_MS = 15_000L
private const val CHANNELS_USABLE_TIMEOUT_MS = 15_000L
}
}

Expand Down
7 changes: 4 additions & 3 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1132,13 +1132,14 @@ class AppViewModel @Inject constructor(
val maxSendOnchain = walletRepo.balanceState.value.maxSendOnchainSats

val lnInvoice = extractViableLightningInvoice(invoice.params)
val amount = lnInvoice?.amountSatoshis?.takeIf { it > 0uL } ?: invoice.amountSatoshis
_sendUiState.update {
it.copy(
address = invoice.address,
addressInput = scanResult,
isAddressInputValid = true,
amount = invoice.amountSatoshis,
isUnified = lnInvoice != null && invoice.amountSatoshis <= maxSendOnchain && maxSendOnchain > 0u,
amount = amount,
isUnified = lnInvoice != null && amount <= maxSendOnchain && maxSendOnchain > 0u,
decodedInvoice = lnInvoice,
payMethod = lnInvoice?.let { SendMethod.LIGHTNING } ?: SendMethod.ONCHAIN,
)
Expand Down Expand Up @@ -1192,7 +1193,7 @@ class AppViewModel @Inject constructor(
}

Logger.info(
when (invoice.amountSatoshis > 0u) {
when (amount > 0u) {
true -> "Found amount in invoice, proceeding to edit amount"
else -> "No amount found in invoice, proceeding to enter amount"
},
Expand Down
33 changes: 21 additions & 12 deletions app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,13 @@ import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyBlocking
import org.mockito.kotlin.whenever
import to.bitkit.data.AppCacheData
import to.bitkit.data.CacheStore
import to.bitkit.data.SettingsData
import to.bitkit.data.SettingsStore
import to.bitkit.data.backup.VssBackupClientLdk
import to.bitkit.data.keychain.Keychain
import to.bitkit.ext.createChannelDetails
import to.bitkit.ext.of
import to.bitkit.models.BalanceState
import to.bitkit.models.CoinSelectionPreference
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.OpenChannelResult
Expand Down Expand Up @@ -187,8 +185,10 @@ class LightningRepoTest : BaseUnitTest() {
}

@Test
fun `payInvoice should succeed when node is running`() = test {
fun `payInvoice should succeed when node is running and channels are usable`() = test {
startNodeForTesting()
val usableChannel = createChannelDetails().copy(isUsable = true)
whenever(lightningService.channels).thenReturn(listOf(usableChannel))
val testPaymentId = "testPaymentId"
whenever(lightningService.send("bolt11", 1000uL)).thenReturn(testPaymentId)

Expand All @@ -197,6 +197,22 @@ class LightningRepoTest : BaseUnitTest() {
assertEquals(testPaymentId, result.getOrNull())
}

@Test
fun `payInvoice should proceed after timeout when channels are not usable`() = test {
startNodeForTesting()
val testPaymentId = "testPaymentId"
whenever(lightningService.send("bolt11", 1000uL)).thenReturn(testPaymentId)

// Channels are ready but not usable (peer disconnected)
val readyButNotUsable = createChannelDetails().copy(isChannelReady = true, isUsable = false)
whenever(lightningService.channels).thenReturn(listOf(readyButNotUsable))

// payInvoice should wait, timeout, then still attempt to send
val result = sut.payInvoice("bolt11", 1000uL)
assertTrue(result.isSuccess)
assertEquals(testPaymentId, result.getOrNull())
}

@Test
fun `getPayments should fail when node is not running`() = test {
val result = sut.getPayments()
Expand Down Expand Up @@ -312,15 +328,8 @@ class LightningRepoTest : BaseUnitTest() {
}

@Test
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))
fun `canSend should return false when node is stopped`() = test {
assertFalse(sut.canSend(1000uL, fallbackToCachedBalance = true))
}

@Test
Expand Down
Loading