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
46 changes: 46 additions & 0 deletions app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import to.bitkit.repositories.LightningRepo
import to.bitkit.repositories.ProbeOutcome
import to.bitkit.utils.Logger
import kotlin.time.Duration.Companion.seconds
import to.bitkit.repositories.ProbeReadiness as NodeProbeReadiness

private const val TAG = "DevToolsProvider"
private val DEV_JSON = Json { encodeDefaults = true }
Expand Down Expand Up @@ -61,6 +62,7 @@ private sealed interface DevCommand {
fun parse(method: String, arg: String?): DevCommand? = when (method) {
CreateInvoice.METHOD -> CreateInvoice.parse(arg)
ProbeInvoice.METHOD -> ProbeInvoice.parse(arg)
ProbeReadiness.METHOD -> ProbeReadiness
else -> null
}
}
Expand Down Expand Up @@ -123,6 +125,13 @@ private sealed interface DevCommand {
)
}
}

data object ProbeReadiness : DevCommand {
const val METHOD = "probeReadiness"

override suspend fun execute(deps: DevToolsProvider.Dependencies): DevResult =
DevResult.ProbeReadiness.from(deps.lightningRepo().probeReadiness())
}
}

@Serializable
Expand Down Expand Up @@ -159,6 +168,43 @@ private sealed interface DevResult {
}
}

@Serializable
data class ProbeReadiness(
val ready: Boolean,
val nodeRunning: Boolean,
val lifecycle: String,
val peers: Int,
val connectedPeers: Int,
val channels: Int,
val readyChannels: Int,
val usableChannels: Int,
val outboundCapacitySats: ULong,
val syncHealthy: Boolean,
val nodeId: String? = null,
val graphNodeCount: Int? = null,
val graphChannelCount: Int? = null,
val latestRgsSyncTimestamp: ULong? = null,
) : DevResult {
companion object {
fun from(readiness: NodeProbeReadiness) = ProbeReadiness(
ready = readiness.ready,
nodeRunning = readiness.nodeRunning,
lifecycle = readiness.lifecycle,
peers = readiness.peers,
connectedPeers = readiness.connectedPeers,
channels = readiness.channels,
readyChannels = readiness.readyChannels,
usableChannels = readiness.usableChannels,
outboundCapacitySats = readiness.outboundCapacitySats,
syncHealthy = readiness.syncHealthy,
nodeId = readiness.nodeId,
graphNodeCount = readiness.graphNodeCount,
graphChannelCount = readiness.graphChannelCount,
latestRgsSyncTimestamp = readiness.latestRgsSyncTimestamp,
)
}
}

@Serializable data class Error(val message: String? = null) : DevResult

fun toBundle() = bundleOf(KEY_RESULT to DEV_JSON.encodeToString(this))
Expand Down
44 changes: 44 additions & 0 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,26 @@ class LightningRepo @Inject constructor(
outcome?.let { Result.success(it) }
?: Result.failure(ProbeError.TimedOut())
}

fun probeReadiness(): ProbeReadiness {
val state = _lightningState.value
val graph = getNetworkGraphInfo()
return ProbeReadiness(
nodeRunning = state.nodeLifecycleState.isRunning(),
nodeId = state.nodeId.takeIf { it.isNotBlank() },
lifecycle = state.nodeLifecycleState.toString(),
peers = state.peers.size,
connectedPeers = state.peers.count { it.isConnected },
channels = state.channels.size,
readyChannels = state.channels.count { it.isChannelReady },
usableChannels = state.channels.count { it.isUsable },
outboundCapacitySats = state.channels.totalNextOutboundHtlcLimitSats(),
graphNodeCount = graph?.nodeCount,
graphChannelCount = graph?.channelCount,
latestRgsSyncTimestamp = graph?.latestRgsSyncTimestamp,
syncHealthy = state.isSyncHealthy,
)
}
// endregion

