Skip to content
Merged
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
15 changes: 3 additions & 12 deletions src/org/bitcoindevkit/devkitwallet/data/Kyoto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,9 @@ private constructor(
return sharedFlow
}

/** Starts two coroutines that collect [infoLog] and [warningLog] into Android Logcat. */
fun logToLogcat() {
scope.launch {
infoLog().collect {
Log.i(TAG, it.toString())
}
}
scope.launch {
warningLog().collect {
Log.i(TAG, it.toString())
}
}
/** Addresses of the peers the node is currently connected to. Throws [CbfException] once the node has stopped. */
suspend fun peerInfo(): List<IpAddress> {
return kyotoClient.peerInfo()
}

/** Resolves a hostname to IPv4 addresses via the Kyoto client. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.recoveryScanCh
/**
* Settings screen for managing the Kyoto Compact Block Filters (CBF) node.
*
* Shows node status, latest known block height, a configurable peer list, and Start/Stop controls that dispatch to
* [WalletViewModel]. A wallet that has never scanned the chain gets a [ScanTypeDialog] asking where to start; once it
* has scanned once, starting the node just resumes from the wallet's own checkpoint.
* Shows node status, the chain tip, the number of connected peers, a configurable peer list, and Start/Stop controls
* that dispatch to [WalletViewModel]. A wallet that has never scanned the chain gets a [ScanTypeDialog] asking where to
* start; once it has scanned once, starting the node just resumes from the wallet's own checkpoint.
*/
@Composable
internal fun CbfNodeScreen(
Expand All @@ -73,6 +73,12 @@ internal fun CbfNodeScreen(
) {
val colorScheme = MaterialTheme.colorScheme
val isRunning = state.kyotoNodeStatus == CbfNodeStatus.Running
val nodeStatus =
when {
!isRunning -> NodeStatusDisplay("Offline", Color(0xFFE76F51))
state.connectedPeerCount == 0 -> NodeStatusDisplay("Connecting", Color(0xFFE9C46A))
else -> NodeStatusDisplay("Online", Color(0xFF8FD998))
}
var showScanTypeDialog by rememberSaveable { mutableStateOf(false) }

if (showScanTypeDialog) {
Expand Down Expand Up @@ -117,35 +123,18 @@ internal fun CbfNodeScreen(
textAlign = TextAlign.Start,
)
Text(
text = if (isRunning) "Online" else "Offline",
color = if (isRunning) Color(0xFF8FD998) else Color(0xFFE76F51),
text = nodeStatus.label,
color = nodeStatus.color,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
fontFamily = inter,
textAlign = TextAlign.End,
)
}

Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
) {
Text(
text = "Latest known block:",
color = colorScheme.onSurface,
fontSize = 14.sp,
fontFamily = inter,
textAlign = TextAlign.Start,
)
Text(
text = "${state.bestBlockHeight}",
color = colorScheme.onSurface,
fontSize = 14.sp,
fontFamily = inter,
textAlign = TextAlign.Start,
)
}
NodeInfoRow(label = "Chain tip:", value = "${state.bestBlockHeight}")

NodeInfoRow(label = "Connected peers:", value = "${state.connectedPeerCount}")

Spacer(modifier = Modifier.padding(8.dp))
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.30f))
Expand Down Expand Up @@ -203,6 +192,35 @@ internal fun CbfNodeScreen(
}
}

/** Label and color used to render the node's high-level status. */
private data class NodeStatusDisplay(val label: String, val color: Color)

/** A label/value line in the node info section, matching the layout used across [CbfNodeScreen]. */
@Composable
private fun NodeInfoRow(label: String, value: String) {
val colorScheme = MaterialTheme.colorScheme
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
) {
Text(
text = label,
color = colorScheme.onSurface,
fontSize = 14.sp,
fontFamily = inter,
textAlign = TextAlign.Start,
)
Text(
text = value,
color = colorScheme.onSurface,
fontSize = 14.sp,
fontFamily = inter,
textAlign = TextAlign.End,
)
}
}

