Skip to content
Closed
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
10 changes: 10 additions & 0 deletions bdk-jvm/examples/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ repositories {
dependencies {
implementation(project(":lib"))
testImplementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.slf4j:slf4j-api:2.0.16")
implementation("ch.qos.logback:logback-classic:1.5.13")
}

tasks.test {
Expand Down Expand Up @@ -48,5 +51,12 @@ tasks.register<JavaExec>("MultisigTransaction") {
classpath = sourceSets["main"].runtimeClasspath
}

tasks.register<JavaExec>("Kyoto") {
group = "application"
description = "Runs the main function in the Kyoto example"
mainClass.set("org.bitcoindevkit.KyotoKt")
classpath = sourceSets["main"].runtimeClasspath
}



85 changes: 85 additions & 0 deletions bdk-jvm/examples/src/main/kotlin/Kyoto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.bitcoindevkit

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.nio.file.Files
import java.nio.file.Paths
import org.slf4j.Logger
import org.slf4j.LoggerFactory



fun main() {
// Regtest environment must have Compact block filter enabled to run this
// Setup your environment according to the https://github.com/thunderbiscuit/podman-regtest-infinity-pro documentation.

// Add your regtest IP address
val ip: IpAddress = IpAddress.fromIpv4(127u, 0u, 0u, 1u)
val peer: Peer = Peer(ip, 18444u, false)
val peers: List<Peer> = listOf(peer)
val currentPath = Paths.get(".").toAbsolutePath().normalize()
val persistenceFilePath = Files.createTempDirectory(currentPath, "tempDirPrefix_")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is an example, maybe we can preserve the data in between syncs

Copy link
Collaborator Author

@ItoroD ItoroD Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. I think that totally makes sense for a Kyoto example. Will be great to see the progressive update to your wallet on every sync.

But in this example, each run is getting a new wallet and generating descriptors for those wallets for you (The wallet is non-persistent). User is not adding descriptors of their own. So the idea of them seeing updates between syncs on same wallet will need a little tweaking of this example.

That said I am thinking of maybe creating another Kyoto example that allows persisting, adding your own wallet descriptors, persisting your wallet data, allows/shows user how to play around by sending funds to see new sync updates. Will like to know your thoughts as I think your idea makes a lot of sense

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how difficult this would be in Kotlin, but you could potentially prompt the user with y/n if they would like to add funds.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how difficult this would be in Kotlin, but you could potentially prompt the user with y/n if they would like to add funds.

Pretty simple. For this example its always a new wallet gotten so balance will always be 0 at start.

I will consider adding that in a more interactive example.


val wallet = getNewWallet(ActiveWalletScriptType.P2WPKH, Network.REGTEST)
val address = wallet.revealNextAddress(KeychainKind.EXTERNAL)

// Fund this address. Send coins from your regtest to this address
println("Receiving address. Send funds to this address: ${address.address}")

// Wait 70 seconds for funds to arrive before syncing
println("Waiting 70 seconds for funds to arrive here ${address.address} before kyoto (compact block filter syncing) ...")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the wallet has a non-zero amount at start up, maybe we can skip the funding?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure its sufficient enough to just say if non-zero amount, as they might want to send more than what they have. Maybe checking if amount is sufficient enough for example amount then we can skip.

Thread.sleep(10000)

// Create CBF node and client
runBlocking {
val lightClient = CbfBuilder()
.peers(peers)
.connections(1u)
.scanType(ScanType.New)
.dataDir(persistenceFilePath.toString())
.build(wallet)
val client = lightClient.client
val node = lightClient.node

val logJob = launchLogCollector(this, client::nextLog, "LOG")
val logWarningJob = launchLogCollector(this, client::nextWarning, "WARNING")
val logInfoJob = launchLogCollector(this, client::nextInfo, "INFO")

//Start CBF node
node.run()
println("Node running")

//Update wallet
val update: Update = client.update()
wallet.applyUpdate(update)
println("Wallet balance: ${wallet.balance().total.toSat()} sats")

if(wallet.balance().total.toSat() > 0uL) {
println("Wallet is synced and ready for use!")
println("Test completed successfully")
}else{
println("Wallet balance is 0. Try sending funds to ${address.address} and try again.")
}
logJob.cancelAndJoin()
logWarningJob.cancelAndJoin()
logInfoJob.cancelAndJoin()
client.shutdown()
}
}

fun <T> launchLogCollector(scope: CoroutineScope, collector: suspend () -> T, logType: String) = scope.launch {
val logger: Logger = LoggerFactory.getLogger("LogCollector")

while (true) {
val log = collector()
when (logType) {
"LOG" -> logger.info("$log")
"WARNING" -> logger.warn("$log")
"INFO" -> logger.info("$log")
else -> logger.debug("[$logType]: $log")
}
}
}