Skip to content

Commit a91251d

Browse files
feat: extract kyoto client into its own class
1 parent 4ddf3f2 commit a91251d

8 files changed

Lines changed: 227 additions & 137 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ app.run.xml
2020
release/
2121
.claude/
2222
design/
23+
website/.venv/
24+
website/site/
25+
website/.cache/
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2021-2026 thunderbiscuit and contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file.
4+
*/
5+
6+
package org.bitcoindevkit.devkitwallet.data
7+
8+
import android.util.Log
9+
import kotlinx.coroutines.CoroutineScope
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.flow.Flow
12+
import kotlinx.coroutines.flow.MutableSharedFlow
13+
import kotlinx.coroutines.flow.SharedFlow
14+
import kotlinx.coroutines.flow.flow
15+
import kotlinx.coroutines.launch
16+
import org.bitcoindevkit.CbfBuilder
17+
import org.bitcoindevkit.CbfClient
18+
import org.bitcoindevkit.CbfNode
19+
import org.bitcoindevkit.Info
20+
import org.bitcoindevkit.IpAddress
21+
import org.bitcoindevkit.Network
22+
import org.bitcoindevkit.Peer
23+
import org.bitcoindevkit.ScanType
24+
import org.bitcoindevkit.Transaction
25+
import org.bitcoindevkit.Update
26+
import org.bitcoindevkit.Wallet
27+
import org.bitcoindevkit.Warning
28+
import org.bitcoindevkit.Wtxid
29+
import kotlin.collections.listOf
30+
31+
private const val TAG = "KyotoClient"
32+
33+
// TODO: Document this class well
34+
class Kyoto private constructor(
35+
private val kyotoNode: CbfNode,
36+
private val kyotoClient: CbfClient,
37+
) {
38+
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
39+
40+
fun start(): Flow<Update> {
41+
kyotoNode.run()
42+
43+
return flow {
44+
// Set this to stop under certain circumstances
45+
while (true) {
46+
val update = kyotoClient.update()
47+
emit(update)
48+
}
49+
}
50+
}
51+
52+
fun infoLog(): SharedFlow<Info> {
53+
val sharedFlow = MutableSharedFlow<Info>(replay = 0)
54+
scope.launch {
55+
while (true) {
56+
val info = kyotoClient.nextInfo()
57+
sharedFlow.emit(info)
58+
}
59+
}
60+
return sharedFlow
61+
}
62+
63+
fun warningLog(): SharedFlow<Warning> {
64+
val sharedFlow = MutableSharedFlow<Warning>(replay = 0)
65+
scope.launch {
66+
while (true) {
67+
val warning = kyotoClient.nextWarning()
68+
sharedFlow.emit(warning)
69+
}
70+
}
71+
return sharedFlow
72+
}
73+
74+
fun logToLogcat() {
75+
scope.launch {
76+
infoLog().collect {
77+
Log.i(TAG, it.toString())
78+
}
79+
}
80+
scope.launch {
81+
warningLog().collect {
82+
Log.i(TAG, it.toString())
83+
}
84+
}
85+
}
86+
87+
fun lookupHost(hostname: String): List<IpAddress> {
88+
return kyotoClient.lookupHost(hostname)
89+
}
90+
91+
suspend fun broadcast(transaction: Transaction): Wtxid {
92+
return kyotoClient.broadcast(transaction)
93+
}
94+
95+
fun connect(peer: Peer) {
96+
kyotoClient.connect(peer)
97+
}
98+
99+
fun isRunning(): Boolean {
100+
return kyotoClient.isRunning()
101+
}
102+
103+
fun shutdown() {
104+
kyotoClient.shutdown()
105+
}
106+
107+
companion object {
108+
private var instance: Kyoto? = null
109+
110+
fun getInstance(): Kyoto = instance ?: throw KyotoNotInitialized()
111+
112+
fun create(wallet: Wallet, dataDir: String, network: Network): Kyoto {
113+
Log.i(TAG, "Starting Kyoto node")
114+
val peers: List<Peer> = when(network) {
115+
Network.REGTEST -> {
116+
val ip: IpAddress = IpAddress.fromIpv4(10u, 0u, 2u, 2u)
117+
val peer1: Peer = Peer(ip, 18444u, false)
118+
listOf(peer1)
119+
}
120+
Network.SIGNET -> {
121+
val ip: IpAddress = IpAddress.fromIpv4(68u, 47u, 229u, 218u)
122+
val peer1: Peer = Peer(ip, null, false)
123+
listOf(peer1)
124+
}
125+
else -> { listOf() }
126+
}
127+
128+
val (client, node) =
129+
CbfBuilder()
130+
.dataDir(dataDir)
131+
.peers(peers)
132+
.connections(1u)
133+
.scanType(ScanType.Sync)
134+
.build(wallet)
135+
136+
return Kyoto(node, client).also { instance = it }
137+
}
138+
}
139+
}
140+
141+
class KyotoNotInitialized : Exception()

