diff --git a/.gitignore b/.gitignore index 22598c01..95d0d902 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ local.properties *.log *.dylib *.so +*.db +*/data/signet .DS_Store testdb .lsp diff --git a/bdk-android/lib/build.gradle.kts b/bdk-android/lib/build.gradle.kts index 1f5b7866..098f041f 100644 --- a/bdk-android/lib/build.gradle.kts +++ b/bdk-android/lib/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") implementation("androidx.appcompat:appcompat:1.4.0") implementation("androidx.core:core-ktx:1.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") api("org.slf4j:slf4j-api:1.7.30") androidTestImplementation("com.github.tony19:logback-android:2.0.0") diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index 83f4fb59..300c6644 100644 --- a/bdk-ffi/Cargo.lock +++ b/bdk-ffi/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.11" @@ -66,9 +81,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" [[package]] name = "arrayvec" @@ -88,6 +103,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base58ck" version = "0.1.0" @@ -127,6 +157,7 @@ dependencies = [ "bdk_core", "bdk_electrum", "bdk_esplora", + "bdk_kyoto", "bdk_wallet", "thiserror", "uniffi", @@ -177,6 +208,16 @@ dependencies = [ "miniscript", ] +[[package]] +name = "bdk_kyoto" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8922b9a7b279e260bca897fa14fd6f735b529c6ac95d87a25d2b170799e103b0" +dependencies = [ + "bdk_wallet", + "kyoto-cbf", +] + [[package]] name = "bdk_wallet" version = "1.1.0" @@ -198,6 +239,19 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +[[package]] +name = "bip324" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b443a76f86143c093b211628be683ee592a097d316db6b90f723ed816bde1a49" +dependencies = [ + "bitcoin", + "bitcoin_hashes 0.15.0", + "chacha20-poly1305", + "rand", + "tokio", +] + [[package]] name = "bip39" version = "2.1.0" @@ -219,7 +273,7 @@ dependencies = [ "base64 0.21.7", "bech32", "bitcoin-internals 0.3.0", - "bitcoin-io", + "bitcoin-io 0.1.3", "bitcoin-units", "bitcoin_hashes 0.14.0", "hex-conservative 0.2.1", @@ -243,12 +297,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin-internals" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b854212e29b96c8f0fe04cab11d57586c8f3257de0d146c76cb3b42b3eb9118" + [[package]] name = "bitcoin-io" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +[[package]] +name = "bitcoin-io" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26792cd2bf245069a1c5acb06aa7ad7abe1de69b507c90b490bca81e0665d0ee" +dependencies = [ + "bitcoin-internals 0.4.0", +] + [[package]] name = "bitcoin-units" version = "0.1.2" @@ -275,11 +344,21 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" dependencies = [ - "bitcoin-io", + "bitcoin-io 0.1.3", "hex-conservative 0.2.1", "serde", ] +[[package]] +name = "bitcoin_hashes" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0982261c82a50d89d1a411602afee0498b3e0debe3d36693f0c661352809639" +dependencies = [ + "bitcoin-io 0.2.0", + "hex-conservative 0.3.0", +] + [[package]] name = "bitflags" version = "2.8.0" @@ -345,6 +424,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20-poly1305" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac8be588b1de2b7f1537ed39ba453a388d2cce60ce78ef5db449f71bebe58ba" + [[package]] name = "clap" version = "4.5.30" @@ -453,6 +538,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "glob" version = "0.3.2" @@ -495,6 +586,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex-conservative" version = "0.1.2" @@ -510,6 +607,15 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-conservative" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex_lit" version = "0.1.1" @@ -528,6 +634,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "kyoto-cbf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86c956588363eeaab7c784e0ef9d1de7f93b7d550b241bf5dbcdb9a7106cf8d9" +dependencies = [ + "bip324", + "bitcoin", + "rusqlite", + "tokio", +] + [[package]] name = "libc" version = "0.2.169" @@ -590,6 +708,15 @@ dependencies = [ "serde", ] +[[package]] +name = "miniz_oxide" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +dependencies = [ + "adler2", +] + [[package]] name = "minreq" version = "2.13.2" @@ -606,6 +733,17 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "nom" version = "7.1.3" @@ -616,6 +754,25 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.3" @@ -628,6 +785,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pkg-config" version = "0.3.31" @@ -764,6 +927,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -892,18 +1061,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", @@ -912,9 +1081,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" dependencies = [ "itoa", "memchr", @@ -946,6 +1115,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1019,6 +1198,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.5.11" @@ -1036,9 +1243,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unicode-normalization" @@ -1243,13 +1450,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1258,7 +1474,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -1267,28 +1498,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1301,24 +1550,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/bdk-ffi/Cargo.toml b/bdk-ffi/Cargo.toml index 2b23fff8..fd3fa58c 100644 --- a/bdk-ffi/Cargo.toml +++ b/bdk-ffi/Cargo.toml @@ -22,6 +22,7 @@ bdk_wallet = { version = "1.1.0", features = ["all-keys", "keys-bip39", "rusqlit bdk_core = { version = "0.4.1" } bdk_esplora = { version = "0.20.1", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] } bdk_electrum = { version = "0.21.0", default-features = false, features = ["use-rustls-ring"] } +bdk_kyoto = { version = "0.8.0" } uniffi = { version = "=0.29.0" } thiserror = "1.0.58" diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index 4bea57e0..1acc3d2a 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -330,6 +330,20 @@ interface TxidParseError { InvalidTxid(string txid); }; +/// Errors that may occur building a light client. +[Error] +interface LightClientBuilderError { + /// A database could not be opened or created. + DatabaseError(string reason); +}; + +/// Errors that may occur sending messages to a node. +[Error] +enum LightClientError { + /// The node is not currently running. + "NodeStopped", +}; + // ------------------------------------------------------------------------ // bdk_wallet crate - types module // ------------------------------------------------------------------------ @@ -1300,6 +1314,229 @@ interface AddressData { Segwit(WitnessProgram witness_program); }; +// ------------------------------------------------------------------------ +// bdk-kyoto crate +// ------------------------------------------------------------------------ + +/// Build a BIP 157/158 light client to fetch transactions for a `Wallet`. +/// +/// Options: +/// * List of `Peer`: Bitcoin full-nodes for the light client to connect to. May be empty. +/// * `connections`: The number of connections for the light client to maintain. +/// * `scan_type`: Sync, recover, or start a new wallet. For more information see [`ScanType`]. +/// * `data_dir`: Optional directory to store block headers and peers. +/// +/// A note on recovering wallets. Developers should allow users to provide an +/// approximate recovery height and an estimated number of transactions for the +/// wallet. When determining how many scripts to check filters for, the `Wallet` +/// `lookahead` value will be used. To ensure all transactions are recovered, the +/// `lookahead` should be roughly the number of transactions in the wallet history. +interface LightClientBuilder { + /// Start a new [`LightClientBuilder`] + constructor(); + + /// The number of connections for the light client to maintain. Default is two. + LightClientBuilder connections(u8 connections); + + /// Directory to store block headers and peers. If none is provided, the current + /// working directory will be used. + LightClientBuilder data_dir(string data_dir); + + /// Select between syncing, recovering, or scanning for new wallets. + LightClientBuilder scan_type(ScanType scan_type); + + /// Bitcoin full-nodes to attempt a connection with. + LightClientBuilder peers(sequence peers); + + /// Construct a [`LightClient`] for a [`Wallet`]. + [Throws=LightClientBuilderError] + LightClient build([ByRef] Wallet wallet); +}; + +/// A [`Client`] handles wallet updates from a [`LightNode`]. +interface Client { + /// Return the next available log message from a node. If none is returned, the node has stopped. + [Async, Throws=LightClientError] + Log next_log(); + + /// Return the next available warning message from a node. If none is returned, the node has stopped. + [Async, Throws=LightClientError] + Warning next_warning(); + + /// Return an [`Update`]. This is method returns once the node syncs to the rest of + /// the network or a new block has been gossiped. + [Async] + Update? update(); + + /// Add scripts for the node to watch for as they are revealed. Typically used after creating + /// a transaction or revealing a receive address. + /// + /// Note that only future blocks will be checked for these scripts, not past blocks. + [Async, Throws=LightClientError] + void add_revealed_scripts([ByRef] Wallet wallet); + + /// The minimum fee rate required to broadcast a transcation to all connected peers. + [Async, Throws=LightClientError] + FeeRate min_broadcast_feerate(); + + /// Broadcast a transaction to the network, erroring if the node has stopped running. + [Async, Throws=LightClientError] + void broadcast([ByRef] Transaction transaction); + + /// Check if the node is still running in the background. + [Async] + boolean is_running(); + + /// Stop the [`LightNode`]. Errors if the node is already stopped. + [Async, Throws=LightClientError] + void shutdown(); +}; + +/// A [`LightNode`] gathers transactions for a [`Wallet`]. +/// To receive [`Update`] for [`Wallet`], refer to the +/// [`Client`]. The [`LightNode`] will run until instructed +/// to stop. +interface LightNode { + /// Start the node on a detached OS thread and immediately return. + void run(); +}; + +/// Receive a [`Client`] and [`LightNode`]. +dictionary LightClient { + /// Publish events to the node, like broadcasting transactions or adding scripts. + Client client; + + /// The node to run and fetch transactions for a [`Wallet`]. + LightNode node; +}; + +/// Sync a wallet from the last known block hash, recover a wallet from a specified height, +/// or perform an expedited block header download for a new wallet. +[Enum] +interface ScanType { + /// Perform an expedited header and filter download for a new wallet. + /// If this option is not set, and the wallet has no history, the + /// entire chain will be scanned for script inclusions. + New(); + + /// Sync an existing wallet from the last stored chain checkpoint. + Sync(); + + /// Recover an existing wallet by scanning from the specified height. + Recovery(u32 from_height); +}; + +/// A peer to connect to over the Bitcoin peer-to-peer network. +dictionary Peer { + /// The IP address to reach the node. + IpAddress address; + + /// The port to reach the node. If none is provided, the default + /// port for the selected network will be used. + u16? port; + + /// Does the remote node offer encrypted peer-to-peer connection. + boolean v2_transport; +}; + +/// An IP address to connect to over TCP. +interface IpAddress { + /// Build an IPv4 address. + [Name=from_ipv4] + constructor(u8 q1, u8 q2, u8 q3, u8 q4); + + /// Build an IPv6 address. + [Name=from_ipv6] + constructor(u16 a, u16 b, u16 c, u16 d, u16 e, u16 f, u16 g, u16 h); +}; + +/// A log message from the node. +[Enum] +interface Log { + /// A human-readable debug message. + Debug(string log); + + /// All the required connections have been met. This is subject to change. + ConnectionsMet(); + + /// A percentage value of filters that have been scanned. + Progress(f32 progress); + + /// A state in the node syncing process. + StateUpdate(NodeState node_state); + + /// A transaction was broadcast over the wire. + /// The transaction may or may not be rejected by recipient nodes. + TxSent(string txid); +}; + +/// Warnings a node may issue while running. +[Enum] +interface Warning { + /// The node is looking for connections to peers. + NeedConnections(); + + /// A connection to a peer timed out. + PeerTimedOut(); + + /// The node was unable to connect to a peer in the database. + CouldNotConnect(); + + /// A connection was maintained, but the peer does not signal for compact block filers. + NoCompactFilters(); + + /// The node has been waiting for new inv and will find new peers to avoid block withholding. + PotentialStaleTip(); + + /// A peer sent us a peer-to-peer message the node did not request. + UnsolicitedMessage(); + + /// The provided starting height is deeper than the database history. + /// This should not occur under normal use. + InvalidStartHeight(); + + /// The headers in the database do not link together. + /// Recoverable by deleting the database. + CorruptedHeaders(); + + /// A transaction got rejected, likely for being an insufficient fee or non-standard transaction. + TransactionRejected(string txid, string? reason); + + /// A database failed to persist some data and may retry again. + FailedPersistence(string warning); + + /// The peer sent us a potential fork. + EvaluatingFork(); + + /// The peer database has no values. + EmptyPeerDatabase(); + + /// An unexpected error occured processing a peer-to-peer message. + UnexpectedSyncError(string warning); + + /// The node failed to respond to a message sent from the client. + RequestFailed(); +}; + +/// The state of the node with respect to connected peers. +[Remote] +enum NodeState { + /// Behind on block headers according to our peers. + "Behind", + + /// Downloading compact block filter headers. + "HeadersSynced", + + /// Scanning compact block filters. + "FilterHeadersSynced", + + /// Asking for blocks with matches. + "FiltersSynced", + + /// Found all known transactions to the wallet. + "TransactionsSynced" +}; + // ------------------------------------------------------------------------ // bdk_wallet crate - bitcoin re-exports // ------------------------------------------------------------------------ diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index 1f1bfac1..4456afe2 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -782,6 +782,18 @@ pub enum TxidParseError { InvalidTxid { txid: String }, } +#[derive(Debug, thiserror::Error)] +pub enum LightClientBuilderError { + #[error("the database could not be opened or created: {reason}")] + DatabaseError { reason: String }, +} + +#[derive(Debug, thiserror::Error)] +pub enum LightClientError { + #[error("the node is no longer running")] + NodeStopped, +} + // ------------------------------------------------------------------------ // error conversions // ------------------------------------------------------------------------ @@ -1503,6 +1515,20 @@ impl From for SqliteError { } } +impl From for LightClientBuilderError { + fn from(value: bdk_kyoto::builder::SqlInitializationError) -> Self { + LightClientBuilderError::DatabaseError { + reason: value.to_string(), + } + } +} + +impl From for LightClientError { + fn from(_value: bdk_kyoto::kyoto::ClientError) -> Self { + LightClientError::NodeStopped + } +} + // ------------------------------------------------------------------------ // error tests // ------------------------------------------------------------------------ diff --git a/bdk-ffi/src/kyoto.rs b/bdk-ffi/src/kyoto.rs new file mode 100644 index 00000000..b37f5d80 --- /dev/null +++ b/bdk-ffi/src/kyoto.rs @@ -0,0 +1,378 @@ +use bdk_kyoto::builder::LightClientBuilder as BDKLightClientBuilder; +use bdk_kyoto::builder::ServiceFlags; +use bdk_kyoto::builder::TrustedPeer; +use bdk_kyoto::kyoto::tokio; +use bdk_kyoto::kyoto::AddrV2; +use bdk_kyoto::kyoto::ScriptBuf; +use bdk_kyoto::LightClient as BDKLightClient; +use bdk_kyoto::NodeDefault; +use bdk_kyoto::NodeState; +use bdk_kyoto::Receiver; +use bdk_kyoto::RejectReason; +use bdk_kyoto::Requester; +use bdk_kyoto::ScanType as WalletScanType; +use bdk_kyoto::UnboundedReceiver; +use bdk_kyoto::UpdateSubscriber; +use bdk_kyoto::WalletExt; +use bdk_kyoto::Warning as Warn; + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use tokio::sync::Mutex; + +use crate::bitcoin::Transaction; +use crate::error::{LightClientBuilderError, LightClientError}; +use crate::wallet::Wallet; +use crate::FeeRate; +use crate::Update; + +const TIMEOUT: u64 = 10; +const DEFAULT_CONNECTIONS: u8 = 2; +const CWD_PATH: &str = "."; + +pub struct LightClient { + pub client: Arc, + pub node: Arc, +} + +pub struct Client { + sender: Arc, + log_rx: Mutex>, + warning_rx: Mutex>, + update_rx: Mutex, +} + +pub struct LightNode { + node: NodeDefault, +} + +impl LightNode { + pub fn run(self: Arc) { + std::thread::spawn(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let _ = self.node.run().await; + }) + }); + } +} + +#[derive(Clone)] +pub struct LightClientBuilder { + connections: u8, + data_dir: Option, + scan_type: ScanType, + peers: Vec, +} + +impl LightClientBuilder { + pub fn new() -> Self { + LightClientBuilder { + connections: DEFAULT_CONNECTIONS, + data_dir: None, + scan_type: ScanType::default(), + peers: Vec::new(), + } + } + + pub fn connections(&self, connections: u8) -> Arc { + Arc::new(LightClientBuilder { + connections, + ..self.clone() + }) + } + + pub fn data_dir(&self, data_dir: String) -> Arc { + Arc::new(LightClientBuilder { + data_dir: Some(data_dir), + ..self.clone() + }) + } + + pub fn scan_type(&self, scan_type: ScanType) -> Arc { + Arc::new(LightClientBuilder { + scan_type, + ..self.clone() + }) + } + + pub fn peers(&self, peers: Vec) -> Arc { + Arc::new(LightClientBuilder { + peers, + ..self.clone() + }) + } + + pub fn build(&self, wallet: &Wallet) -> Result { + let wallet = wallet.get_wallet(); + + let mut trusted_peers = Vec::new(); + for peer in self.peers.iter() { + trusted_peers.push(peer.clone().into()); + } + let path_buf = self + .data_dir + .clone() + .map(|path| PathBuf::from(&path)) + .unwrap_or(PathBuf::from(CWD_PATH)); + + let builder = BDKLightClientBuilder::new() + .connections(self.connections) + .data_dir(path_buf) + .scan_type(self.scan_type.into()) + .timeout_duration(Duration::from_secs(TIMEOUT)) + .peers(trusted_peers); + + let BDKLightClient { + requester, + log_subscriber, + warning_subscriber, + update_subscriber, + node, + } = builder.build(&wallet)?; + + let node = LightNode { node }; + + let client = Client { + sender: Arc::new(requester), + log_rx: Mutex::new(log_subscriber), + warning_rx: Mutex::new(warning_subscriber), + update_rx: Mutex::new(update_subscriber), + }; + + Ok(LightClient { + client: Arc::new(client), + node: Arc::new(node), + }) + } +} + +impl Client { + pub async fn next_log(&self) -> Result { + let mut log_rx = self.log_rx.lock().await; + log_rx + .recv() + .await + .map(|log| log.into()) + .ok_or(LightClientError::NodeStopped) + } + + pub async fn next_warning(&self) -> Result { + let mut warn_rx = self.warning_rx.lock().await; + warn_rx + .recv() + .await + .map(|warn| warn.into()) + .ok_or(LightClientError::NodeStopped) + } + + pub async fn update(&self) -> Option> { + let update = self.update_rx.lock().await.update().await; + update.map(|update| Arc::new(Update(update))) + } + + pub async fn add_revealed_scripts(&self, wallet: &Wallet) -> Result<(), LightClientError> { + let script_iter: Vec = { + let wallet_lock = wallet.get_wallet(); + wallet_lock.peek_revealed_plus_lookahead().collect() + }; + for script in script_iter.into_iter() { + self.sender + .add_script(script) + .await + .map_err(|_| LightClientError::NodeStopped)? + } + Ok(()) + } + + pub async fn broadcast(&self, transaction: &Transaction) -> Result<(), LightClientError> { + let tx = transaction.into(); + self.sender.broadcast_random(tx).await.map_err(From::from) + } + + pub async fn min_broadcast_feerate(&self) -> Result, LightClientError> { + self.sender + .broadcast_min_feerate() + .await + .map_err(|_| LightClientError::NodeStopped) + .map(|fee| Arc::new(FeeRate(fee))) + } + + pub async fn is_running(&self) -> bool { + self.sender.is_running().await + } + + pub async fn shutdown(&self) -> Result<(), LightClientError> { + self.sender.shutdown().await.map_err(From::from) + } +} + +pub enum Log { + Debug { log: String }, + ConnectionsMet, + Progress { progress: f32 }, + StateUpdate { node_state: NodeState }, + TxSent { txid: String }, +} + +impl From for Log { + fn from(value: bdk_kyoto::Log) -> Log { + match value { + bdk_kyoto::Log::Debug(log) => Log::Debug { log }, + bdk_kyoto::Log::ConnectionsMet => Log::ConnectionsMet, + bdk_kyoto::Log::Progress(progress) => Log::Progress { + progress: progress.percentage_complete(), + }, + bdk_kyoto::Log::TxSent(txid) => Log::TxSent { + txid: txid.to_string(), + }, + bdk_kyoto::Log::StateChange(state) => Log::StateUpdate { node_state: state }, + } + } +} + +pub enum Warning { + NeedConnections, + PeerTimedOut, + CouldNotConnect, + NoCompactFilters, + PotentialStaleTip, + UnsolicitedMessage, + InvalidStartHeight, + CorruptedHeaders, + TransactionRejected { + txid: String, + reason: Option, + }, + FailedPersistence { + warning: String, + }, + EvaluatingFork, + EmptyPeerDatabase, + UnexpectedSyncError { + warning: String, + }, + RequestFailed, +} + +impl From for Warning { + fn from(value: Warn) -> Warning { + match value { + Warn::NeedConnections { + connected: _, + required: _, + } => Warning::NeedConnections, + Warn::PeerTimedOut => Warning::PeerTimedOut, + Warn::CouldNotConnect => Warning::CouldNotConnect, + Warn::NoCompactFilters => Warning::NoCompactFilters, + Warn::PotentialStaleTip => Warning::PotentialStaleTip, + Warn::UnsolicitedMessage => Warning::UnsolicitedMessage, + Warn::InvalidStartHeight => Warning::InvalidStartHeight, + Warn::CorruptedHeaders => Warning::CorruptedHeaders, + Warn::TransactionRejected { payload } => { + let reason = payload.reason.map(|r| r.into_string()); + Warning::TransactionRejected { + txid: payload.txid.to_string(), + reason, + } + } + Warn::FailedPersistence { warning } => Warning::FailedPersistence { warning }, + Warn::EvaluatingFork => Warning::EvaluatingFork, + Warn::EmptyPeerDatabase => Warning::EmptyPeerDatabase, + Warn::UnexpectedSyncError { warning } => Warning::UnexpectedSyncError { warning }, + Warn::ChannelDropped => Warning::RequestFailed, + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub enum ScanType { + New, + #[default] + Sync, + Recovery { + from_height: u32, + }, +} + +impl From for WalletScanType { + fn from(value: ScanType) -> Self { + match value { + ScanType::New => WalletScanType::New, + ScanType::Recovery { from_height } => WalletScanType::Recovery { from_height }, + ScanType::Sync => WalletScanType::Sync, + } + } +} + +#[derive(Clone)] +pub struct Peer { + pub address: Arc, + pub port: Option, + pub v2_transport: bool, +} + +pub struct IpAddress { + inner: IpAddr, +} + +impl IpAddress { + pub fn from_ipv4(q1: u8, q2: u8, q3: u8, q4: u8) -> Self { + Self { + inner: IpAddr::V4(Ipv4Addr::new(q1, q2, q3, q4)), + } + } + + #[allow(clippy::too_many_arguments)] + pub fn from_ipv6(a: u16, b: u16, c: u16, d: u16, e: u16, f: u16, g: u16, h: u16) -> Self { + Self { + inner: IpAddr::V6(Ipv6Addr::new(a, b, c, d, e, f, g, h)), + } + } +} + +impl From for TrustedPeer { + fn from(peer: Peer) -> Self { + let services = if peer.v2_transport { + let mut services = ServiceFlags::P2P_V2; + services.add(ServiceFlags::NETWORK); + services.add(ServiceFlags::COMPACT_FILTERS); + services + } else { + let mut services = ServiceFlags::COMPACT_FILTERS; + services.add(ServiceFlags::NETWORK); + services + }; + let addr_v2 = match peer.address.inner { + IpAddr::V4(ipv4_addr) => AddrV2::Ipv4(ipv4_addr), + IpAddr::V6(ipv6_addr) => AddrV2::Ipv6(ipv6_addr), + }; + TrustedPeer::new(addr_v2, peer.port, services) + } +} + +trait DisplayExt { + fn into_string(self) -> String; +} + +impl DisplayExt for RejectReason { + fn into_string(self) -> String { + let message = match self { + RejectReason::Malformed => "Message could not be decoded.", + RejectReason::Invalid => "Transaction was invalid for some reason.", + RejectReason::Obsolete => "Client version is no longer supported.", + RejectReason::Duplicate => "Duplicate version message received.", + RejectReason::NonStandard => "Transaction was nonstandard.", + RejectReason::Dust => "One or more outputs are below the dust threshold.", + RejectReason::Fee => "Transaction does not have enough fee to be mined.", + RejectReason::Checkpoint => "Inconsistent with compiled checkpoint.", + }; + message.into() + } +} diff --git a/bdk-ffi/src/lib.rs b/bdk-ffi/src/lib.rs index 2da0ec05..705f9851 100644 --- a/bdk-ffi/src/lib.rs +++ b/bdk-ffi/src/lib.rs @@ -4,6 +4,7 @@ mod electrum; mod error; mod esplora; mod keys; +mod kyoto; mod macros; mod store; mod tx_builder; @@ -41,6 +42,8 @@ use crate::error::EsploraError; use crate::error::ExtractTxError; use crate::error::FeeRateError; use crate::error::FromScriptError; +use crate::error::LightClientBuilderError; +use crate::error::LightClientError; use crate::error::LoadWithPersistError; use crate::error::MiniscriptError; use crate::error::ParseAmountError; @@ -95,4 +98,15 @@ use bdk_wallet::tx_builder::ChangeSpendPolicy; use bdk_wallet::ChangeSet; use bdk_wallet::KeychainKind; +use bdk_kyoto::NodeState; +use kyoto::Client; +use kyoto::IpAddress; +use kyoto::LightClient; +use kyoto::LightClientBuilder; +use kyoto::LightNode; +use kyoto::Log; +use kyoto::Peer; +use kyoto::ScanType; +use kyoto::Warning; + uniffi::include_scaffolding!("bdk"); diff --git a/bdk-jvm/lib/build.gradle.kts b/bdk-jvm/lib/build.gradle.kts index c1860daa..a9f89947 100644 --- a/bdk-jvm/lib/build.gradle.kts +++ b/bdk-jvm/lib/build.gradle.kts @@ -39,6 +39,7 @@ tasks.test { exclude("**/LiveTransactionTest.class") exclude("**/LiveTxBuilderTest.class") exclude("**/LiveWalletTest.class") + exclude("**/LiveKyotoTest.class") } } @@ -63,6 +64,7 @@ tasks.withType { dependencies { implementation(platform("org.jetbrains.kotlin:kotlin-bom")) implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("net.java.dev.jna:jna:5.14.0") api("org.slf4j:slf4j-api:1.7.30") diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt new file mode 100644 index 00000000..a5667990 --- /dev/null +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt @@ -0,0 +1,62 @@ +package org.bitcoindevkit + +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlin.test.Test +import kotlin.test.AfterTest +import kotlin.test.assertNotNull + +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.deleteRecursively + +class LiveKyotoTest { + private val descriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", Network.SIGNET) + private val changeDescriptor: Descriptor = Descriptor("wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", Network.SIGNET) + private val ip: IpAddress = IpAddress.fromIpv4(68u, 47u, 229u, 218u) + private val peer: Peer = Peer(ip, null, false) + private val currentPath = Paths.get(".").toAbsolutePath().normalize() + private val persistenceFilePath = Files.createTempDirectory(currentPath, "tempDirPrefix_") + + @OptIn(ExperimentalPathApi::class) + @AfterTest + fun cleanup() { + persistenceFilePath.deleteRecursively() + } + + @Test + fun testKyoto() { + val conn: Connection = Connection.newInMemory() + val wallet: Wallet = Wallet(descriptor, changeDescriptor, Network.SIGNET, conn) + val peers = listOf(peer) + runBlocking { + val lightClient = LightClientBuilder() + .peers(peers) + .connections(1u) + .scanType(ScanType.New) + .dataDir(persistenceFilePath.toString()) + .build(wallet) + val client = lightClient.client + val node = lightClient.node + println("Node running") + val logJob = launch { + while (true) { + val log = client.nextLog() + println("$log") + } + } + node.run() + val updateOpt: Update? = client.update() + val update = assertNotNull(updateOpt) + wallet.applyUpdate(update) + assert(wallet.balance().total.toSat() > 0uL) { + "Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again." + } + logJob.cancelAndJoin() + client.shutdown() + println("Test completed successfully") + } + } +} diff --git a/bdk-python/tests/test_live_kyoto.py b/bdk-python/tests/test_live_kyoto.py new file mode 100644 index 00000000..44fe6f55 --- /dev/null +++ b/bdk-python/tests/test_live_kyoto.py @@ -0,0 +1,55 @@ +from bdkpython import * +import unittest +import os +import asyncio + +network: Network = Network.SIGNET + +ip: IpAddress = IpAddress.from_ipv4(68, 47, 229, 218) +peer: Peer = Peer(address=ip, port=None, v2_transport=False) + +descriptor: Descriptor = Descriptor( + "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", + network=network +) +change_descriptor: Descriptor = Descriptor( + "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", + network=network +) + +class LiveKyotoTest(unittest.IsolatedAsyncioTestCase): + + def tearDown(self) -> None: + if os.path.exists("./bdk_persistence.sqlite"): + os.remove("./bdk_persistence.sqlite") + if os.path.exists("./data/signet/headers.db"): + os.remove("./data/signet/headers.db") + if os.path.exists("./data/signet/peers.db"): + os.remove("./data/signet/peers.db") + + async def testKyoto(self) -> None: + connection: Connection = Connection.new_in_memory() + wallet: Wallet = Wallet( + descriptor, + change_descriptor, + network, + connection + ) + peers = [peer] + light_client: LightClient = LightClientBuilder().scan_type(ScanType.NEW()).peers(peers).connections(1).build(wallet) + client: Client = light_client.client + node: LightNode = light_client.node + node.run() + update: Update = await client.update() + self.assertIsNotNone(update, "Update is None. This should not be possible.") + wallet.apply_update(update) + self.assertGreater( + wallet.balance().total.to_sat(), + 0, + f"Wallet balance must be greater than 0! Please send funds to {wallet.reveal_next_address(KeychainKind.EXTERNAL).address} and try again." + ) + log_task.cancel() + await client.shutdown() + +if __name__ == "__main__": + unittest.main() diff --git a/bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift new file mode 100644 index 00000000..cc993a61 --- /dev/null +++ b/bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift @@ -0,0 +1,63 @@ +import XCTest +@testable import BitcoinDevKit + +final class LiveKyotoTests: XCTestCase { + private let descriptor = try! Descriptor( + descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", + network: Network.signet + ) + private let changeDescriptor = try! Descriptor( + descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)", + network: Network.signet + ) + private let peer = IpAddress.fromIpv4(q1: 68, q2: 47, q3: 229, q4: 218) + private let cwd = FileManager.default.currentDirectoryPath.appending("/temp") + + override func tearDownWithError() throws { + let fileManager = FileManager.default + if fileManager.fileExists(atPath: cwd) { + try fileManager.removeItem(atPath: cwd) + } + } + + func testSuccessfullySyncs() async throws { + let connection = try Connection.newInMemory() + let wallet = try Wallet( + descriptor: descriptor, + changeDescriptor: changeDescriptor, + network: Network.signet, + connection: connection + ) + let trustedPeer = Peer(address: peer, port: nil, v2Transport: false) + let lightClient = try LightClientBuilder() + .peers(peers: [trustedPeer]) + .connections(connections: 1) + .scanType(scanType: ScanType.new) + .dataDir(dataDir: cwd) + .build(wallet: wallet) + let client = lightClient.client + let node = lightClient.node + node.run() + Task { + while true { + if let log = try? await client.nextLog() { + print("\(log)") + } + } + } + let update = await client.update() + if let update = update { + try wallet.applyUpdate(update: update) + let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description + XCTAssertGreaterThan( + wallet.balance().total.toSat(), + UInt64(0), + "Wallet must have positive balance, please send funds to \(address)" + ) + print("Update applied correctly") + try await client.shutdown() + } else { + print("Update is nil. Ensure this test is ran infrequently.") + } + } +}