/**
* Dialog shown the first time the user starts the node on a wallet that has never scanned the chain, asking whether
* these keys are new.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.bitcoindevkit.CbfException
import org.bitcoindevkit.Info
import org.bitcoindevkit.Network
import org.bitcoindevkit.RecoveryPoint
import org.bitcoindevkit.ScanType
Expand Down Expand Up @@ -128,9 +130,10 @@ internal class WalletViewModel(private val wallet: Wallet) : ViewModel() {
val peers = state.value.customPeers.ifEmpty { listOfNotNull(defaultPeer) }

val dataDir = wallet.internalAppFilesPath
this.kyoto = Kyoto.create(wallet.wallet, dataDir, wallet.network, peers, scanChoice.toScanType(wallet.network))
val updatesFlow = kyoto!!.start()
state.update { it.copy(kyotoNodeStatus = CbfNodeStatus.Running) }
val kyoto = Kyoto.create(wallet.wallet, dataDir, wallet.network, peers, scanChoice.toScanType(wallet.network))
this.kyoto = kyoto
val updatesFlow = kyoto.start()
state.update { it.copy(kyotoNodeStatus = CbfNodeStatus.Running, connectedPeerCount = 0) }
kyotoCoroutineScope.launch {
var previousHeight: UInt = wallet.bestBlock()

Expand All @@ -153,15 +156,48 @@ internal class WalletViewModel(private val wallet: Wallet) : ViewModel() {

// The updates flow ends when the node stops, whether requested or on its own
Log.i(TAG, "Kyoto updates flow ended, node is no longer running")
state.update { it.copy(kyotoNodeStatus = CbfNodeStatus.Stopped) }
state.update { it.copy(kyotoNodeStatus = CbfNodeStatus.Stopped, connectedPeerCount = 0) }
}
kyoto!!.logToLogcat()

// Single consumer of the node's info log: mirrors it to Logcat and refreshes the peer count. The flow
// completes on its own once the node stops. The peer set is only re-read when a connection is made or
// handshaked, since those are the events that change it.
kyotoCoroutineScope.launch {
kyoto.infoLog().collect { info ->
Log.i(TAG, info.toString())
when (info) {
Info.ConnectionsMet,
Info.SuccessfulHandshake -> refreshConnectedPeerCount(kyoto)
else -> {}
}
}
}

// Warnings such as dropped or failed connections can shrink the peer set, so re-read it on each one.
kyotoCoroutineScope.launch {
kyoto.warningLog().collect { warning ->
Log.i(TAG, warning.toString())
refreshConnectedPeerCount(kyoto)
}
}
}

/** Reads the node's current peer count into [state], ignoring the error thrown once the node has stopped. */
private suspend fun refreshConnectedPeerCount(kyoto: Kyoto) {
val count =
try {
kyoto.peerInfo().size
} catch (e: CbfException) {
Log.i(TAG, "Skipping peer refresh, node is no longer running: ${e.message}")
return
}
state.update { it.copy(connectedPeerCount = count) }
}

/** Requests a graceful shutdown of the Kyoto node. */
private fun stopKyotoNode() {
kyoto!!.shutdown()
state.update { it.copy(kyotoNodeStatus = CbfNodeStatus.Stopped) }
state.update { it.copy(kyotoNodeStatus = CbfNodeStatus.Stopped, connectedPeerCount = 0) }
}

/** Reads the wallet's latest checkpoint height into [state]. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.bitcoindevkit.devkitwallet.domain.CurrencyUnit
* @property network The active Bitcoin network.
* @property bestBlockHeight Chain tip known to the wallet.
* @property kyotoNodeStatus Whether the Kyoto CBF node is running or stopped.
* @property connectedPeerCount Number of peers the running node is currently connected to.
* @property defaultPeer Hard-coded default peer for the current network, if any.
* @property customPeers User-added peers for the Kyoto node.
* @property initialRecoveryDone Whether this wallet has ever completed a scan of the chain.
Expand All @@ -27,6 +28,7 @@ data class WalletScreenState(
val network: Network = Network.SIGNET,
val bestBlockHeight: UInt = 0u,
val kyotoNodeStatus: CbfNodeStatus = CbfNodeStatus.Stopped,
val connectedPeerCount: Int = 0,
val defaultPeer: NodePeer? = null,
val customPeers: List<NodePeer> = emptyList(),
val initialRecoveryDone: Boolean = false,
Expand Down
Loading