suspend fun restartNode(): Result<Unit> = withContext(bgDispatcher) {
Expand Down Expand Up @@ -1669,6 +1689,30 @@ data class ProbeDispatch(
val paymentIds: Set<PaymentId>,
)

data class ProbeReadiness(
val nodeRunning: Boolean,
val nodeId: String?,
val lifecycle: String,
val peers: Int,
val connectedPeers: Int,
val channels: Int,
val readyChannels: Int,
val usableChannels: Int,
val outboundCapacitySats: ULong,
val graphNodeCount: Int?,
val graphChannelCount: Int?,
val latestRgsSyncTimestamp: ULong?,
val syncHealthy: Boolean,
) {
val ready: Boolean
get() = nodeRunning &&
connectedPeers > 0 &&
usableChannels > 0 &&
outboundCapacitySats > 0u &&
(graphChannelCount ?: 0) > 0 &&
syncHealthy
Comment thread
piotr-iohk marked this conversation as resolved.
}

sealed interface ProbeOutcome {
val paymentId: PaymentId
val paymentHash: PaymentHash
Expand Down
91 changes: 91 additions & 0 deletions app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import to.bitkit.services.CoreService
import to.bitkit.services.LightningService
import to.bitkit.services.LnurlService
import to.bitkit.services.LspNotificationsService
import to.bitkit.services.NetworkGraphInfo
import to.bitkit.services.NodeEventHandler
import to.bitkit.test.BaseUnitTest
import to.bitkit.utils.UrlValidator
Expand Down Expand Up @@ -1372,6 +1373,96 @@ class LightningRepoTest : BaseUnitTest() {
verifyBlocking(lightningService) { sendKeysendProbe(probeNodeId, 42_000uL) }
}

@Test
fun `probeReadiness reports ready with connected peer, usable channel and network graph`() = test {
startNodeForTesting()
val peer = PeerDetails(
nodeId = probeNodeId,
address = "1.2.3.4:9735",
isConnected = true,
isPersisted = true,
)
val channel = createChannelDetails().copy(
isChannelReady = true,
isUsable = true,
nextOutboundHtlcLimitMsat = 2_000_000u,
)
whenever(lightningService.nodeId).thenReturn("node-1")
whenever(lightningService.peers).thenReturn(listOf(peer))
whenever(lightningService.channels).thenReturn(listOf(channel))
whenever(lightningService.getNetworkGraphInfo())
.thenReturn(NetworkGraphInfo(nodeCount = 1500, channelCount = 4200, latestRgsSyncTimestamp = 123u))
sut.syncState()

val readiness = sut.probeReadiness()

assertTrue(readiness.ready)
assertTrue(readiness.nodeRunning)
assertTrue(readiness.syncHealthy)
assertEquals("node-1", readiness.nodeId)
assertEquals(1, readiness.connectedPeers)
assertEquals(1, readiness.readyChannels)
assertEquals(1, readiness.usableChannels)
assertEquals(2_000uL, readiness.outboundCapacitySats)
assertEquals(1500, readiness.graphNodeCount)
assertEquals(4200, readiness.graphChannelCount)
assertEquals(123u, readiness.latestRgsSyncTimestamp)
}

@Test
fun `probeReadiness reports not ready when usable channel has no outbound capacity`() = test {
startNodeForTesting()
val peer = PeerDetails(
nodeId = probeNodeId,
address = "1.2.3.4:9735",
isConnected = true,
isPersisted = true,
)
val channel = createChannelDetails().copy(
isChannelReady = true,
isUsable = true,
nextOutboundHtlcLimitMsat = 0u,
)
whenever(lightningService.peers).thenReturn(listOf(peer))
whenever(lightningService.channels).thenReturn(listOf(channel))
whenever(lightningService.getNetworkGraphInfo())
.thenReturn(NetworkGraphInfo(nodeCount = 1500, channelCount = 4200, latestRgsSyncTimestamp = 123u))
sut.syncState()

val readiness = sut.probeReadiness()

assertFalse(readiness.ready)
assertEquals(1, readiness.usableChannels)
assertEquals(0uL, readiness.outboundCapacitySats)
}

@Test
fun `probeReadiness reports not ready when channels are not usable`() = test {
startNodeForTesting()
val peer = PeerDetails(
nodeId = probeNodeId,
address = "1.2.3.4:9735",
isConnected = true,
isPersisted = true,
)
val channel = createChannelDetails().copy(
isChannelReady = true,
isUsable = false,
nextOutboundHtlcLimitMsat = 0u,
)
whenever(lightningService.peers).thenReturn(listOf(peer))
whenever(lightningService.channels).thenReturn(listOf(channel))
whenever(lightningService.getNetworkGraphInfo())
.thenReturn(NetworkGraphInfo(nodeCount = 1500, channelCount = 4200, latestRgsSyncTimestamp = 123u))
sut.syncState()

val readiness = sut.probeReadiness()

assertFalse(readiness.ready)
assertEquals(0, readiness.usableChannels)
assertEquals(0uL, readiness.outboundCapacitySats)
}

@Test
fun `start should not retry when node lifecycle state is Running`() = test {
sut.setInitNodeLifecycleState()
Expand Down
Loading