From c0925a0577a23ec6184810bc97188b9c074c460c Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Thu, 5 Mar 2026 15:57:12 -0500 Subject: [PATCH] testing the new potential kyoto client --- bdk-android/build.gradle.kts | 2 +- bdk-android/lib/build.gradle.kts | 1 + .../kotlin/org/bitcoindevkit/CbfSyncTest.kt | 79 +++++++++++ bdk-ffi/Cargo.lock | 5 +- bdk-ffi/Cargo.toml | 3 +- bdk-ffi/src/error.rs | 2 + bdk-ffi/src/kyoto.rs | 128 +++++++++--------- 7 files changed, 153 insertions(+), 67 deletions(-) create mode 100644 bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/CbfSyncTest.kt diff --git a/bdk-android/build.gradle.kts b/bdk-android/build.gradle.kts index 1f3a38b2..d4820a2f 100644 --- a/bdk-android/build.gradle.kts +++ b/bdk-android/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("com.android.library").version("8.13.2").apply(false) - id("org.jetbrains.kotlin.android").version("2.1.10").apply(false) + id("org.jetbrains.kotlin.android").version("2.3.0").apply(false) id("org.jetbrains.dokka").version("2.1.0").apply(false) id("com.vanniktech.maven.publish").version("0.36.0").apply(false) } diff --git a/bdk-android/lib/build.gradle.kts b/bdk-android/lib/build.gradle.kts index 647d956f..9532a1f9 100644 --- a/bdk-android/lib/build.gradle.kts +++ b/bdk-android/lib/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") androidTestImplementation("org.jetbrains.kotlin:kotlin-test:1.6.10") androidTestImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.6.10") + androidTestImplementation("org.kotlinbitcointools:regtest-toolbox:0.2.0") } mavenPublishing { diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/CbfSyncTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/CbfSyncTest.kt new file mode 100644 index 00000000..43287795 --- /dev/null +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/CbfSyncTest.kt @@ -0,0 +1,79 @@ +package org.bitcoindevkit + +import kotlin.test.Test +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.runner.RunWith +// import org.kotlinbitcointools.regtesttoolbox.regenv.RegEnv +import java.io.File +import kotlin.time.Duration.Companion.seconds + +@RunWith(AndroidJUnit4::class) +class CbfSyncTest { + private val conn: Persister = Persister.newInMemory() + + private val kyotoDataDir: String by lazy { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val dir = File(context.filesDir, "kyoto_test_data") + dir.mkdirs() + dir.absolutePath + } + + @Test + fun syncWithKyotoClient() { + runBlocking { + // println("Hello, Compact Block Filters!") + // val regtestEnv = RegEnv.connectTo(walletName = "faucet", username = "regtest", password = "password") + + val wallet: Wallet = Wallet.createSingle( + descriptor = BIP86_DESCRIPTOR, + network = Network.REGTEST, + persister = conn + ) + val newAddress = wallet.revealNextAddress(KeychainKind.EXTERNAL).address + println("New address: $newAddress") + delay(2.seconds) + + // regtestEnv.send(newAddress.toString(), 0.12345678, 2.0) + // regtestEnv.mine(2) + + val ip: IpAddress = IpAddress.fromIpv4(10u, 0u, 2u, 2u) + val peer1: Peer = Peer(ip, 18444u, false) + val peers: List = listOf(peer1) + val node = CbfBuilder() + .peers(peers) + .connections(1u) + .scanType(ScanType.Sync) + .dataDir(kyotoDataDir) + .build(wallet) + + println("Test is complete") + val client = node.start() + + val warningJob = launch { + try { + while (true) println("Warning: ${client.nextWarning()}") + } catch (e: CbfException) { println("Warning channel closed: $e") } + } + val infoJob = launch { + try { + while (true) println("Info: ${client.nextInfo()}") + } catch (e: CbfException) { println("Info channel closed: $e") } + } + + val update: Update = client.update() + delay(8.seconds) + wallet.applyUpdate(update) + + val balance = wallet.balance().total.toSat() + assert(balance > 0uL) + + warningJob.cancel() + infoJob.cancel() + client.shutdown() + } + } +} diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index 1c534061..3150421e 100644 --- a/bdk-ffi/Cargo.lock +++ b/bdk-ffi/Cargo.lock @@ -119,7 +119,7 @@ dependencies = [ [[package]] name = "bdk-ffi" -version = "2.3.0-alpha.0" +version = "2.4.0-alpha.0" dependencies = [ "assert_matches", "bdk_electrum", @@ -177,8 +177,7 @@ dependencies = [ [[package]] name = "bdk_kyoto" version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f35b9f8b3aa8c4647bec7a92b050c496742d955e0ac1edcb4e7c2deabf63c54" +source = "git+https://github.com/bitcoindevkit/bdk-kyoto.git?branch=master#539fef2ec32b49ddd0ad31c3601ad0474a84f646" dependencies = [ "bdk_wallet", "bip157", diff --git a/bdk-ffi/Cargo.toml b/bdk-ffi/Cargo.toml index 662ffe67..6e33f54b 100644 --- a/bdk-ffi/Cargo.toml +++ b/bdk-ffi/Cargo.toml @@ -18,7 +18,8 @@ path = "uniffi-bindgen.rs" bdk_wallet = { version = "2.3.0", features = ["all-keys", "keys-bip39", "rusqlite"] } bdk_esplora = { version = "0.22.1", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] } bdk_electrum = { version = "0.23.2", default-features = false, features = ["use-rustls-ring"] } -bdk_kyoto = { version = "0.15.4" } +bdk_kyoto = { git = "https://github.com/bitcoindevkit/bdk-kyoto.git", branch = "master" } +#bdk_kyoto = { version = "0.15.4" } uniffi = { version = "=0.30.0", features = ["cli"]} thiserror = "2.0.17" diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index 10b6d8b1..cb3dbf94 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -797,6 +797,8 @@ pub enum TxidParseError { pub enum CbfError { #[error("the node is no longer running")] NodeStopped, + #[error("the node has already been started")] + AlreadyStarted, } // ------------------------------------------------------------------------ diff --git a/bdk-ffi/src/kyoto.rs b/bdk-ffi/src/kyoto.rs index 2323e94d..5fc8d5f8 100644 --- a/bdk-ffi/src/kyoto.rs +++ b/bdk-ffi/src/kyoto.rs @@ -2,12 +2,9 @@ use bdk_kyoto::bip157::lookup_host; use bdk_kyoto::bip157::tokio; use bdk_kyoto::bip157::AddrV2; use bdk_kyoto::bip157::Network; -use bdk_kyoto::bip157::Node; use bdk_kyoto::bip157::ServiceFlags; use bdk_kyoto::builder::Builder as BDKCbfBuilder; use bdk_kyoto::builder::BuilderExt; -use bdk_kyoto::HeaderCheckpoint; -use bdk_kyoto::LightClient as BDKLightClient; use bdk_kyoto::Receiver; use bdk_kyoto::RejectReason; use bdk_kyoto::Requester; @@ -15,6 +12,7 @@ use bdk_kyoto::TrustedPeer; use bdk_kyoto::UnboundedReceiver; use bdk_kyoto::UpdateSubscriber; use bdk_kyoto::Warning as Warn; +use bdk_kyoto::{HeaderCheckpoint, Idle, LightClient}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::path::PathBuf; @@ -37,51 +35,37 @@ const CWD_PATH: &str = "."; const TCP_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2); const MESSAGE_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5); -/// Receive a [`CbfClient`] and [`CbfNode`]. -#[derive(Debug, uniffi::Record)] -pub struct CbfComponents { - /// Publish events to the node, like broadcasting transactions or adding scripts. - pub client: Arc, - /// The node to run and fetch transactions for a [`Wallet`]. - pub node: Arc, +/// A compact block filters node that has been configured but not yet started. +/// +/// Built via [`CbfBuilder`]. Call [`CbfNode::start`] to prepare and spawn the node, receiving a +/// [`CbfClient`] to interact with the running node. +/// +/// `CbfNode::start` may only be called once. Calling it a second time returns +/// [`CbfError::AlreadyStarted`]. +#[derive(uniffi::Object)] +pub struct CbfNode { + client: std::sync::Mutex>>, } -/// A [`CbfClient`] handles wallet updates from a [`CbfNode`]. -#[derive(Debug, uniffi::Object)] +/// Interact with a running compact block filters node. +/// +/// Obtained by calling [`CbfNode::start`]. Provides access to three independent +/// channels — each message is consumed by exactly one caller, so dedicate a single +/// task or thread to draining each channel: +/// +/// * [`CbfClient::next_info`] — progress and connection events. +/// * [`CbfClient::next_warning`] — non-fatal issues the node encountered. +/// * [`CbfClient::update`] — wallet updates ready to be applied to a [`Wallet`]. +/// +/// Transactions can be broadcast and peers added at any time via the remaining methods. +#[derive(uniffi::Object)] pub struct CbfClient { - sender: Arc, + sender: Requester, info_rx: Mutex>, warning_rx: Mutex>, update_rx: Mutex, } -/// A [`CbfNode`] gathers transactions for a [`Wallet`]. -/// To receive [`Update`] for [`Wallet`], refer to the -/// [`CbfClient`]. The [`CbfNode`] will run until instructed -/// to stop. -#[derive(Debug, uniffi::Object)] -pub struct CbfNode { - node: std::sync::Mutex>, -} - -#[uniffi::export] -impl CbfNode { - /// Start the node on a detached OS thread and immediately return. - pub fn run(self: Arc) { - let mut lock = self.node.lock().unwrap(); - let node = lock.take().expect("cannot call run more than once"); - std::thread::spawn(|| { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .block_on(async move { - let _ = node.run().await; - }) - }); - } -} - /// Build a BIP 157/158 light client to fetch transactions for a `Wallet`. /// /// Options: @@ -178,7 +162,7 @@ impl CbfBuilder { } /// Construct a [`CbfComponents`] for a [`Wallet`]. - pub fn build(&self, wallet: &Wallet) -> CbfComponents { + pub fn build(&self, wallet: &Wallet) -> CbfNode { let wallet = wallet.get_wallet(); let mut trusted_peers = Vec::new(); @@ -241,31 +225,51 @@ impl CbfBuilder { builder = builder.socks5_proxy((addr, port)); } - let BDKLightClient { - requester, - info_subscriber, - warning_subscriber, - update_subscriber, - node, - } = builder + let light_client_idle = builder .build_with_wallet(&wallet, scan_type) .expect("networks match by definition"); - let node = CbfNode { - node: std::sync::Mutex::new(Some(node)), - }; + CbfNode { + client: std::sync::Mutex::new(Some(light_client_idle)), + } + } +} - let client = CbfClient { - sender: Arc::new(requester), - info_rx: Mutex::new(info_subscriber), - warning_rx: Mutex::new(warning_subscriber), - update_rx: Mutex::new(update_subscriber), - }; +#[uniffi::export] +impl CbfNode { + /// Subscribe to log and update channels, then spawn the node on a background thread. + /// + /// Returns a [`CbfClient`] that can be used to receive wallet updates, info and warning + /// messages, and to broadcast transactions. + /// + /// This method may only be called once. A second call returns [`CbfError::AlreadyStarted`]. + pub fn start(&self) -> Result, CbfError> { + let light_client = self + .client + .lock() + .unwrap() + .take() + .ok_or(CbfError::AlreadyStarted)?; - CbfComponents { - client: Arc::new(client), - node: Arc::new(node), - } + let (subscribed, logging, updates) = light_client.subscribe(); + let (active, node) = subscribed.managed_start(); + + std::thread::spawn(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let _ = node.run().await; + }) + }); + + Ok(Arc::new(CbfClient { + sender: active.requester(), + info_rx: Mutex::new(logging.info_subscriber), + warning_rx: Mutex::new(logging.warning_subscriber), + update_rx: Mutex::new(updates), + })) } } @@ -340,7 +344,7 @@ impl CbfClient { /// Add another [`Peer`] to attempt a connection with. pub fn connect(&self, peer: Peer) -> Result<(), CbfError> { self.sender - .add_peer(peer) + .add_peer(TrustedPeer::from(peer)) .map_err(|_| CbfError::NodeStopped) }