diff --git a/bdk-ffi/Cargo.lock b/bdk-ffi/Cargo.lock index 1c534061..fe4c82a3 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" 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", @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "bdk_kyoto" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f35b9f8b3aa8c4647bec7a92b050c496742d955e0ac1edcb4e7c2deabf63c54" +checksum = "aa6765bbb7d82c0b99ddd13d583b1157137b9e6a6ba3593bbbf75897599f3895" dependencies = [ "bdk_wallet", "bip157", @@ -186,9 +186,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "2.3.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03f1e31ccc562f600981f747d2262b84428cbff52c9c9cdf14d15fb15bd2286" +checksum = "67f3c4f9526d22374fca5b7ff1d6bf8d921ab56db2dac8df66a2c5561b31d4ef" dependencies = [ "bdk_chain", "bip39", @@ -207,13 +207,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", ] @@ -269,6 +270,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" @@ -284,6 +294,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" @@ -330,6 +346,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 662ffe67..f9fd258b 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" 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", 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 = { version = "0.16.0" } 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..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")] @@ -698,6 +714,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}")] @@ -1333,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; @@ -1561,7 +1601,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/kyoto.rs b/bdk-ffi/src/kyoto.rs index 2323e94d..78e360cd 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; @@ -52,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`]. @@ -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/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 { 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..ab4f1ff6 100644 --- a/bdk-ffi/src/tx_builder.rs +++ b/bdk-ffi/src/tx_builder.rs @@ -1,20 +1,22 @@ 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; 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; use std::convert::{TryFrom, TryInto}; +use std::str::FromStr; use std::sync::Arc; type ChangeSpendPolicy = bdk_wallet::ChangeSpendPolicy; @@ -41,10 +43,12 @@ pub struct TxBuilder { locktime: Option, allow_dust: bool, version: Option, + sighash: Option, + ordering: TxOrdering, 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)] @@ -71,6 +75,8 @@ impl TxBuilder { locktime: None, allow_dust: false, version: None, + sighash: None, + ordering: TxOrdering::Shuffle, exclude_unconfirmed: false, exclude_below_confirmations: None, only_witness_utxo: false, @@ -371,6 +377,30 @@ 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() + })) + } + + /// 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. /// @@ -469,7 +499,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, @@ -545,6 +614,10 @@ impl TxBuilder { if let Some(version) = self.version { tx_builder.version(version); } + if let Some(sighash) = self.sighash { + tx_builder.sighash(sighash); + } + tx_builder.ordering(self.ordering.into()); if self.exclude_unconfirmed { tx_builder.exclude_unconfirmed(); } @@ -554,10 +627,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)?; @@ -576,6 +659,8 @@ pub struct BumpFeeTxBuilder { locktime: Option, allow_dust: bool, version: Option, + sighash: Option, + ordering: TxOrdering, } #[uniffi::export] @@ -590,6 +675,8 @@ impl BumpFeeTxBuilder { locktime: None, allow_dust: false, version: None, + sighash: None, + ordering: TxOrdering::Shuffle, } } @@ -653,6 +740,30 @@ 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() + })) + } + + /// 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). @@ -683,6 +794,10 @@ impl BumpFeeTxBuilder { if let Some(version) = self.version { tx_builder.version(version); } + if let Some(sighash) = self.sighash { + tx_builder.sighash(sighash); + } + tx_builder.ordering(self.ordering.into()); let psbt: BdkPsbt = tx_builder.finish()?; @@ -701,3 +816,34 @@ pub enum ChangeSpendPolicy { /// Only use non-change outputs (see [`bdk_wallet::TxBuilder::do_not_spend_change`]). 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(), + }) +} diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index f432988a..6e17278e 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; @@ -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,6 +1362,9 @@ impl From for bdk_wallet::ChangeSet { local_chain, tx_graph, indexer, + locked_outpoints: bdk_wallet::locked_outpoints::ChangeSet { + outpoints: locked_outpoints, + }, } } } @@ -1336,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, @@ -1343,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 47da506c..01f07c2c 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 { @@ -348,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. @@ -361,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 { @@ -541,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. @@ -561,6 +637,23 @@ 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 + /// (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 @@ -605,6 +698,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()