From 6e4a03f1089fe1666c1acd572edea3ea3608f456 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 11 Mar 2026 11:59:45 -0500 Subject: [PATCH 01/15] chore: align bdk-ffi with bdk_wallet 3.0.0-rc.1 --- bdk-ffi/Cargo.lock | 8 +- bdk-ffi/Cargo.toml | 6 +- bdk-ffi/src/descriptor.rs | 31 +-- bdk-ffi/src/error.rs | 2 +- bdk-ffi/src/keys.rs | 2 +- bdk-ffi/src/types.rs | 3 +- bdk-ffi/src/wallet.rs | 7 - vendor/bdk_kyoto/.github/workflows/ci.yml | 36 +++ vendor/bdk_kyoto/Cargo.toml | 35 +++ vendor/bdk_kyoto/LICENSE | 7 + vendor/bdk_kyoto/LICENSE-APACHE | 201 ++++++++++++++++ vendor/bdk_kyoto/LICENSE-MIT | 16 ++ vendor/bdk_kyoto/README.md | 19 ++ vendor/bdk_kyoto/src/builder.rs | 152 ++++++++++++ vendor/bdk_kyoto/src/lib.rs | 267 ++++++++++++++++++++++ 15 files changed, 761 insertions(+), 31 deletions(-) create mode 100644 vendor/bdk_kyoto/.github/workflows/ci.yml create mode 100644 vendor/bdk_kyoto/Cargo.toml create mode 100644 vendor/bdk_kyoto/LICENSE create mode 100644 vendor/bdk_kyoto/LICENSE-APACHE create mode 100644 vendor/bdk_kyoto/LICENSE-MIT create mode 100644 vendor/bdk_kyoto/README.md create mode 100644 vendor/bdk_kyoto/src/builder.rs create mode 100644 vendor/bdk_kyoto/src/lib.rs diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index 1c534061..f5b8114b 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 = "3.0.0-rc.1" dependencies = [ "assert_matches", "bdk_electrum", @@ -177,8 +177,6 @@ dependencies = [ [[package]] name = "bdk_kyoto" version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f35b9f8b3aa8c4647bec7a92b050c496742d955e0ac1edcb4e7c2deabf63c54" dependencies = [ "bdk_wallet", "bip157", @@ -186,9 +184,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "2.3.0" +version = "3.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03f1e31ccc562f600981f747d2262b84428cbff52c9c9cdf14d15fb15bd2286" +checksum = "70031c96c223c780525f4b9c52813e4f5bd8000a5712aac3248d00838af1e436" dependencies = [ "bdk_chain", "bip39", diff --git a/bdk-ffi/Cargo.toml b/bdk-ffi/Cargo.toml index 662ffe67..017033f5 100644 --- a/bdk-ffi/Cargo.toml +++ b/bdk-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk-ffi" -version = "2.4.0-alpha.0" +version = "3.0.0-rc.1" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" edition = "2018" @@ -15,10 +15,10 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" [dependencies] -bdk_wallet = { version = "2.3.0", features = ["all-keys", "keys-bip39", "rusqlite"] } +bdk_wallet = { version = "=3.0.0-rc.1", 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 = { path = "../vendor/bdk_kyoto" } uniffi = { version = "=0.30.0", features = ["cli"]} thiserror = "2.0.17" diff --git a/bdk-ffi/src/descriptor.rs b/bdk-ffi/src/descriptor.rs index db705391..f4e84f89 100644 --- a/bdk-ffi/src/descriptor.rs +++ b/bdk-ffi/src/descriptor.rs @@ -38,7 +38,8 @@ impl Descriptor { #[uniffi::constructor] pub fn new(descriptor: String, network: Network) -> Result { let secp = Secp256k1::new(); - let (extended_descriptor, key_map) = descriptor.into_wallet_descriptor(&secp, network)?; + let (extended_descriptor, key_map) = + descriptor.into_wallet_descriptor(&secp, network.into())?; Ok(Self { extended_descriptor, key_map, @@ -60,8 +61,9 @@ impl Descriptor { } BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { let derivable_key = descriptor_x_key.xkey; - let (extended_descriptor, key_map, _) = - Bip44(derivable_key, keychain_kind).build(network).unwrap(); + let (extended_descriptor, key_map, _) = Bip44(derivable_key, keychain_kind) + .build(network.into()) + .unwrap(); Self { extended_descriptor, key_map, @@ -96,7 +98,7 @@ impl Descriptor { let derivable_key = descriptor_x_key.xkey; let (extended_descriptor, key_map, _) = Bip44Public(derivable_key, fingerprint, keychain_kind) - .build(network) + .build(network.into()) .map_err(DescriptorError::from)?; Ok(Self { @@ -125,8 +127,9 @@ impl Descriptor { } BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { let derivable_key = descriptor_x_key.xkey; - let (extended_descriptor, key_map, _) = - Bip49(derivable_key, keychain_kind).build(network).unwrap(); + let (extended_descriptor, key_map, _) = Bip49(derivable_key, keychain_kind) + .build(network.into()) + .unwrap(); Self { extended_descriptor, key_map, @@ -161,7 +164,7 @@ impl Descriptor { let derivable_key = descriptor_x_key.xkey; let (extended_descriptor, key_map, _) = Bip49Public(derivable_key, fingerprint, keychain_kind) - .build(network) + .build(network.into()) .map_err(DescriptorError::from)?; Ok(Self { @@ -190,8 +193,9 @@ impl Descriptor { } BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { let derivable_key = descriptor_x_key.xkey; - let (extended_descriptor, key_map, _) = - Bip84(derivable_key, keychain_kind).build(network).unwrap(); + let (extended_descriptor, key_map, _) = Bip84(derivable_key, keychain_kind) + .build(network.into()) + .unwrap(); Self { extended_descriptor, key_map, @@ -226,7 +230,7 @@ impl Descriptor { let derivable_key = descriptor_x_key.xkey; let (extended_descriptor, key_map, _) = Bip84Public(derivable_key, fingerprint, keychain_kind) - .build(network) + .build(network.into()) .map_err(DescriptorError::from)?; Ok(Self { @@ -255,8 +259,9 @@ impl Descriptor { } BdkDescriptorSecretKey::XPrv(descriptor_x_key) => { let derivable_key = descriptor_x_key.xkey; - let (extended_descriptor, key_map, _) = - Bip86(derivable_key, keychain_kind).build(network).unwrap(); + let (extended_descriptor, key_map, _) = Bip86(derivable_key, keychain_kind) + .build(network.into()) + .unwrap(); Self { extended_descriptor, key_map, @@ -291,7 +296,7 @@ impl Descriptor { let derivable_key = descriptor_x_key.xkey; let (extended_descriptor, key_map, _) = Bip86Public(derivable_key, fingerprint, keychain_kind) - .build(network) + .build(network.into()) .map_err(DescriptorError::from)?; Ok(Self { diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index 10b6d8b1..f64a614b 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -1561,7 +1561,7 @@ impl From for SignerError { BdkSignerError::MissingKey => SignerError::MissingKey, BdkSignerError::InvalidKey => SignerError::InvalidKey, BdkSignerError::UserCanceled => SignerError::UserCanceled, - BdkSignerError::InputIndexOutOfRange => SignerError::InputIndexOutOfRange, + BdkSignerError::InputIndexOutOfRange(_) => SignerError::InputIndexOutOfRange, BdkSignerError::MissingNonWitnessUtxo => SignerError::MissingNonWitnessUtxo, BdkSignerError::InvalidNonWitnessUtxo => SignerError::InvalidNonWitnessUtxo, BdkSignerError::MissingWitnessUtxo => SignerError::MissingWitnessUtxo, diff --git a/bdk-ffi/src/keys.rs b/bdk-ffi/src/keys.rs index 34bdf0b1..0cc904a3 100644 --- a/bdk-ffi/src/keys.rs +++ b/bdk-ffi/src/keys.rs @@ -147,7 +147,7 @@ impl DescriptorSecretKey { let xkey: ExtendedKey = (mnemonic, password).into_extended_key().unwrap(); let descriptor_secret_key = BdkDescriptorSecretKey::XPrv(DescriptorXKey { origin: None, - xkey: xkey.into_xprv(network).unwrap(), + xkey: xkey.into_xprv(network.into()).unwrap(), derivation_path: BdkDerivationPath::master(), wildcard: Wildcard::Unhardened, }); diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index f432988a..903f2917 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -23,13 +23,13 @@ use bdk_wallet::descriptor::policy::{ Condition as BdkCondition, PkOrF as BdkPkOrF, Policy as BdkPolicy, Satisfaction as BdkSatisfaction, SatisfiableItem as BdkSatisfiableItem, }; -use bdk_wallet::event::WalletEvent as BdkWalletEvent; #[allow(deprecated)] use bdk_wallet::signer::{SignOptions as BdkSignOptions, TapLeavesOptions}; use bdk_wallet::AddressInfo as BdkAddressInfo; use bdk_wallet::Balance as BdkBalance; use bdk_wallet::LocalOutput as BdkLocalOutput; use bdk_wallet::Update as BdkUpdate; +use bdk_wallet::WalletEvent as BdkWalletEvent; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::convert::TryFrom; @@ -1314,6 +1314,7 @@ impl From for bdk_wallet::ChangeSet { local_chain, tx_graph, indexer, + locked_outpoints: Default::default(), } } } diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 47da506c..17b01319 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -205,13 +205,6 @@ impl Wallet { }) } - /// Informs the wallet that you no longer intend to broadcast a tx that was built from it. - /// - /// This frees up the change address used when creating the tx for use in future transactions. - pub fn cancel_tx(&self, tx: &Transaction) { - self.get_wallet().cancel_tx(&tx.into()) - } - /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the /// wallet's database. pub fn get_utxo(&self, op: OutPoint) -> Option { diff --git a/vendor/bdk_kyoto/.github/workflows/ci.yml b/vendor/bdk_kyoto/.github/workflows/ci.yml new file mode 100644 index 00000000..f37a1ea7 --- /dev/null +++ b/vendor/bdk_kyoto/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: Build & Test + +on: + push: + branches: + - master + pull_request: + +jobs: + check: + strategy: + matrix: + toolchain: [stable, beta, nightly] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v3 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + components: clippy,rustfmt + - run: just check + integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v3 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: just test integration + msrv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v3 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: just test msrv diff --git a/vendor/bdk_kyoto/Cargo.toml b/vendor/bdk_kyoto/Cargo.toml new file mode 100644 index 00000000..e8755b23 --- /dev/null +++ b/vendor/bdk_kyoto/Cargo.toml @@ -0,0 +1,35 @@ +[package] +edition = "2021" +rust-version = "1.84.0" +name = "bdk_kyoto" +version = "0.15.4" +authors = [ + "Rob rob@2140.dev", + "Bitcoin Dev Kit Developers", +] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "BDK blockchain integration using P2P light client Kyoto" +documentation = "https://docs.rs/bdk-kyoto" +readme = "README.md" +keywords = [ + "bitcoin", + "peer-to-peer", + "light-client", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/bitcoindevkit/bdk-kyoto" + +[lib] +name = "bdk_kyoto" +path = "src/lib.rs" + +[dependencies.bdk_wallet] +version = "3.0.0-rc.1" + +[dependencies.bip157] +version = "0.3.4" diff --git a/vendor/bdk_kyoto/LICENSE b/vendor/bdk_kyoto/LICENSE new file mode 100644 index 00000000..313ad056 --- /dev/null +++ b/vendor/bdk_kyoto/LICENSE @@ -0,0 +1,7 @@ +This software is licensed under Apache 2.0 or MIT, at your option. + +Some files retain their own copyright notice, however, for full authorship information, see version control history. + +Except as otherwise noted in individual files, all files in this repository are licensed under the Apache License, Version 2.0 or the MIT license , at your option. + +You may not use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of this software or any files in this repository except in accordance with one or both of these licenses. \ No newline at end of file diff --git a/vendor/bdk_kyoto/LICENSE-APACHE b/vendor/bdk_kyoto/LICENSE-APACHE new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/vendor/bdk_kyoto/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/vendor/bdk_kyoto/LICENSE-MIT b/vendor/bdk_kyoto/LICENSE-MIT new file mode 100644 index 00000000..f8da085d --- /dev/null +++ b/vendor/bdk_kyoto/LICENSE-MIT @@ -0,0 +1,16 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/bdk_kyoto/README.md b/vendor/bdk_kyoto/README.md new file mode 100644 index 00000000..f4d62499 --- /dev/null +++ b/vendor/bdk_kyoto/README.md @@ -0,0 +1,19 @@ +# BDK Kyoto + +BDK-Kyoto is an extension of [Kyoto](https://github.com/2140-dev/kyoto), a client-side implementation of BIP157/BIP158. +These proposals define a way for users to fetch transactions privately, using _compact block filters_. +You may want to read the specification [here](https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki). +Kyoto runs as a psuedo-node, sending messages over the Bitcoin peer-to-peer layer, finding new peers to connect to, and managing a +light-weight database of Bitcoin block headers. As such, developing a wallet application using this crate is distinct from a typical +client/server relationship. Esplora and Electrum offer _proactive_ APIs, in that the servers will respond to events as they are requested. + +In the case of running a node as a background process, the developer experience is far more _reactive_, in that the node may emit any number of events, and the application may respond to them. BDK-Kyoto curates these events into structures that are easily handled by BDK APIs, making integration of compact block filters easily understood. + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. diff --git a/vendor/bdk_kyoto/src/builder.rs b/vendor/bdk_kyoto/src/builder.rs new file mode 100644 index 00000000..dcbe85f8 --- /dev/null +++ b/vendor/bdk_kyoto/src/builder.rs @@ -0,0 +1,152 @@ +//! Construct a [`LightClient`] by using a reference to a [`Wallet`]. +//! +//! ## Details +//! +//! The node has a number of configurations. Notably, the height of in the blockchain to start a +//! wallet recovery and the nodes on the peer-to-peer network are both configurable. +//! +//! ```no_run +//! # const RECEIVE: &str = "tr([7d94197e/86'/1'/0']tpubDCyQVJj8KzjiQsFjmb3KwECVXPvMwvAxxZGCP9XmWSopmjW3bCV3wD7TgxrUhiGSueDS1MU5X1Vb1YjYcp8jitXc5fXfdC1z68hDDEyKRNr/0/*)"; +//! # const CHANGE: &str = "tr([7d94197e/86'/1'/0']tpubDCyQVJj8KzjiQsFjmb3KwECVXPvMwvAxxZGCP9XmWSopmjW3bCV3wD7TgxrUhiGSueDS1MU5X1Vb1YjYcp8jitXc5fXfdC1z68hDDEyKRNr/1/*)"; +//! use std::net::{IpAddr, Ipv4Addr}; +//! use std::path::PathBuf; +//! use std::time::Duration; +//! use bdk_wallet::Wallet; +//! use bdk_kyoto::bip157::{Network, TrustedPeer}; +//! use bdk_kyoto::builder::{Builder, BuilderExt}; +//! use bdk_kyoto::{LightClient, ScanType}; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! // Add specific peers to connect to. +//! let peer = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); +//! let trusted = TrustedPeer::from_ip(peer); +//! +//! let db_path = ".".parse::()?; +//! +//! let mut wallet = Wallet::create(RECEIVE, CHANGE) +//! .network(Network::Signet) +//! .create_wallet_no_persist()?; +//! +//! let scan_type = ScanType::Sync; +//! +//! let LightClient { +//! requester, +//! info_subscriber, +//! warning_subscriber, +//! update_subscriber, +//! node +//! } = Builder::new(Network::Signet) +//! // A node may handle multiple connections +//! .required_peers(2) +//! // Choose where to store node data +//! .data_dir(db_path) +//! // How long peers have to respond messages +//! .response_timeout(Duration::from_secs(2)) +//! // Added trusted peers to initialize the sync +//! .add_peer(trusted) +//! .build_with_wallet(&wallet, scan_type)?; +//! Ok(()) +//! } +//! ``` + +use std::fmt::Display; + +use bdk_wallet::{ + chain::{CheckPoint, IndexedTxGraph}, + Wallet, +}; +pub use bip157::Builder; +use bip157::{chain::ChainState, HeaderCheckpoint}; + +use crate::{LightClient, ScanType, UpdateSubscriber}; + +const IMPOSSIBLE_REORG_DEPTH: usize = 7; + +/// Build a compact block filter client and node for a specified wallet +pub trait BuilderExt { + /// Attempt to build the node with scripts from a [`Wallet`] and following a [`ScanType`]. + fn build_with_wallet( + self, + wallet: &Wallet, + scan_type: ScanType, + ) -> Result; +} + +impl BuilderExt for Builder { + fn build_with_wallet( + mut self, + wallet: &Wallet, + scan_type: ScanType, + ) -> Result { + let network = wallet.network(); + if self.network().ne(&network) { + return Err(BuilderError::NetworkMismatch); + } + match scan_type { + ScanType::Sync => { + let current_cp = wallet.latest_checkpoint(); + let sync_start = walk_back_max_reorg(current_cp); + self = self.chain_state(ChainState::Checkpoint(sync_start)); + } + ScanType::Recovery { + used_script_index: _, + checkpoint, + } => self = self.chain_state(ChainState::Checkpoint(checkpoint)), + } + let (node, client) = self.build(); + let bip157::Client { + requester, + info_rx, + warn_rx, + event_rx, + } = client; + let indexed_graph = IndexedTxGraph::new(wallet.spk_index().clone()); + let update_subscriber = UpdateSubscriber::new( + requester.clone(), + scan_type, + event_rx, + wallet.latest_checkpoint(), + indexed_graph, + ); + Ok(LightClient { + requester, + info_subscriber: info_rx, + warning_subscriber: warn_rx, + update_subscriber, + node, + }) + } +} + +/// Walk back 7 blocks in case the last sync was an orphan block. +fn walk_back_max_reorg(checkpoint: CheckPoint) -> HeaderCheckpoint { + let mut ret_cp = HeaderCheckpoint::new(checkpoint.height(), checkpoint.hash()); + let cp_iter = checkpoint.iter(); + for (index, next) in cp_iter.enumerate() { + if index > IMPOSSIBLE_REORG_DEPTH { + return ret_cp; + } + ret_cp = HeaderCheckpoint::new(next.height(), next.hash()); + } + ret_cp +} + +/// Errors the builder may throw. +#[derive(Debug)] +pub enum BuilderError { + /// The wallet network and node network do not match. + NetworkMismatch, +} + +impl Display for BuilderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BuilderError::NetworkMismatch => { + write!(f, "wallet network and node network do not match") + } + } + } +} + +impl std::error::Error for BuilderError {} diff --git a/vendor/bdk_kyoto/src/lib.rs b/vendor/bdk_kyoto/src/lib.rs new file mode 100644 index 00000000..2721a996 --- /dev/null +++ b/vendor/bdk_kyoto/src/lib.rs @@ -0,0 +1,267 @@ +#![doc = include_str!("../README.md")] +//! ## Examples +//! +//! If you have an existing project that leverages `bdk_wallet`, building the compact block filter +//! _node_ and _client_ is simple. You may construct and configure a node to integrate with your +//! wallet by using the [`BuilderExt`](crate::builder) and [`Builder`](crate::builder). +//! +//! ```no_run +//! # const RECEIVE: &str = "tr([7d94197e/86'/1'/0']tpubDCyQVJj8KzjiQsFjmb3KwECVXPvMwvAxxZGCP9XmWSopmjW3bCV3wD7TgxrUhiGSueDS1MU5X1Vb1YjYcp8jitXc5fXfdC1z68hDDEyKRNr/0/*)"; +//! # const CHANGE: &str = "tr([7d94197e/86'/1'/0']tpubDCyQVJj8KzjiQsFjmb3KwECVXPvMwvAxxZGCP9XmWSopmjW3bCV3wD7TgxrUhiGSueDS1MU5X1Vb1YjYcp8jitXc5fXfdC1z68hDDEyKRNr/1/*)"; +//! use bdk_wallet::Wallet; +//! use bdk_wallet::bitcoin::Network; +//! use bdk_kyoto::builder::{Builder, BuilderExt}; +//! use bdk_kyoto::{LightClient, ScanType}; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! let mut wallet = Wallet::create(RECEIVE, CHANGE) +//! .network(Network::Signet) +//! .create_wallet_no_persist()?; +//! +//! let LightClient { +//! requester, +//! info_subscriber: _, +//! warning_subscriber: _, +//! mut update_subscriber, +//! node +//! } = Builder::new(Network::Signet).build_with_wallet(&wallet, ScanType::Sync)?; +//! +//! tokio::task::spawn(async move { node.run().await }); +//! +//! loop { +//! let update = update_subscriber.update().await?; +//! wallet.apply_update(update)?; +//! return Ok(()); +//! } +//! } +//! ``` + +#![warn(missing_docs)] +use std::collections::HashSet; + +use bdk_wallet::chain::BlockId; +use bdk_wallet::chain::CheckPoint; +pub use bdk_wallet::Update; + +use bdk_wallet::chain::{keychain_txout::KeychainTxOutIndex, IndexedTxGraph}; +use bdk_wallet::chain::{ConfirmationBlockTime, TxUpdate}; +use bdk_wallet::KeychainKind; + +pub extern crate bip157; + +use bip157::chain::BlockHeaderChanges; +use bip157::ScriptBuf; +#[doc(inline)] +pub use bip157::{ + BlockHash, ClientError, FeeRate, HeaderCheckpoint, Info, Node, RejectPayload, RejectReason, + Requester, TrustedPeer, TxBroadcast, TxBroadcastPolicy, Warning, Wtxid, +}; +use bip157::{Event, SyncUpdate}; + +#[doc(inline)] +pub use bip157::Receiver; +#[doc(inline)] +pub use bip157::UnboundedReceiver; + +#[doc(inline)] +pub use builder::BuilderExt; +pub mod builder; + +#[derive(Debug)] +/// A node and associated structs to send and receive events to and from the node. +pub struct LightClient { + /// Send events to a running node (i.e. broadcast a transaction). + pub requester: Requester, + /// Receive informational messages as the node runs. + pub info_subscriber: Receiver, + /// Receive warnings from the node as it runs. + pub warning_subscriber: UnboundedReceiver, + /// Receive wallet updates from a node. + pub update_subscriber: UpdateSubscriber, + /// The underlying node that must be run to fetch blocks from peers. + pub node: Node, +} + +/// Interpret events from a node that is running to apply +/// updates to an underlying wallet. +#[derive(Debug)] +pub struct UpdateSubscriber { + // request information from the client + requester: Requester, + // channel receiver + receiver: UnboundedReceiver, + // changes to local chain + cp: CheckPoint, + // receive graph + graph: IndexedTxGraph>, + // queued blocks to fetch + queued_blocks: Vec, + // queued scripts to check filters + spk_cache: HashSet, +} + +impl UpdateSubscriber { + fn new( + requester: Requester, + scan_type: ScanType, + receiver: UnboundedReceiver, + cp: CheckPoint, + graph: IndexedTxGraph>, + ) -> Self { + let spk_cache = match scan_type { + ScanType::Sync => Self::peek_scripts(&graph.index, graph.index.lookahead()), + ScanType::Recovery { + used_script_index, + checkpoint: _, + } => Self::peek_scripts(&graph.index, used_script_index), + }; + Self { + requester, + receiver, + cp, + graph, + queued_blocks: Vec::new(), + spk_cache, + } + } + /// Return the most recent update from the node once it has synced to the network's tip. + /// This may take a significant portion of time during wallet recoveries or dormant wallets. + /// Note that you may call this method in a loop as long as the node is running. + /// + /// **Warning** + /// + /// This method is _not_ cancel safe. You cannot use it within a `tokio::select` arm. + pub async fn update(&mut self) -> Result { + let mut cp = self.cp.clone(); + while let Some(message) = self.receiver.recv().await { + match message { + Event::IndexedFilter(filter) => { + let block_hash = filter.block_hash(); + if filter.contains_any(self.spk_cache.iter()) { + self.queued_blocks.push(block_hash); + } + } + // these are emitted for every block + Event::ChainUpdate(BlockHeaderChanges::Connected(at)) => { + let block_id = BlockId { + hash: at.block_hash(), + height: at.height, + }; + cp = cp.insert(block_id); + } + Event::ChainUpdate(BlockHeaderChanges::Reorganized { + accepted, + reorganized: _, + }) => { + for header in accepted { + let block_id = BlockId { + hash: header.block_hash(), + height: header.height, + }; + cp = cp.insert(block_id); + } + } + Event::FiltersSynced(SyncUpdate { + tip: _, + recent_history: _, + }) => { + for hash in core::mem::take(&mut self.queued_blocks) { + let block = self + .requester + .get_block(hash) + .await + .map_err(|_| UpdateError::NodeStopped)?; + let height = block.height; + let block = block.block; + let _ = self.graph.apply_block_relevant(&block, height); + } + self.cp = cp; + self.spk_cache.extend(Self::peek_scripts( + &self.graph.index, + self.graph.index.lookahead(), + )); + return Ok(self.get_scan_response()); + } + _ => (), + } + } + Err(UpdateError::NodeStopped) + } + + // When the client is believed to have synced to the chain tip of most work, + // we can return a wallet update. + fn get_scan_response(&mut self) -> Update { + let tx_update = TxUpdate::from(self.graph.graph().clone()); + let graph = core::mem::take(&mut self.graph); + let last_active_indices = graph.index.last_used_indices(); + self.graph = IndexedTxGraph::new(graph.index); + Update { + tx_update, + last_active_indices, + chain: Some(self.cp.clone()), + } + } + + fn peek_scripts( + keychain: &KeychainTxOutIndex, + to_index: u32, + ) -> HashSet { + let mut spk_cache = HashSet::new(); + // We pre-compute an SPK cache so as to not call `unbounded_spk_iter` for each filter + let last_revealed = keychain.last_revealed_indices(); + let ext_index = last_revealed + .get(&KeychainKind::External) + .copied() + .unwrap_or(0); + let unbounded_ext_spk_iter = keychain + .unbounded_spk_iter(KeychainKind::External) + .expect("wallet must have external keychain"); + let bound = (ext_index + to_index) as usize; + let bounded_ext_iter = unbounded_ext_spk_iter.take(bound).map(|(_, script)| script); + spk_cache.extend(bounded_ext_iter); + let int_index = last_revealed + .get(&KeychainKind::Internal) + .copied() + .unwrap_or(0); + let unbounded_int_spk_iter = keychain.unbounded_spk_iter(KeychainKind::Internal); + if let Some(int_spk_iter) = unbounded_int_spk_iter { + let bound = (int_index + to_index) as usize; + let bounded_int_iter = int_spk_iter.take(bound).map(|(_, script)| script); + spk_cache.extend(bounded_int_iter); + } + spk_cache + } +} + +/// Errors encountered when attempting to construct a wallet update. +#[derive(Debug, Clone, Copy)] +pub enum UpdateError { + /// The node has stopped running. + NodeStopped, +} + +impl std::fmt::Display for UpdateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UpdateError::NodeStopped => write!(f, "the node halted execution."), + } + } +} + +impl std::error::Error for UpdateError {} + +/// How to scan compact block filters on start up. +#[derive(Debug, Clone, Copy, Default)] +pub enum ScanType { + /// Sync the wallet from the last known wallet checkpoint to the rest of the network. + #[default] + Sync, + /// Recover an old wallet by scanning after the specified height. + Recovery { + /// The amount of scripts used by the wallet that is being recovered. + used_script_index: u32, + /// The height in the block chain to begin searching for transactions. + checkpoint: HeaderCheckpoint, + }, +} From 8c4248f56a315a49af7dd25b458d132d46af12f5 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 11 Mar 2026 13:43:11 -0500 Subject: [PATCH 02/15] chore: replace vendored kyoto with kyoto rc1 --- bdk-ffi/Cargo.lock | 32 ++- bdk-ffi/Cargo.toml | 2 +- bdk-ffi/src/kyoto.rs | 24 +- vendor/bdk_kyoto/.github/workflows/ci.yml | 36 --- vendor/bdk_kyoto/Cargo.toml | 35 --- vendor/bdk_kyoto/LICENSE | 7 - vendor/bdk_kyoto/LICENSE-APACHE | 201 ---------------- vendor/bdk_kyoto/LICENSE-MIT | 16 -- vendor/bdk_kyoto/README.md | 19 -- vendor/bdk_kyoto/src/builder.rs | 152 ------------ vendor/bdk_kyoto/src/lib.rs | 267 ---------------------- 11 files changed, 41 insertions(+), 750 deletions(-) delete mode 100644 vendor/bdk_kyoto/.github/workflows/ci.yml delete mode 100644 vendor/bdk_kyoto/Cargo.toml delete mode 100644 vendor/bdk_kyoto/LICENSE delete mode 100644 vendor/bdk_kyoto/LICENSE-APACHE delete mode 100644 vendor/bdk_kyoto/LICENSE-MIT delete mode 100644 vendor/bdk_kyoto/README.md delete mode 100644 vendor/bdk_kyoto/src/builder.rs delete mode 100644 vendor/bdk_kyoto/src/lib.rs diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index f5b8114b..9dc68e4a 100644 --- a/bdk-ffi/Cargo.lock +++ b/bdk-ffi/Cargo.lock @@ -177,6 +177,7 @@ dependencies = [ [[package]] name = "bdk_kyoto" version = "0.15.4" +source = "git+https://github.com/reez/bdk-kyoto?branch=rc1#7303e1e2d2d944a7ba2b13bcc3d92c605b0f1afa" dependencies = [ "bdk_wallet", "bip157", @@ -205,13 +206,14 @@ checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bip157" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88df5c18baaea9be4219679afbd4fc26491606f89f6ecdaffcdcabd67635b07b" +checksum = "8af7987396c76633777e335345347395cd383b46bd95c1534691ab717859482e" dependencies = [ "bip324", "bitcoin", "bitcoin-address-book", + "bitcoin_hashes 0.20.0", "tokio", ] @@ -267,6 +269,15 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "bitcoin-consensus-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d7ca3dc8ff835693ad73bf1596240c06f974a31eeb3f611aaedf855f1f2725" +dependencies = [ + "bitcoin-internals 0.5.0", +] + [[package]] name = "bitcoin-internals" version = "0.3.0" @@ -282,6 +293,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90bbbfa552b49101a230fb2668f3f9ef968c81e6f83cf577e1d4b80f689e1aa" +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" + [[package]] name = "bitcoin-io" version = "0.1.4" @@ -328,6 +345,17 @@ dependencies = [ "hex-conservative 0.3.2", ] +[[package]] +name = "bitcoin_hashes" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a45c2b41c457a9a9e4670422fcbdf109afb3b22bc920b4045e8bdfd788a3d" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals 0.5.0", + "hex-conservative 0.3.2", +] + [[package]] name = "bitflags" version = "2.11.0" diff --git a/bdk-ffi/Cargo.toml b/bdk-ffi/Cargo.toml index 017033f5..05d5f714 100644 --- a/bdk-ffi/Cargo.toml +++ b/bdk-ffi/Cargo.toml @@ -18,7 +18,7 @@ path = "uniffi-bindgen.rs" bdk_wallet = { version = "=3.0.0-rc.1", 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 = { path = "../vendor/bdk_kyoto" } +bdk_kyoto = { git = "https://github.com/reez/bdk-kyoto", branch = "rc1" } uniffi = { version = "=0.30.0", features = ["cli"]} thiserror = "2.0.17" diff --git a/bdk-ffi/src/kyoto.rs b/bdk-ffi/src/kyoto.rs index 2323e94d..18c6cb05 100644 --- a/bdk-ffi/src/kyoto.rs +++ b/bdk-ffi/src/kyoto.rs @@ -7,7 +7,6 @@ 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; @@ -16,7 +15,7 @@ use bdk_kyoto::UnboundedReceiver; use bdk_kyoto::UpdateSubscriber; use bdk_kyoto::Warning as Warn; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -238,18 +237,15 @@ impl CbfBuilder { if let Some(proxy) = &self.socks5_proxy { let port = proxy.port; let addr = proxy.address.inner; - builder = builder.socks5_proxy((addr, port)); + builder = builder.socks5_proxy(SocketAddr::new(addr, port)); } - let BDKLightClient { - requester, - info_subscriber, - warning_subscriber, - update_subscriber, - node, - } = builder + let (client, logging, update_subscriber) = builder .build_with_wallet(&wallet, scan_type) - .expect("networks match by definition"); + .expect("networks match by definition") + .subscribe(); + let (client, node) = client.managed_start(); + let requester = client.requester(); let node = CbfNode { node: std::sync::Mutex::new(Some(node)), @@ -257,8 +253,8 @@ impl CbfBuilder { let client = CbfClient { sender: Arc::new(requester), - info_rx: Mutex::new(info_subscriber), - warning_rx: Mutex::new(warning_subscriber), + info_rx: Mutex::new(logging.info_subscriber), + warning_rx: Mutex::new(logging.warning_subscriber), update_rx: Mutex::new(update_subscriber), }; @@ -308,7 +304,7 @@ impl CbfClient { pub async fn broadcast(&self, transaction: &Transaction) -> Result, CbfError> { let tx = transaction.into(); self.sender - .broadcast_random(tx) + .broadcast_tx(tx) .await .map_err(From::from) .map(|wtxid| Arc::new(Wtxid(wtxid))) diff --git a/vendor/bdk_kyoto/.github/workflows/ci.yml b/vendor/bdk_kyoto/.github/workflows/ci.yml deleted file mode 100644 index f37a1ea7..00000000 --- a/vendor/bdk_kyoto/.github/workflows/ci.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Build & Test - -on: - push: - branches: - - master - pull_request: - -jobs: - check: - strategy: - matrix: - toolchain: [stable, beta, nightly] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: extractions/setup-just@v3 - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: ${{ matrix.toolchain }} - components: clippy,rustfmt - - run: just check - integration: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: extractions/setup-just@v3 - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - run: just test integration - msrv: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: extractions/setup-just@v3 - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - run: just test msrv diff --git a/vendor/bdk_kyoto/Cargo.toml b/vendor/bdk_kyoto/Cargo.toml deleted file mode 100644 index e8755b23..00000000 --- a/vendor/bdk_kyoto/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -edition = "2021" -rust-version = "1.84.0" -name = "bdk_kyoto" -version = "0.15.4" -authors = [ - "Rob rob@2140.dev", - "Bitcoin Dev Kit Developers", -] -build = false -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "BDK blockchain integration using P2P light client Kyoto" -documentation = "https://docs.rs/bdk-kyoto" -readme = "README.md" -keywords = [ - "bitcoin", - "peer-to-peer", - "light-client", -] -license = "MIT OR Apache-2.0" -repository = "https://github.com/bitcoindevkit/bdk-kyoto" - -[lib] -name = "bdk_kyoto" -path = "src/lib.rs" - -[dependencies.bdk_wallet] -version = "3.0.0-rc.1" - -[dependencies.bip157] -version = "0.3.4" diff --git a/vendor/bdk_kyoto/LICENSE b/vendor/bdk_kyoto/LICENSE deleted file mode 100644 index 313ad056..00000000 --- a/vendor/bdk_kyoto/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -This software is licensed under Apache 2.0 or MIT, at your option. - -Some files retain their own copyright notice, however, for full authorship information, see version control history. - -Except as otherwise noted in individual files, all files in this repository are licensed under the Apache License, Version 2.0 or the MIT license , at your option. - -You may not use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of this software or any files in this repository except in accordance with one or both of these licenses. \ No newline at end of file diff --git a/vendor/bdk_kyoto/LICENSE-APACHE b/vendor/bdk_kyoto/LICENSE-APACHE deleted file mode 100644 index f49a4e16..00000000 --- a/vendor/bdk_kyoto/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/vendor/bdk_kyoto/LICENSE-MIT b/vendor/bdk_kyoto/LICENSE-MIT deleted file mode 100644 index f8da085d..00000000 --- a/vendor/bdk_kyoto/LICENSE-MIT +++ /dev/null @@ -1,16 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/bdk_kyoto/README.md b/vendor/bdk_kyoto/README.md deleted file mode 100644 index f4d62499..00000000 --- a/vendor/bdk_kyoto/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# BDK Kyoto - -BDK-Kyoto is an extension of [Kyoto](https://github.com/2140-dev/kyoto), a client-side implementation of BIP157/BIP158. -These proposals define a way for users to fetch transactions privately, using _compact block filters_. -You may want to read the specification [here](https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki). -Kyoto runs as a psuedo-node, sending messages over the Bitcoin peer-to-peer layer, finding new peers to connect to, and managing a -light-weight database of Bitcoin block headers. As such, developing a wallet application using this crate is distinct from a typical -client/server relationship. Esplora and Electrum offer _proactive_ APIs, in that the servers will respond to events as they are requested. - -In the case of running a node as a background process, the developer experience is far more _reactive_, in that the node may emit any number of events, and the application may respond to them. BDK-Kyoto curates these events into structures that are easily handled by BDK APIs, making integration of compact block filters easily understood. - -## License - -Licensed under either of - -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or ) - -at your option. diff --git a/vendor/bdk_kyoto/src/builder.rs b/vendor/bdk_kyoto/src/builder.rs deleted file mode 100644 index dcbe85f8..00000000 --- a/vendor/bdk_kyoto/src/builder.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Construct a [`LightClient`] by using a reference to a [`Wallet`]. -//! -//! ## Details -//! -//! The node has a number of configurations. Notably, the height of in the blockchain to start a -//! wallet recovery and the nodes on the peer-to-peer network are both configurable. -//! -//! ```no_run -//! # const RECEIVE: &str = "tr([7d94197e/86'/1'/0']tpubDCyQVJj8KzjiQsFjmb3KwECVXPvMwvAxxZGCP9XmWSopmjW3bCV3wD7TgxrUhiGSueDS1MU5X1Vb1YjYcp8jitXc5fXfdC1z68hDDEyKRNr/0/*)"; -//! # const CHANGE: &str = "tr([7d94197e/86'/1'/0']tpubDCyQVJj8KzjiQsFjmb3KwECVXPvMwvAxxZGCP9XmWSopmjW3bCV3wD7TgxrUhiGSueDS1MU5X1Vb1YjYcp8jitXc5fXfdC1z68hDDEyKRNr/1/*)"; -//! use std::net::{IpAddr, Ipv4Addr}; -//! use std::path::PathBuf; -//! use std::time::Duration; -//! use bdk_wallet::Wallet; -//! use bdk_kyoto::bip157::{Network, TrustedPeer}; -//! use bdk_kyoto::builder::{Builder, BuilderExt}; -//! use bdk_kyoto::{LightClient, ScanType}; -//! -//! #[tokio::main] -//! async fn main() -> anyhow::Result<()> { -//! // Add specific peers to connect to. -//! let peer = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); -//! let trusted = TrustedPeer::from_ip(peer); -//! -//! let db_path = ".".parse::()?; -//! -//! let mut wallet = Wallet::create(RECEIVE, CHANGE) -//! .network(Network::Signet) -//! .create_wallet_no_persist()?; -//! -//! let scan_type = ScanType::Sync; -//! -//! let LightClient { -//! requester, -//! info_subscriber, -//! warning_subscriber, -//! update_subscriber, -//! node -//! } = Builder::new(Network::Signet) -//! // A node may handle multiple connections -//! .required_peers(2) -//! // Choose where to store node data -//! .data_dir(db_path) -//! // How long peers have to respond messages -//! .response_timeout(Duration::from_secs(2)) -//! // Added trusted peers to initialize the sync -//! .add_peer(trusted) -//! .build_with_wallet(&wallet, scan_type)?; -//! Ok(()) -//! } -//! ``` - -use std::fmt::Display; - -use bdk_wallet::{ - chain::{CheckPoint, IndexedTxGraph}, - Wallet, -}; -pub use bip157::Builder; -use bip157::{chain::ChainState, HeaderCheckpoint}; - -use crate::{LightClient, ScanType, UpdateSubscriber}; - -const IMPOSSIBLE_REORG_DEPTH: usize = 7; - -/// Build a compact block filter client and node for a specified wallet -pub trait BuilderExt { - /// Attempt to build the node with scripts from a [`Wallet`] and following a [`ScanType`]. - fn build_with_wallet( - self, - wallet: &Wallet, - scan_type: ScanType, - ) -> Result; -} - -impl BuilderExt for Builder { - fn build_with_wallet( - mut self, - wallet: &Wallet, - scan_type: ScanType, - ) -> Result { - let network = wallet.network(); - if self.network().ne(&network) { - return Err(BuilderError::NetworkMismatch); - } - match scan_type { - ScanType::Sync => { - let current_cp = wallet.latest_checkpoint(); - let sync_start = walk_back_max_reorg(current_cp); - self = self.chain_state(ChainState::Checkpoint(sync_start)); - } - ScanType::Recovery { - used_script_index: _, - checkpoint, - } => self = self.chain_state(ChainState::Checkpoint(checkpoint)), - } - let (node, client) = self.build(); - let bip157::Client { - requester, - info_rx, - warn_rx, - event_rx, - } = client; - let indexed_graph = IndexedTxGraph::new(wallet.spk_index().clone()); - let update_subscriber = UpdateSubscriber::new( - requester.clone(), - scan_type, - event_rx, - wallet.latest_checkpoint(), - indexed_graph, - ); - Ok(LightClient { - requester, - info_subscriber: info_rx, - warning_subscriber: warn_rx, - update_subscriber, - node, - }) - } -} - -/// Walk back 7 blocks in case the last sync was an orphan block. -fn walk_back_max_reorg(checkpoint: CheckPoint) -> HeaderCheckpoint { - let mut ret_cp = HeaderCheckpoint::new(checkpoint.height(), checkpoint.hash()); - let cp_iter = checkpoint.iter(); - for (index, next) in cp_iter.enumerate() { - if index > IMPOSSIBLE_REORG_DEPTH { - return ret_cp; - } - ret_cp = HeaderCheckpoint::new(next.height(), next.hash()); - } - ret_cp -} - -/// Errors the builder may throw. -#[derive(Debug)] -pub enum BuilderError { - /// The wallet network and node network do not match. - NetworkMismatch, -} - -impl Display for BuilderError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - BuilderError::NetworkMismatch => { - write!(f, "wallet network and node network do not match") - } - } - } -} - -impl std::error::Error for BuilderError {} diff --git a/vendor/bdk_kyoto/src/lib.rs b/vendor/bdk_kyoto/src/lib.rs deleted file mode 100644 index 2721a996..00000000 --- a/vendor/bdk_kyoto/src/lib.rs +++ /dev/null @@ -1,267 +0,0 @@ -#![doc = include_str!("../README.md")] -//! ## Examples -//! -//! If you have an existing project that leverages `bdk_wallet`, building the compact block filter -//! _node_ and _client_ is simple. You may construct and configure a node to integrate with your -//! wallet by using the [`BuilderExt`](crate::builder) and [`Builder`](crate::builder). -//! -//! ```no_run -//! # const RECEIVE: &str = "tr([7d94197e/86'/1'/0']tpubDCyQVJj8KzjiQsFjmb3KwECVXPvMwvAxxZGCP9XmWSopmjW3bCV3wD7TgxrUhiGSueDS1MU5X1Vb1YjYcp8jitXc5fXfdC1z68hDDEyKRNr/0/*)"; -//! # const CHANGE: &str = "tr([7d94197e/86'/1'/0']tpubDCyQVJj8KzjiQsFjmb3KwECVXPvMwvAxxZGCP9XmWSopmjW3bCV3wD7TgxrUhiGSueDS1MU5X1Vb1YjYcp8jitXc5fXfdC1z68hDDEyKRNr/1/*)"; -//! use bdk_wallet::Wallet; -//! use bdk_wallet::bitcoin::Network; -//! use bdk_kyoto::builder::{Builder, BuilderExt}; -//! use bdk_kyoto::{LightClient, ScanType}; -//! -//! #[tokio::main] -//! async fn main() -> anyhow::Result<()> { -//! let mut wallet = Wallet::create(RECEIVE, CHANGE) -//! .network(Network::Signet) -//! .create_wallet_no_persist()?; -//! -//! let LightClient { -//! requester, -//! info_subscriber: _, -//! warning_subscriber: _, -//! mut update_subscriber, -//! node -//! } = Builder::new(Network::Signet).build_with_wallet(&wallet, ScanType::Sync)?; -//! -//! tokio::task::spawn(async move { node.run().await }); -//! -//! loop { -//! let update = update_subscriber.update().await?; -//! wallet.apply_update(update)?; -//! return Ok(()); -//! } -//! } -//! ``` - -#![warn(missing_docs)] -use std::collections::HashSet; - -use bdk_wallet::chain::BlockId; -use bdk_wallet::chain::CheckPoint; -pub use bdk_wallet::Update; - -use bdk_wallet::chain::{keychain_txout::KeychainTxOutIndex, IndexedTxGraph}; -use bdk_wallet::chain::{ConfirmationBlockTime, TxUpdate}; -use bdk_wallet::KeychainKind; - -pub extern crate bip157; - -use bip157::chain::BlockHeaderChanges; -use bip157::ScriptBuf; -#[doc(inline)] -pub use bip157::{ - BlockHash, ClientError, FeeRate, HeaderCheckpoint, Info, Node, RejectPayload, RejectReason, - Requester, TrustedPeer, TxBroadcast, TxBroadcastPolicy, Warning, Wtxid, -}; -use bip157::{Event, SyncUpdate}; - -#[doc(inline)] -pub use bip157::Receiver; -#[doc(inline)] -pub use bip157::UnboundedReceiver; - -#[doc(inline)] -pub use builder::BuilderExt; -pub mod builder; - -#[derive(Debug)] -/// A node and associated structs to send and receive events to and from the node. -pub struct LightClient { - /// Send events to a running node (i.e. broadcast a transaction). - pub requester: Requester, - /// Receive informational messages as the node runs. - pub info_subscriber: Receiver, - /// Receive warnings from the node as it runs. - pub warning_subscriber: UnboundedReceiver, - /// Receive wallet updates from a node. - pub update_subscriber: UpdateSubscriber, - /// The underlying node that must be run to fetch blocks from peers. - pub node: Node, -} - -/// Interpret events from a node that is running to apply -/// updates to an underlying wallet. -#[derive(Debug)] -pub struct UpdateSubscriber { - // request information from the client - requester: Requester, - // channel receiver - receiver: UnboundedReceiver, - // changes to local chain - cp: CheckPoint, - // receive graph - graph: IndexedTxGraph>, - // queued blocks to fetch - queued_blocks: Vec, - // queued scripts to check filters - spk_cache: HashSet, -} - -impl UpdateSubscriber { - fn new( - requester: Requester, - scan_type: ScanType, - receiver: UnboundedReceiver, - cp: CheckPoint, - graph: IndexedTxGraph>, - ) -> Self { - let spk_cache = match scan_type { - ScanType::Sync => Self::peek_scripts(&graph.index, graph.index.lookahead()), - ScanType::Recovery { - used_script_index, - checkpoint: _, - } => Self::peek_scripts(&graph.index, used_script_index), - }; - Self { - requester, - receiver, - cp, - graph, - queued_blocks: Vec::new(), - spk_cache, - } - } - /// Return the most recent update from the node once it has synced to the network's tip. - /// This may take a significant portion of time during wallet recoveries or dormant wallets. - /// Note that you may call this method in a loop as long as the node is running. - /// - /// **Warning** - /// - /// This method is _not_ cancel safe. You cannot use it within a `tokio::select` arm. - pub async fn update(&mut self) -> Result { - let mut cp = self.cp.clone(); - while let Some(message) = self.receiver.recv().await { - match message { - Event::IndexedFilter(filter) => { - let block_hash = filter.block_hash(); - if filter.contains_any(self.spk_cache.iter()) { - self.queued_blocks.push(block_hash); - } - } - // these are emitted for every block - Event::ChainUpdate(BlockHeaderChanges::Connected(at)) => { - let block_id = BlockId { - hash: at.block_hash(), - height: at.height, - }; - cp = cp.insert(block_id); - } - Event::ChainUpdate(BlockHeaderChanges::Reorganized { - accepted, - reorganized: _, - }) => { - for header in accepted { - let block_id = BlockId { - hash: header.block_hash(), - height: header.height, - }; - cp = cp.insert(block_id); - } - } - Event::FiltersSynced(SyncUpdate { - tip: _, - recent_history: _, - }) => { - for hash in core::mem::take(&mut self.queued_blocks) { - let block = self - .requester - .get_block(hash) - .await - .map_err(|_| UpdateError::NodeStopped)?; - let height = block.height; - let block = block.block; - let _ = self.graph.apply_block_relevant(&block, height); - } - self.cp = cp; - self.spk_cache.extend(Self::peek_scripts( - &self.graph.index, - self.graph.index.lookahead(), - )); - return Ok(self.get_scan_response()); - } - _ => (), - } - } - Err(UpdateError::NodeStopped) - } - - // When the client is believed to have synced to the chain tip of most work, - // we can return a wallet update. - fn get_scan_response(&mut self) -> Update { - let tx_update = TxUpdate::from(self.graph.graph().clone()); - let graph = core::mem::take(&mut self.graph); - let last_active_indices = graph.index.last_used_indices(); - self.graph = IndexedTxGraph::new(graph.index); - Update { - tx_update, - last_active_indices, - chain: Some(self.cp.clone()), - } - } - - fn peek_scripts( - keychain: &KeychainTxOutIndex, - to_index: u32, - ) -> HashSet { - let mut spk_cache = HashSet::new(); - // We pre-compute an SPK cache so as to not call `unbounded_spk_iter` for each filter - let last_revealed = keychain.last_revealed_indices(); - let ext_index = last_revealed - .get(&KeychainKind::External) - .copied() - .unwrap_or(0); - let unbounded_ext_spk_iter = keychain - .unbounded_spk_iter(KeychainKind::External) - .expect("wallet must have external keychain"); - let bound = (ext_index + to_index) as usize; - let bounded_ext_iter = unbounded_ext_spk_iter.take(bound).map(|(_, script)| script); - spk_cache.extend(bounded_ext_iter); - let int_index = last_revealed - .get(&KeychainKind::Internal) - .copied() - .unwrap_or(0); - let unbounded_int_spk_iter = keychain.unbounded_spk_iter(KeychainKind::Internal); - if let Some(int_spk_iter) = unbounded_int_spk_iter { - let bound = (int_index + to_index) as usize; - let bounded_int_iter = int_spk_iter.take(bound).map(|(_, script)| script); - spk_cache.extend(bounded_int_iter); - } - spk_cache - } -} - -/// Errors encountered when attempting to construct a wallet update. -#[derive(Debug, Clone, Copy)] -pub enum UpdateError { - /// The node has stopped running. - NodeStopped, -} - -impl std::fmt::Display for UpdateError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - UpdateError::NodeStopped => write!(f, "the node halted execution."), - } - } -} - -impl std::error::Error for UpdateError {} - -/// How to scan compact block filters on start up. -#[derive(Debug, Clone, Copy, Default)] -pub enum ScanType { - /// Sync the wallet from the last known wallet checkpoint to the rest of the network. - #[default] - Sync, - /// Recover an old wallet by scanning after the specified height. - Recovery { - /// The amount of scripts used by the wallet that is being recovered. - used_script_index: u32, - /// The height in the block chain to begin searching for transactions. - checkpoint: HeaderCheckpoint, - }, -} From 01e562082ba81025e284f3c92f639ec3725e5b49 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 19 Mar 2026 10:45:57 -0500 Subject: [PATCH 03/15] feat(wallet): expose apply_unconfirmed_txs_events --- bdk-ffi/src/wallet.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 17b01319..ea8b3a75 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -341,6 +341,29 @@ impl Wallet { ) } + /// Apply relevant unconfirmed transactions to the wallet and returns events. + /// + /// See [`apply_unconfirmed_txs`] for more information. + /// + /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. + /// + /// [`apply_unconfirmed_txs`]: Self::apply_unconfirmed_txs + /// [`apply_update_events`]: Self::apply_update_events + pub fn apply_unconfirmed_txs_events( + &self, + unconfirmed_txs: Vec, + ) -> Vec { + self.get_wallet() + .apply_unconfirmed_txs_events( + unconfirmed_txs + .into_iter() + .map(|utx| (Arc::new(utx.tx.as_ref().into()), utx.last_seen)), + ) + .into_iter() + .map(|event| event.into()) + .collect() + } + /// Apply transactions that have been evicted from the mempool. /// Transactions may be evicted for paying too-low fee, or for being malformed. /// Irrelevant transactions are ignored. From ce0e4baa55cb2acddeda6b24ff3d5f56f42a6767 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 19 Mar 2026 10:50:35 -0500 Subject: [PATCH 04/15] feat(wallet): expose apply_evicted_txs_events --- bdk-ffi/src/wallet.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index ea8b3a75..d0b7ae7a 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -377,6 +377,27 @@ impl Wallet { ); } + /// Apply evictions of the given transaction IDs with their associated timestamps and returns + /// events. + /// + /// See [`apply_evicted_txs`] for more information. + /// + /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. + /// + /// [`apply_evicted_txs`]: Self::apply_evicted_txs + /// [`apply_update_events`]: Self::apply_update_events + pub fn apply_evicted_txs_events(&self, evicted_txs: Vec) -> Vec { + self.get_wallet() + .apply_evicted_txs_events( + evicted_txs + .into_iter() + .map(|etx| (etx.txid.0, etx.evicted_at)), + ) + .into_iter() + .map(|event| event.into()) + .collect() + } + /// The derivation index of this wallet. It will return `None` if it has not derived any addresses. /// Otherwise, it will return the index of the highest address it has derived. pub fn derivation_index(&self, keychain: KeychainKind) -> Option { From f38f63013a750512e45b853907439826f576f0b5 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 20 Mar 2026 10:48:03 -0500 Subject: [PATCH 05/15] feat(wallet): expose checkpoints --- bdk-ffi/src/wallet.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index d0b7ae7a..0140901d 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -642,6 +642,14 @@ impl Wallet { self.get_wallet().latest_checkpoint().block_id().into() } + /// Get all the checkpoints the wallet is currently storing indexed by height. + pub fn checkpoints(&self) -> Vec { + self.get_wallet() + .checkpoints() + .map(|checkpoint| checkpoint.block_id().into()) + .collect() + } + /// Get the [`TxDetails`] of a wallet transaction. pub fn tx_details(&self, txid: Arc) -> Option { self.get_wallet() From 2ff90b732615f5963e4bb145cf1eb9760646a02b Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 23 Mar 2026 08:41:59 -0500 Subject: [PATCH 06/15] feat(wallet): expose start_sync_with_revealed_spks_at --- bdk-ffi/src/wallet.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 0140901d..3ca414a7 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -598,6 +598,17 @@ impl Wallet { Arc::new(FullScanRequestBuilder(Mutex::new(Some(builder)))) } + /// Create a partial [`SyncRequest`] for all revealed spks at `start_time`. + /// + /// The `start_time` is used to record the time that a mempool transaction was last seen + /// (or evicted). See [`Wallet::start_sync_with_revealed_spks`] for more. + pub fn start_sync_with_revealed_spks_at(&self, start_time: u64) -> Arc { + let builder = self + .get_wallet() + .start_sync_with_revealed_spks_at(start_time); + Arc::new(SyncRequestBuilder(Mutex::new(Some(builder)))) + } + /// Create a partial [`SyncRequest`] for this wallet for all revealed spks. /// /// This is the first step when performing a spk-based wallet partial sync, the returned From 7f8dcfc632e5fbe5c0d7c14d4d244ae504b0e1c6 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 23 Mar 2026 08:45:37 -0500 Subject: [PATCH 07/15] feat(wallet): expose start_full_scan_at --- bdk-ffi/src/wallet.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 3ca414a7..4d4ae752 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -598,6 +598,12 @@ impl Wallet { Arc::new(FullScanRequestBuilder(Mutex::new(Some(builder)))) } + /// Create a [`FullScanRequest`] builder at `start_time`. + pub fn start_full_scan_at(&self, start_time: u64) -> Arc { + let builder = self.get_wallet().start_full_scan_at(start_time); + Arc::new(FullScanRequestBuilder(Mutex::new(Some(builder)))) + } + /// Create a partial [`SyncRequest`] for all revealed spks at `start_time`. /// /// The `start_time` is used to record the time that a mempool transaction was last seen From be532edf9ca32163f09a4232d707cb20974c0634 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 23 Mar 2026 08:58:57 -0500 Subject: [PATCH 08/15] feat(tx-builder): expose sighash --- bdk-ffi/src/error.rs | 6 ++++ bdk-ffi/src/tests/tx_builder.rs | 56 +++++++++++++++++++++++++++++++++ bdk-ffi/src/tx_builder.rs | 42 ++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index f64a614b..ae88e687 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -698,6 +698,12 @@ pub enum PsbtParseError { Base64Encoding { error_message: String }, } +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum SighashParseError { + #[error("invalid sighash type: {error_message}")] + Invalid { error_message: String }, +} + #[derive(Debug, thiserror::Error, uniffi::Error)] pub enum PsbtFinalizeError { #[error("an input at index {index} is invalid: {reason}")] diff --git a/bdk-ffi/src/tests/tx_builder.rs b/bdk-ffi/src/tests/tx_builder.rs index 12b634f0..cf18506e 100644 --- a/bdk-ffi/src/tests/tx_builder.rs +++ b/bdk-ffi/src/tests/tx_builder.rs @@ -1,5 +1,6 @@ use crate::bitcoin::{Amount, Input, Network, OutPoint, Script, TxOut}; use crate::descriptor::Descriptor; +use crate::error::SighashParseError; use crate::esplora::EsploraClient; use crate::store::Persister; use crate::tx_builder::TxBuilder; @@ -196,6 +197,61 @@ fn test_only_witness_utxo_with_finish() { } } +#[test] +fn test_sighash_invalid_string_returns_error() { + let result = TxBuilder::new().sighash("not-a-sighash".to_string()); + + assert!(matches!(result, Err(SighashParseError::Invalid { .. }))); +} + +#[test] +fn test_sighash_sets_psbt_input_sighash_type() { + let wallet = create_and_sync_wallet(); + let utxos = wallet.list_unspent(); + + if utxos.is_empty() { + println!("No UTXOs available, skipping sighash PSBT assertion"); + return; + } + + let address = wallet + .next_unused_address(bdk_wallet::KeychainKind::External) + .address; + let ext_policy = wallet.policies(bdk_wallet::KeychainKind::External); + let int_policy = wallet.policies(bdk_wallet::KeychainKind::Internal); + + if let (Ok(Some(ext_policy)), Ok(Some(int_policy))) = (ext_policy, int_policy) { + let ext_path: HashMap<_, _> = vec![(ext_policy.id().clone(), vec![0, 1])] + .into_iter() + .collect(); + let int_path: HashMap<_, _> = vec![(int_policy.id().clone(), vec![0, 1])] + .into_iter() + .collect(); + let wallet_arc = Arc::new(wallet); + + let psbt = TxBuilder::new() + .add_recipient( + &(*address.script_pubkey()).to_owned(), + Arc::new(Amount::from_sat(1000)), + ) + .policy_path(ext_path, bdk_wallet::KeychainKind::External) + .policy_path(int_path, bdk_wallet::KeychainKind::Internal) + .do_not_spend_change() + .sighash("SIGHASH_SINGLE|SIGHASH_ANYONECANPAY".to_string()) + .expect("valid sighash type") + .finish(&wallet_arc) + .expect("build transaction with sighash"); + + let inputs = psbt.input(); + assert!(!inputs.is_empty()); + assert!(inputs.iter().all(|input| { + input.sighash_type.as_deref() == Some("SIGHASH_SINGLE|SIGHASH_ANYONECANPAY") + })); + } else { + panic!("Failed to retrieve valid policies for keychains"); + } +} + #[test] fn test_add_foreign_utxo_missing_witness_data() { // Create a foreign UTXO without witness_utxo or non_witness_utxo diff --git a/bdk-ffi/src/tx_builder.rs b/bdk-ffi/src/tx_builder.rs index 2f254721..8aa64597 100644 --- a/bdk-ffi/src/tx_builder.rs +++ b/bdk-ffi/src/tx_builder.rs @@ -1,11 +1,12 @@ use crate::bitcoin::{Amount, FeeRate, Input, OutPoint, Psbt, Script, Txid}; -use crate::error::{AddForeignUtxoError, CreateTxError}; +use crate::error::{AddForeignUtxoError, CreateTxError, SighashParseError}; use crate::types::{LockTime, ScriptAmount}; use crate::wallet::Wallet; use bdk_wallet::bitcoin::absolute::LockTime as BdkLockTime; use bdk_wallet::bitcoin::amount::Amount as BdkAmount; use bdk_wallet::bitcoin::psbt::Input as BdkInput; +use bdk_wallet::bitcoin::psbt::PsbtSighashType as BdkPsbtSighashType; use bdk_wallet::bitcoin::script::PushBytesBuf; use bdk_wallet::bitcoin::Psbt as BdkPsbt; use bdk_wallet::bitcoin::ScriptBuf as BdkScriptBuf; @@ -15,6 +16,7 @@ use bdk_wallet::KeychainKind; use std::collections::BTreeMap; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; +use std::str::FromStr; use std::sync::Arc; type ChangeSpendPolicy = bdk_wallet::ChangeSpendPolicy; @@ -41,6 +43,7 @@ pub struct TxBuilder { locktime: Option, allow_dust: bool, version: Option, + sighash: Option, exclude_unconfirmed: bool, exclude_below_confirmations: Option, only_witness_utxo: bool, @@ -71,6 +74,7 @@ impl TxBuilder { locktime: None, allow_dust: false, version: None, + sighash: None, exclude_unconfirmed: false, exclude_below_confirmations: None, only_witness_utxo: false, @@ -371,6 +375,17 @@ impl TxBuilder { }) } + /// Sign with a specific sig hash + /// + /// **Use this option very carefully** + pub fn sighash(&self, sighash: String) -> Result, SighashParseError> { + let sighash = parse_sighash_type(&sighash)?; + Ok(Arc::new(TxBuilder { + sighash: Some(sighash), + ..self.clone() + })) + } + /// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field /// when spending from SegWit descriptors. /// @@ -545,6 +560,9 @@ impl TxBuilder { if let Some(version) = self.version { tx_builder.version(version); } + if let Some(sighash) = self.sighash { + tx_builder.sighash(sighash); + } if self.exclude_unconfirmed { tx_builder.exclude_unconfirmed(); } @@ -576,6 +594,7 @@ pub struct BumpFeeTxBuilder { locktime: Option, allow_dust: bool, version: Option, + sighash: Option, } #[uniffi::export] @@ -590,6 +609,7 @@ impl BumpFeeTxBuilder { locktime: None, allow_dust: false, version: None, + sighash: None, } } @@ -653,6 +673,17 @@ impl BumpFeeTxBuilder { }) } + /// Sign with a specific sig hash + /// + /// **Use this option very carefully** + pub fn sighash(&self, sighash: String) -> Result, SighashParseError> { + let sighash = parse_sighash_type(&sighash)?; + Ok(Arc::new(BumpFeeTxBuilder { + sighash: Some(sighash), + ..self.clone() + })) + } + /// Finish building the transaction. /// /// Uses the thread-local random number generator (rng). @@ -683,6 +714,9 @@ impl BumpFeeTxBuilder { if let Some(version) = self.version { tx_builder.version(version); } + if let Some(sighash) = self.sighash { + tx_builder.sighash(sighash); + } let psbt: BdkPsbt = tx_builder.finish()?; @@ -701,3 +735,9 @@ pub enum ChangeSpendPolicy { /// Only use non-change outputs (see [`bdk_wallet::TxBuilder::do_not_spend_change`]). ChangeForbidden, } + +fn parse_sighash_type(sighash: &str) -> Result { + BdkPsbtSighashType::from_str(sighash).map_err(|error| SighashParseError::Invalid { + error_message: error.to_string(), + }) +} From bb197668c3e9e4942b1989a5959d0d7cd4415854 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 24 Mar 2026 10:58:43 -0500 Subject: [PATCH 09/15] feat(tx-builder): expose add_foreign_utxo_with_sequence --- bdk-ffi/src/tx_builder.rs | 61 +++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/bdk-ffi/src/tx_builder.rs b/bdk-ffi/src/tx_builder.rs index 8aa64597..448069c7 100644 --- a/bdk-ffi/src/tx_builder.rs +++ b/bdk-ffi/src/tx_builder.rs @@ -47,7 +47,7 @@ pub struct TxBuilder { exclude_unconfirmed: bool, exclude_below_confirmations: Option, only_witness_utxo: bool, - foreign_utxos: Vec<(BdkOutPoint, BdkInput, BdkWeight)>, + foreign_utxos: Vec<(BdkOutPoint, BdkInput, BdkWeight, Option)>, } #[allow(clippy::new_without_default)] @@ -484,7 +484,46 @@ impl TxBuilder { let bdk_weight = BdkWeight::from_wu(satisfaction_weight); let mut foreign_utxos = self.foreign_utxos.clone(); - foreign_utxos.push((bdk_outpoint, bdk_input, bdk_weight)); + foreign_utxos.push((bdk_outpoint, bdk_input, bdk_weight, None)); + + Ok(Arc::new(TxBuilder { + foreign_utxos, + ..self.clone() + })) + } + + /// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence + /// value. + pub fn add_foreign_utxo_with_sequence( + &self, + outpoint: OutPoint, + psbt_input: Input, + satisfaction_weight: u64, + sequence: u32, + ) -> Result, AddForeignUtxoError> { + let bdk_outpoint: BdkOutPoint = outpoint.into(); + let bdk_input: BdkInput = psbt_input.try_into()?; + + if bdk_input.witness_utxo.is_none() { + match bdk_input.non_witness_utxo.as_ref() { + Some(tx) => { + if tx.compute_txid() != bdk_outpoint.txid { + return Err(AddForeignUtxoError::InvalidTxid); + } + if tx.output.len() <= bdk_outpoint.vout as usize { + return Err(AddForeignUtxoError::InvalidOutpoint { + outpoint: bdk_outpoint.to_string(), + }); + } + } + None => return Err(AddForeignUtxoError::MissingUtxo), + } + } + + let bdk_weight = BdkWeight::from_wu(satisfaction_weight); + + let mut foreign_utxos = self.foreign_utxos.clone(); + foreign_utxos.push((bdk_outpoint, bdk_input, bdk_weight, Some(sequence))); Ok(Arc::new(TxBuilder { foreign_utxos, @@ -572,10 +611,20 @@ impl TxBuilder { if self.only_witness_utxo { tx_builder.only_witness_utxo(); } - for (outpoint, input, weight) in &self.foreign_utxos { - tx_builder - .add_foreign_utxo(*outpoint, input.clone(), *weight) - .map_err(AddForeignUtxoError::from)?; + for (outpoint, input, weight, sequence) in &self.foreign_utxos { + match sequence { + Some(sequence) => tx_builder + .add_foreign_utxo_with_sequence( + *outpoint, + input.clone(), + *weight, + Sequence(*sequence), + ) + .map_err(AddForeignUtxoError::from)?, + None => tx_builder + .add_foreign_utxo(*outpoint, input.clone(), *weight) + .map_err(AddForeignUtxoError::from)?, + }; } let psbt = tx_builder.finish().map_err(CreateTxError::from)?; From 5bb7094187665f5c1042c3deff34b4f53afeabf3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 24 Mar 2026 14:38:50 -0500 Subject: [PATCH 10/15] feat(tx-builder): expose ordering --- bdk-ffi/src/tx_builder.rs | 59 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/bdk-ffi/src/tx_builder.rs b/bdk-ffi/src/tx_builder.rs index 448069c7..ab4f1ff6 100644 --- a/bdk-ffi/src/tx_builder.rs +++ b/bdk-ffi/src/tx_builder.rs @@ -11,7 +11,7 @@ use bdk_wallet::bitcoin::script::PushBytesBuf; use bdk_wallet::bitcoin::Psbt as BdkPsbt; use bdk_wallet::bitcoin::ScriptBuf as BdkScriptBuf; use bdk_wallet::bitcoin::{OutPoint as BdkOutPoint, Sequence, Weight as BdkWeight}; -use bdk_wallet::KeychainKind; +use bdk_wallet::{KeychainKind, TxOrdering as BdkTxOrdering}; use std::collections::BTreeMap; use std::collections::HashMap; @@ -44,6 +44,7 @@ pub struct TxBuilder { allow_dust: bool, version: Option, sighash: Option, + ordering: TxOrdering, exclude_unconfirmed: bool, exclude_below_confirmations: Option, only_witness_utxo: bool, @@ -75,6 +76,7 @@ impl TxBuilder { allow_dust: false, version: None, sighash: None, + ordering: TxOrdering::Shuffle, exclude_unconfirmed: false, exclude_below_confirmations: None, only_witness_utxo: false, @@ -386,6 +388,19 @@ impl TxBuilder { })) } + /// Choose the ordering for inputs and outputs of the transaction + /// + /// When [TxBuilder::ordering] is set to [TxOrdering::Untouched], the insertion order of + /// recipients and manually selected UTXOs is preserved and reflected exactly in transaction's + /// output and input vectors respectively. If algorithmically selected UTXOs are included, they + /// will be placed after all the manually selected ones in the transaction's input vector. + pub fn ordering(&self, ordering: TxOrdering) -> Arc { + Arc::new(TxBuilder { + ordering, + ..self.clone() + }) + } + /// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field /// when spending from SegWit descriptors. /// @@ -602,6 +617,7 @@ impl TxBuilder { if let Some(sighash) = self.sighash { tx_builder.sighash(sighash); } + tx_builder.ordering(self.ordering.into()); if self.exclude_unconfirmed { tx_builder.exclude_unconfirmed(); } @@ -644,6 +660,7 @@ pub struct BumpFeeTxBuilder { allow_dust: bool, version: Option, sighash: Option, + ordering: TxOrdering, } #[uniffi::export] @@ -659,6 +676,7 @@ impl BumpFeeTxBuilder { allow_dust: false, version: None, sighash: None, + ordering: TxOrdering::Shuffle, } } @@ -733,6 +751,19 @@ impl BumpFeeTxBuilder { })) } + /// Choose the ordering for inputs and outputs of the transaction + /// + /// When [TxBuilder::ordering] is set to [TxOrdering::Untouched], the insertion order of + /// recipients and manually selected UTXOs is preserved and reflected exactly in transaction's + /// output and input vectors respectively. If algorithmically selected UTXOs are included, they + /// will be placed after all the manually selected ones in the transaction's input vector. + pub fn ordering(&self, ordering: TxOrdering) -> Arc { + Arc::new(BumpFeeTxBuilder { + ordering, + ..self.clone() + }) + } + /// Finish building the transaction. /// /// Uses the thread-local random number generator (rng). @@ -766,6 +797,7 @@ impl BumpFeeTxBuilder { if let Some(sighash) = self.sighash { tx_builder.sighash(sighash); } + tx_builder.ordering(self.ordering.into()); let psbt: BdkPsbt = tx_builder.finish()?; @@ -785,6 +817,31 @@ pub enum ChangeSpendPolicy { ChangeForbidden, } +/// Ordering of the transaction's inputs and outputs. +#[derive(Clone, Copy, Debug, Default, uniffi::Enum)] +pub enum TxOrdering { + /// Randomized (default) + #[default] + Shuffle, + /// Untouched + /// + /// Untouched insertion order for recipients and for manually added UTXOs. This guarantees all + /// recipients preserve insertion order in the transaction's output vector and manually added + /// UTXOs preserve insertion order in the transaction's input vector, but does not make any + /// guarantees about algorithmically selected UTXOs. However, by design they will always be + /// placed after the manually selected ones. + Untouched, +} + +impl From for BdkTxOrdering { + fn from(value: TxOrdering) -> Self { + match value { + TxOrdering::Shuffle => BdkTxOrdering::Shuffle, + TxOrdering::Untouched => BdkTxOrdering::Untouched, + } + } +} + fn parse_sighash_type(sighash: &str) -> Result { BdkPsbtSighashType::from_str(sighash).map_err(|error| SighashParseError::Invalid { error_message: error.to_string(), From 402c1e24b6db6a9292234bb49a529472bc59041b Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 24 Mar 2026 15:20:26 -0500 Subject: [PATCH 11/15] feat(wallet): expose locked outpoints and persist them in changesets --- bdk-ffi/src/types.rs | 59 ++++++++++++++++++++++++++++++++++++++++++- bdk-ffi/src/wallet.rs | 39 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index 903f2917..6e17278e 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -1186,6 +1186,7 @@ pub struct ChangeSet { local_chain: LocalChainChangeSet, tx_graph: TxGraphChangeSet, indexer: IndexerChangeSet, + locked_outpoints: HashMap, bool>, } #[uniffi::export] @@ -1213,6 +1214,28 @@ impl ChangeSet { local_chain, tx_graph, indexer, + locked_outpoints: HashMap::new(), + } + } + + #[uniffi::constructor] + pub fn from_aggregate_with_locked_outpoints( + descriptor: Option>, + change_descriptor: Option>, + network: Option, + local_chain: LocalChainChangeSet, + tx_graph: TxGraphChangeSet, + indexer: IndexerChangeSet, + locked_outpoints: HashMap, bool>, + ) -> Self { + Self { + descriptor, + change_descriptor, + network, + local_chain, + tx_graph, + indexer, + locked_outpoints, } } @@ -1229,6 +1252,7 @@ impl ChangeSet { local_chain: LocalChainChangeSet::default(), tx_graph: TxGraphChangeSet::default(), indexer: IndexerChangeSet::default(), + locked_outpoints: HashMap::new(), } } @@ -1257,6 +1281,20 @@ impl ChangeSet { changeset.into() } + /// Start a wallet `ChangeSet` from locked outpoint changes. + #[uniffi::constructor] + pub fn from_locked_outpoints_changeset( + locked_outpoints_changeset: HashMap, bool>, + ) -> Self { + let outpoints = locked_outpoints_changeset + .into_iter() + .map(|(outpoint, is_locked)| (outpoint.outpoint().into(), is_locked)) + .collect(); + let changeset: bdk_wallet::ChangeSet = + bdk_wallet::locked_outpoints::ChangeSet { outpoints }.into(); + changeset.into() + } + /// Build a `ChangeSet` by merging together two `ChangeSet`. #[uniffi::constructor] pub fn from_merge(left: Arc, right: Arc) -> Self { @@ -1295,6 +1333,11 @@ impl ChangeSet { pub fn indexer_changeset(&self) -> IndexerChangeSet { self.indexer.clone() } + + /// Get the changes to locked outpoints. + pub fn locked_outpoints_changeset(&self) -> HashMap, bool> { + self.locked_outpoints.clone() + } } impl From for bdk_wallet::ChangeSet { @@ -1307,6 +1350,11 @@ impl From for bdk_wallet::ChangeSet { let local_chain = value.local_chain.into(); let tx_graph = value.tx_graph.into(); let indexer = value.indexer.into(); + let locked_outpoints = value + .locked_outpoints + .into_iter() + .map(|(outpoint, is_locked)| (outpoint.outpoint().into(), is_locked)) + .collect(); Self { descriptor, change_descriptor, @@ -1314,7 +1362,9 @@ impl From for bdk_wallet::ChangeSet { local_chain, tx_graph, indexer, - locked_outpoints: Default::default(), + locked_outpoints: bdk_wallet::locked_outpoints::ChangeSet { + outpoints: locked_outpoints, + }, } } } @@ -1337,6 +1387,12 @@ impl From for ChangeSet { let local_chain = value.local_chain.into(); let tx_graph = value.tx_graph.into(); let indexer = value.indexer.into(); + let locked_outpoints = value + .locked_outpoints + .outpoints + .into_iter() + .map(|(outpoint, is_locked)| (Arc::new(HashableOutPoint(outpoint.into())), is_locked)) + .collect(); Self { descriptor, change_descriptor, @@ -1344,6 +1400,7 @@ impl From for ChangeSet { local_chain, tx_graph, indexer, + locked_outpoints, } } } diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 4d4ae752..01f07c2c 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -578,6 +578,45 @@ impl Wallet { self.get_wallet().list_unspent().map(|o| o.into()).collect() } + /// List the locked outpoints. + pub fn list_locked_outpoints(&self) -> Vec { + self.get_wallet() + .list_locked_outpoints() + .map(Into::into) + .collect() + } + + /// List unspent outpoints that are currently locked. + pub fn list_locked_unspent(&self) -> Vec { + self.get_wallet() + .list_locked_unspent() + .map(Into::into) + .collect() + } + + /// Whether the `outpoint` is locked. See `Wallet::lock_outpoint` for more. + pub fn is_outpoint_locked(&self, outpoint: OutPoint) -> bool { + self.get_wallet().is_outpoint_locked(outpoint.into()) + } + + /// Lock a wallet output identified by the given `outpoint`. + /// + /// A locked UTXO will not be selected as an input to fund a transaction. This is useful + /// for excluding or reserving candidate inputs during transaction creation. + /// + /// **You must persist the staged change for the lock status to be persistent**. To unlock a + /// previously locked outpoint, see `Wallet::unlock_outpoint`. + pub fn lock_outpoint(&self, outpoint: OutPoint) { + self.get_wallet().lock_outpoint(outpoint.into()); + } + + /// Unlock the wallet output of the specified `outpoint`. + /// + /// **You must persist the staged change for the lock status to be persistent**. + pub fn unlock_outpoint(&self, outpoint: OutPoint) { + self.get_wallet().unlock_outpoint(outpoint.into()); + } + /// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed). /// /// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead. From 32ed879f7b9f2573286c18f286e127e4d600a59c Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 27 Mar 2026 08:35:15 -0500 Subject: [PATCH 12/15] deps: bump bdk_wallet to 3.0.0-rc.2 --- bdk-ffi/Cargo.lock | 6 +++--- bdk-ffi/Cargo.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index 9dc68e4a..3ce97c75 100644 --- a/bdk-ffi/Cargo.lock +++ b/bdk-ffi/Cargo.lock @@ -119,7 +119,7 @@ dependencies = [ [[package]] name = "bdk-ffi" -version = "3.0.0-rc.1" +version = "3.0.0-rc.2" dependencies = [ "assert_matches", "bdk_electrum", @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "3.0.0-rc.1" +version = "3.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70031c96c223c780525f4b9c52813e4f5bd8000a5712aac3248d00838af1e436" +checksum = "9b989a12f0398a844bf22b14a197e2676aea2775aabca52057627f596ad3a0ef" dependencies = [ "bdk_chain", "bip39", diff --git a/bdk-ffi/Cargo.toml b/bdk-ffi/Cargo.toml index 05d5f714..8b59ab18 100644 --- a/bdk-ffi/Cargo.toml +++ b/bdk-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk-ffi" -version = "3.0.0-rc.1" +version = "3.0.0-rc.2" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" edition = "2018" @@ -15,7 +15,7 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" [dependencies] -bdk_wallet = { version = "=3.0.0-rc.1", features = ["all-keys", "keys-bip39", "rusqlite"] } +bdk_wallet = { version = "=3.0.0-rc.2", 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 = { git = "https://github.com/reez/bdk-kyoto", branch = "rc1" } From 71f9583d446fd2e6a5db35694c662e1ede4976ae Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 30 Mar 2026 13:02:08 -0500 Subject: [PATCH 13/15] feat(store): expose pre-v1 sqlite migration helper --- bdk-ffi/src/error.rs | 34 ++++++++++++++++++++++++++++++++++ bdk-ffi/src/store.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index ae88e687..b7e13767 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -20,6 +20,7 @@ use bdk_wallet::descriptor::DescriptorError as BdkDescriptorError; use bdk_wallet::error::BuildFeeBumpError; use bdk_wallet::error::CreateTxError as BdkCreateTxError; use bdk_wallet::keys::bip39::Error as BdkBip39Error; +use bdk_wallet::migration::PreV1MigrationError as BdkPreV1MigrationError; use bdk_wallet::miniscript::descriptor::DescriptorKeyParseError as BdkDescriptorKeyParseError; use bdk_wallet::miniscript::psbt::Error as BdkPsbtFinalizeError; #[allow(deprecated)] @@ -585,6 +586,21 @@ pub enum PersistenceError { Reason { error_message: String }, } +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum PreV1MigrationError { + #[error("migration helper is only available for sqlite-backed persisters")] + SqliteOnly, + + #[error("sqlite migration error: {error_message}")] + Sqlite { error_message: String }, + + #[error("invalid keychain: {keychain}")] + InvalidKeychain { keychain: String }, + + #[error("invalid checksum: {error_message}")] + InvalidChecksum { error_message: String }, +} + #[derive(Debug, thiserror::Error, uniffi::Error)] pub enum PsbtError { #[error("invalid magic")] @@ -1339,6 +1355,24 @@ impl From for PersistenceError { } } +impl From for PreV1MigrationError { + fn from(error: BdkPreV1MigrationError) -> Self { + match error { + BdkPreV1MigrationError::RusqliteError(error) => PreV1MigrationError::Sqlite { + error_message: error.to_string(), + }, + BdkPreV1MigrationError::InvalidKeychain(keychain) => { + PreV1MigrationError::InvalidKeychain { keychain } + } + BdkPreV1MigrationError::InvalidChecksum(error) => { + PreV1MigrationError::InvalidChecksum { + error_message: error.to_string(), + } + } + } + } +} + impl From for MiniscriptError { fn from(error: bdk_wallet::miniscript::Error) -> Self { use bdk_wallet::miniscript::Error as BdkMiniscriptError; diff --git a/bdk-ffi/src/store.rs b/bdk-ffi/src/store.rs index f4484ff1..6cfb6acc 100644 --- a/bdk-ffi/src/store.rs +++ b/bdk-ffi/src/store.rs @@ -1,6 +1,10 @@ -use crate::error::PersistenceError; +use crate::error::{PersistenceError, PreV1MigrationError}; use crate::types::ChangeSet; +use bdk_wallet::migration::{ + get_pre_v1_wallet_keychains as bdk_get_pre_v1_wallet_keychains, + PreV1WalletKeychain as BdkPreV1WalletKeychain, +}; use bdk_wallet::{rusqlite::Connection as BdkConnection, WalletPersister}; use std::ops::DerefMut; @@ -21,6 +25,17 @@ pub(crate) enum PersistenceType { Sql(Mutex), } +/// Metadata describing a keychain in a pre-v1 BDK SQLite wallet database. +#[derive(Debug, Clone, uniffi::Record)] +pub struct PreV1WalletKeychain { + /// The wallet keychain. + pub keychain: bdk_wallet::KeychainKind, + /// The last derivation index stored for the keychain. + pub last_derivation_index: u32, + /// The descriptor checksum associated with the keychain. + pub checksum: String, +} + /// Wallet backend implementations. #[derive(uniffi::Object)] pub struct Persister { @@ -54,6 +69,32 @@ impl Persister { inner: PersistenceType::Custom(persistence).into(), } } + + /// Retrieve keychain metadata from a pre-v1 BDK SQLite wallet database. + pub fn get_pre_v1_wallet_keychains( + &self, + ) -> Result, PreV1MigrationError> { + let mut lock = self.inner.lock().unwrap(); + match lock.deref_mut() { + PersistenceType::Sql(ref conn) => { + let mut conn_lock = conn.lock().unwrap(); + bdk_get_pre_v1_wallet_keychains(conn_lock.deref_mut()) + .map(|keychains| keychains.into_iter().map(Into::into).collect()) + .map_err(Into::into) + } + PersistenceType::Custom(_) => Err(PreV1MigrationError::SqliteOnly), + } + } +} + +impl From for PreV1WalletKeychain { + fn from(value: BdkPreV1WalletKeychain) -> Self { + Self { + keychain: value.keychain, + last_derivation_index: value.last_derivation_index, + checksum: value.checksum, + } + } } impl WalletPersister for PersistenceType { From 3b0a1e327dd2c44f2fd500cfb648d3e08ecad836 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 13 Apr 2026 10:21:24 -0500 Subject: [PATCH 14/15] deps: bump bdk_wallet to 3.0.0 --- bdk-ffi/Cargo.lock | 14 +++++++------- bdk-ffi/Cargo.toml | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index 3ce97c75..1e6dd7d4 100644 --- a/bdk-ffi/Cargo.lock +++ b/bdk-ffi/Cargo.lock @@ -119,7 +119,7 @@ dependencies = [ [[package]] name = "bdk-ffi" -version = "3.0.0-rc.2" +version = "3.0.0" dependencies = [ "assert_matches", "bdk_electrum", @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "bdk_chain" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5d691fd092aacec7e05046b7d04897d58d6d65ed3152cb6cf65dababcfabed" +checksum = "c290eff038799a8ac0c5a82b6160a9ca456baa299a6f22b262c771342d2846c0" dependencies = [ "bdk_core", "bitcoin", @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "bdk_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dbbe4aad0c898bfeb5253c222be3ea3dccfb380a07e72c87e3e4ed6664a6753" +checksum = "cb3028782f6bf14a6df987244333d34e6b272b5a40a53e4879ec2dfd82275a3a" dependencies = [ "bitcoin", "hashbrown 0.14.5", @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "3.0.0-rc.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b989a12f0398a844bf22b14a197e2676aea2775aabca52057627f596ad3a0ef" +checksum = "67f3c4f9526d22374fca5b7ff1d6bf8d921ab56db2dac8df66a2c5561b31d4ef" dependencies = [ "bdk_chain", "bip39", diff --git a/bdk-ffi/Cargo.toml b/bdk-ffi/Cargo.toml index 8b59ab18..e10032ba 100644 --- a/bdk-ffi/Cargo.toml +++ b/bdk-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk-ffi" -version = "3.0.0-rc.2" +version = "3.0.0" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" edition = "2018" @@ -15,7 +15,7 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" [dependencies] -bdk_wallet = { version = "=3.0.0-rc.2", features = ["all-keys", "keys-bip39", "rusqlite"] } +bdk_wallet = { version = "=3.0.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 = { git = "https://github.com/reez/bdk-kyoto", branch = "rc1" } From 27be17fcae5b8ab5883a578c2e9f65c4bb89da1b Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 14 Apr 2026 10:38:41 -0500 Subject: [PATCH 15/15] deps: bump bdk_kyoto to 0.16.0 --- bdk-ffi/Cargo.lock | 5 +++-- bdk-ffi/Cargo.toml | 2 +- bdk-ffi/src/kyoto.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index 1e6dd7d4..fe4c82a3 100644 --- a/bdk-ffi/Cargo.lock +++ b/bdk-ffi/Cargo.lock @@ -176,8 +176,9 @@ dependencies = [ [[package]] name = "bdk_kyoto" -version = "0.15.4" -source = "git+https://github.com/reez/bdk-kyoto?branch=rc1#7303e1e2d2d944a7ba2b13bcc3d92c605b0f1afa" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa6765bbb7d82c0b99ddd13d583b1157137b9e6a6ba3593bbbf75897599f3895" dependencies = [ "bdk_wallet", "bip157", diff --git a/bdk-ffi/Cargo.toml b/bdk-ffi/Cargo.toml index e10032ba..f9fd258b 100644 --- a/bdk-ffi/Cargo.toml +++ b/bdk-ffi/Cargo.toml @@ -18,7 +18,7 @@ path = "uniffi-bindgen.rs" bdk_wallet = { version = "=3.0.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 = { git = "https://github.com/reez/bdk-kyoto", branch = "rc1" } +bdk_kyoto = { version = "0.16.0" } uniffi = { version = "=0.30.0", features = ["cli"]} thiserror = "2.0.17" diff --git a/bdk-ffi/src/kyoto.rs b/bdk-ffi/src/kyoto.rs index 18c6cb05..78e360cd 100644 --- a/bdk-ffi/src/kyoto.rs +++ b/bdk-ffi/src/kyoto.rs @@ -51,7 +51,7 @@ pub struct CbfClient { sender: Arc, info_rx: Mutex>, warning_rx: Mutex>, - update_rx: Mutex, + update_rx: Mutex>, } /// A [`CbfNode`] gathers transactions for a [`Wallet`].