app/src/main/java/org/bitcoindevkit/devkitwallet/domain/Wallet.kt

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,16 @@ import org.bitcoindevkit.AddressInfo
1212
import org.bitcoindevkit.Amount
1313
import org.bitcoindevkit.BlockId
1414
import org.bitcoindevkit.CanonicalTx
15-
import org.bitcoindevkit.CbfBuilder
1615
import org.bitcoindevkit.CbfClient
1716
import org.bitcoindevkit.ChainPosition
1817
import org.bitcoindevkit.Descriptor
1918
import org.bitcoindevkit.DescriptorSecretKey
2019
import org.bitcoindevkit.FeeRate
21-
import org.bitcoindevkit.IpAddress
2220
import org.bitcoindevkit.KeychainKind
2321
import org.bitcoindevkit.Mnemonic
2422
import org.bitcoindevkit.Network
25-
import org.bitcoindevkit.Peer
2623
import org.bitcoindevkit.Persister
2724
import org.bitcoindevkit.Psbt
28-
import org.bitcoindevkit.ScanType
2925
import org.bitcoindevkit.Script
3026
import org.bitcoindevkit.TxBuilder
3127
import org.bitcoindevkit.Update
@@ -46,22 +42,26 @@ import org.bitcoindevkit.Wallet as BdkWallet
4642
private const val TAG = "Wallet"
4743

4844
class Wallet private constructor(
49-
private val wallet: BdkWallet,
45+
val wallet: BdkWallet,
5046
private val walletSecrets: WalletSecrets,
5147
private val connection: Persister,
5248
private var fullScanCompleted: Boolean,
5349
private val walletId: String,
5450
private val userPreferencesRepository: UserPreferencesRepository,
55-
private val internalAppFilesPath: String,
51+
val internalAppFilesPath: String,
5652
blockchainClientsConfig: BlockchainClientsConfig,
53+
val network: Network,
5754
) {
5855
private var currentBlockchainClient: BlockchainClient? = blockchainClientsConfig.getClient()
59-
public var kyotoClient: CbfClient? = null
6056

6157
fun getWalletSecrets(): WalletSecrets {
6258
return walletSecrets
6359
}
6460

61+
fun bestBlock(): UInt {
62+
return wallet.latestCheckpoint().height
63+
}
64+
6565
fun createTransaction(recipientList: List<Recipient>, feeRate: FeeRate, opReturnMsg: String?): Psbt {
6666
// technique 1 for adding a list of recipients to the TxBuilder
6767
// var txBuilder = TxBuilder()
@@ -74,7 +74,7 @@ class Wallet private constructor(
7474
var txBuilder =
7575
recipientList.fold(TxBuilder()) { builder, recipient ->
7676
// val address = Address(recipient.address)
77-
val scriptPubKey: Script = Address(recipient.address, Network.TESTNET).scriptPubkey()
77+
val scriptPubKey: Script = Address(recipient.address, this.network).scriptPubkey()
7878
builder.addRecipient(scriptPubKey, Amount.fromSat(recipient.amount))
7979
}
8080
// if (!opReturnMsg.isNullOrEmpty()) {
@@ -115,11 +115,11 @@ class Wallet private constructor(
115115
return wallet.sign(psbt)
116116
}
117117

118-
fun broadcast(signedPsbt: Psbt): String {
119-
currentBlockchainClient?.broadcast(signedPsbt.extractTx())
120-
?: throw IllegalStateException("Blockchain client not initialized")
121-
return signedPsbt.extractTx().computeTxid().toString()
122-
}
118+
// fun broadcast(signedPsbt: Psbt): String {
119+
// currentBlockchainClient?.broadcast(signedPsbt.extractTx())
120+
// ?: throw IllegalStateException("Blockchain client not initialized")
121+
// return signedPsbt.extractTx().computeTxid().toString()
122+
// }
123123

124124
private fun getAllTransactions(): List<CanonicalTx> = wallet.transactions()
125125

@@ -180,39 +180,10 @@ class Wallet private constructor(
180180

181181
fun getNewAddress(): AddressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL)
182182

183-
fun getLastCheckpoint(): BlockId = wallet.latestCheckpoint()
184-
185-
fun startKyotoNode() {
186-
Log.i(TAG, "Starting Kyoto node")
187-
// Regtest
188-
val ip: IpAddress = IpAddress.fromIpv4(10u, 0u, 2u, 2u)
189-
val peer1: Peer = Peer(ip, 18444u, false)
190-
191-
// Signet
192-
// val ip: IpAddress = IpAddress.fromIpv4(68u, 47u, 229u, 218u)
193-
// val peer1: Peer = Peer(ip, null, false)
194-
val peers: List<Peer> = listOf(peer1)
195-
196-
val (client, node) =
197-
CbfBuilder()
198-
.dataDir(this.internalAppFilesPath)
199-
.peers(peers)
200-
.connections(1u)
201-
.scanType(ScanType.Sync)
202-
.build(this.wallet)
203-
204-
node.run()
205-
kyotoClient = client
206-
Log.i(TAG, "Kyoto node started")
207-
}
208-
209-
suspend fun stopKyotoNode() {
210-
kyotoClient?.shutdown()
211-
}
212-
213183
fun applyUpdate(update: Update) {
214184
wallet.applyUpdate(update)
215185
wallet.persist(connection)
186+
Log.i("KYOTOTEST", "Wallet applied a Kyoto update")
216187
}
217188

218189
companion object {
@@ -276,6 +247,7 @@ class Wallet private constructor(
276247
userPreferencesRepository = userPreferencesRepository,
277248
internalAppFilesPath = internalAppFilesPath,
278249
blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(newWalletConfig.network),
250+
network = newWalletConfig.network
279251
)
280252
}
281253

@@ -306,6 +278,7 @@ class Wallet private constructor(
306278
blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(
307279
activeWallet.network.intoDomain()
308280
),
281+
network = activeWallet.network.intoDomain()
309282
)
310283
}
311284

@@ -380,6 +353,7 @@ class Wallet private constructor(
380353
userPreferencesRepository = userPreferencesRepository,
381354
internalAppFilesPath = internalAppFilesPath,
382355
blockchainClientsConfig = BlockchainClientsConfig.createDefaultConfig(recoverWalletConfig.network),
356+
network = recoverWalletConfig.network
383357
)
384358
}
385359
}

