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
9 changes: 6 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
default:
just --list --unsorted

# Run the app in an emulator
run:
./kotlin run
@devices:
adb devices

# Run the app in an emulator or on a device (list your connected devices using `just devices`)
run DEVICE="":
./kotlin run {{ if DEVICE == "" { "" } else { "--device-id=" + DEVICE } }}

# Build the docs website
docs-build:
Expand Down
2 changes: 2 additions & 0 deletions module.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ settings:
compose: enabled
kotlin:
serialization: json
freeCompilerArgs: [ -Xexplicit-backing-fields ]
junit: junit-4

dependencies:
Expand All @@ -23,6 +24,7 @@ dependencies:
- androidx.compose.ui:ui
- androidx.compose.material3:material3
- androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0
- androidx.lifecycle:lifecycle-runtime-compose:2.10.0
- androidx.navigation:navigation-compose:2.9.7

# Icons
Expand Down
95 changes: 70 additions & 25 deletions src/org/bitcoindevkit/devkitwallet/data/Kyoto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package org.bitcoindevkit.devkitwallet.data

import android.util.Log
import kotlin.collections.listOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
Expand All @@ -16,6 +15,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import org.bitcoindevkit.CbfBuilder
import org.bitcoindevkit.CbfClient
import org.bitcoindevkit.CbfException
import org.bitcoindevkit.CbfNode
import org.bitcoindevkit.Info
import org.bitcoindevkit.IpAddress
Expand All @@ -41,10 +41,17 @@ private constructor(
fun start(): Flow<Update> {
kyotoNode.run()

// The client throws an Exception on the `update()` method once the node has stopped (requested shutdown,
// unreachable peers, lost connections); end the flow instead of crashing the collector
return flow {
// Set this to stop under certain circumstances
while (true) {
val update = kyotoClient.update()
val update =
try {
kyotoClient.update()
} catch (e: CbfException) {
Log.i(TAG, "Node has stopped, ending the updates flow: ${e.message}")
break
}
emit(update)
}
}
Expand All @@ -54,7 +61,12 @@ private constructor(
val sharedFlow = MutableSharedFlow<Info>(replay = 0)
scope.launch {
while (true) {
val info = kyotoClient.nextInfo()
val info =
try {
kyotoClient.nextInfo()
} catch (e: CbfException) {
break
}
sharedFlow.emit(info)
}
}
Expand All @@ -65,7 +77,12 @@ private constructor(
val sharedFlow = MutableSharedFlow<Warning>(replay = 0)
scope.launch {
while (true) {
val warning = kyotoClient.nextWarning()
val warning =
try {
kyotoClient.nextWarning()
} catch (e: CbfException) {
break
}
sharedFlow.emit(warning)
}
}
Expand Down Expand Up @@ -102,34 +119,30 @@ private constructor(
}

fun shutdown() {
kyotoClient.shutdown()
try {
kyotoClient.shutdown()
} catch (e: CbfException) {
Log.i(TAG, "Shutdown requested but the node had already stopped: ${e.message}")
}
}

companion object {
private const val TAG = "KyotoClient"

private var instance: Kyoto? = null

fun getInstance(): Kyoto = instance ?: throw KyotoNotInitialized()

fun create(wallet: Wallet, dataDir: String, network: Network): Kyoto {
Log.i(TAG, "Starting Kyoto node")
val peers: List<Peer> =
when (network) {
Network.REGTEST -> {
val ip: IpAddress = IpAddress.fromIpv4(10u, 0u, 2u, 2u)
val peer1: Peer = Peer(ip, 18444u, false)
listOf(peer1)
}

Network.SIGNET -> {
val ip: IpAddress = IpAddress.fromIpv4(68u, 47u, 229u, 218u)
val peer1: Peer = Peer(ip, null, false)
listOf(peer1)
}
fun defaultPeer(network: Network): NodePeer? =
when (network) {
// Default connection point from the emulator with a local regtest setup
Network.REGTEST -> NodePeer(ip = "10.0.2.2", port = 18444u)
else -> null
}

else -> {
listOf()
}
}
fun create(wallet: Wallet, dataDir: String, network: Network, nodePeers: List<NodePeer>): Kyoto {
Log.i(TAG, "Starting Kyoto node with peers: $nodePeers")
val peers: List<Peer> = nodePeers.map { it.toPeer() }

val (client, node) =
CbfBuilder().dataDir(dataDir).peers(peers).connections(1u).scanType(ScanType.Sync).build(wallet)
Expand All @@ -140,3 +153,35 @@ private constructor(
}

class KyotoNotInitialized : Exception()

/**
* A peer the Kyoto node can connect to, kept as simple displayable values. A null port means the default port for the
* network is used.
*/
data class NodePeer(val ip: String, val port: UShort?) {
fun toPeer(): Peer {
val octets = ip.split(".").map { it.toUByte() }
val ipAddress: IpAddress = IpAddress.fromIpv4(octets[0], octets[1], octets[2], octets[3])
return Peer(ipAddress, port, false)
}

override fun toString(): String = if (port != null) "$ip:$port" else ip

companion object {
/** Parses user input into a [NodePeer], returning null if the IP address or port is invalid. */
fun fromInput(ip: String, port: String): NodePeer? {
val octets = ip.trim().split(".")
if (octets.size != 4 || octets.any { it.toUByteOrNull() == null }) return null

val trimmedPort = port.trim()
val parsedPort: UShort? =
if (trimmedPort.isEmpty()) {
null
} else {
trimmedPort.toUShortOrNull() ?: return null
}

return NodePeer(ip.trim(), parsedPort)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
Expand All @@ -29,7 +31,7 @@ import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.CreateNewWal
import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.RecoverWalletScreen
import org.bitcoindevkit.devkitwallet.presentation.ui.screens.intro.WalletChoiceScreen
import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.AboutScreen
import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.BlockchainClientScreen
import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.CbfNodeScreen
import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.LogsScreen
import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.RecoveryDataScreen
import org.bitcoindevkit.devkitwallet.presentation.ui.screens.settings.SettingsScreen
Expand Down Expand Up @@ -135,17 +137,19 @@ fun AppNavigation(

// Wallet screens
composable<HomeScreen> {
val state by walletViewModel!!.state.collectAsStateWithLifecycle()
WalletHomeScreen(
state = walletViewModel!!.state,
state = state,
onAction = walletViewModel::onAction,
snackbarMessages = walletViewModel.snackbarMessages,
navController = navController,
)
}

composable<ReceiveScreen> {
val state by addressViewModel!!.state.collectAsStateWithLifecycle()
ReceiveScreen(
state = addressViewModel!!.state,
state = state,
onAction = addressViewModel::onAction,
navController = navController,
)
Expand Down Expand Up @@ -175,8 +179,9 @@ fun AppNavigation(
}

composable<BlockchainClientScreen> {
BlockchainClientScreen(
state = walletViewModel!!.state,
val state by walletViewModel!!.state.collectAsStateWithLifecycle()
CbfNodeScreen(
state = state,
onAction = walletViewModel::onAction,
navController = navController,
)
Expand Down

This file was deleted.

Loading
Loading