app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/drawer/BlockchainClientScreen.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import androidx.navigation.NavController
3131
import org.bitcoindevkit.devkitwallet.presentation.theme.inter
3232
import org.bitcoindevkit.devkitwallet.presentation.ui.components.NeutralButton
3333
import org.bitcoindevkit.devkitwallet.presentation.ui.components.SecondaryScreensAppBar
34-
import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.KyotoNodeStatus
34+
import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.CbfNodeStatus
3535
import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenAction
3636
import org.bitcoindevkit.devkitwallet.presentation.viewmodels.mvi.WalletScreenState
3737

@@ -66,7 +66,7 @@ internal fun BlockchainClientScreen(
6666
horizontalArrangement = Arrangement.SpaceBetween,
6767
modifier = Modifier.fillMaxWidth(),
6868
) {
69-
val status = if (state.kyotoNodeStatus == KyotoNodeStatus.Running) "Online" else "Offline"
69+
val status = if (state.kyotoNodeStatus == CbfNodeStatus.Running) "Online" else "Offline"
7070
Text(
7171
text = "CBF Node Status: $status",
7272
color = colorScheme.onSurface,
@@ -81,7 +81,7 @@ internal fun BlockchainClientScreen(
8181
.size(size = 21.dp)
8282
.clip(shape = CircleShape)
8383
.background(
84-
if (state.kyotoNodeStatus == KyotoNodeStatus.Running) {
84+
if (state.kyotoNodeStatus == CbfNodeStatus.Running) {
8585
Color(0xFF8FD998)
8686
} else {
8787
Color(0xFFE76F51)
@@ -103,7 +103,7 @@ internal fun BlockchainClientScreen(
103103
textAlign = TextAlign.Start,
104104
)
105105
Text(
106-
text = "${state.latestBlock}",
106+
text = "${state.bestBlockHeight}",
107107
color = colorScheme.onSurface,
108108
fontSize = 14.sp,
109109
fontFamily = inter,
@@ -115,12 +115,12 @@ internal fun BlockchainClientScreen(
115115

116116
NeutralButton(
117117
text = "Start Node",
118-
enabled = state.kyotoNodeStatus == KyotoNodeStatus.Stopped,
119-
onClick = { onAction(WalletScreenAction.StartKyotoNode) },
118+
enabled = state.kyotoNodeStatus == CbfNodeStatus.Stopped,
119+
onClick = { onAction(WalletScreenAction.ActivateCbfNode) },
120120
)
121121
NeutralButton(
122122
text = "Stop Node",
123-
enabled = state.kyotoNodeStatus == KyotoNodeStatus.Running,
123+
enabled = state.kyotoNodeStatus == CbfNodeStatus.Running,
124124
onClick = { onAction(WalletScreenAction.StopKyotoNode) },
125125
)
126126
}

app/src/main/java/org/bitcoindevkit/devkitwallet/presentation/ui/screens/wallet/SendScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ private fun ConfirmDialog(
447447
if (checkRecipientList(recipientList = recipientList, feeRate = feeRate, context = context)) {
448448
val txDataBundle =
449449
TxDataBundle(
450-
recipients = recipientList,
450+
recipients = recipientList.toList(),
451451
feeRate = feeRate.value.toULong(),
452452
transactionType = transactionType,
453453
opReturnMsg = opReturnMsg,

0 commit comments

Comments
 (0)