diff --git a/Cargo.toml b/Cargo.toml index f180c8c6ef..e232bb8e35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ miniscript = { version = "9.0.0", features = ["serde"] } bitcoin = { version = "0.29" , features = ["serde", "base64", "rand"]} serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } -bdk_chain = { git = "https://github.com/LLFourn/bdk_core_staging.git", rev = "666ef9acdeb09af9d220a267f4784929886b09a6", features = ["miniscript", "serde"] } +bdk_chain = { git = "https://github.com/LLFourn/bdk_core_staging.git", rev = "842cfc8b50e1eca5b1d83b598eb3d2e4ae2147ca", features = ["miniscript", "serde"] } rand = "^0.8" # Optional dependencies @@ -37,6 +37,7 @@ js-sys = "0.3" [features] default = ["std"] std = [] +file-store = [ "std", "bdk_chain/file_store"] compiler = ["miniscript/compiler"] all-keys = ["keys-bip39"] keys-bip39 = ["bip39"] diff --git a/src/wallet/export.rs b/src/wallet/export.rs index 9db268eb9b..469a50ac97 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -117,8 +117,8 @@ impl FullyNodedExport { /// /// If the database is empty or `include_blockheight` is false, the `blockheight` field /// returned will be `0`. - pub fn export_wallet( - wallet: &Wallet, + pub fn export_wallet( + wallet: &Wallet, label: &str, include_blockheight: bool, ) -> Result { @@ -231,8 +231,8 @@ mod test { descriptor: &str, change_descriptor: Option<&str>, network: Network, - ) -> Wallet { - let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap(); + ) -> Wallet<()> { + let mut wallet = Wallet::new(descriptor, change_descriptor, (), network).unwrap(); let transaction = Transaction { input: vec![], output: vec![], diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 461adc4084..f73b7868a5 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -19,7 +19,7 @@ use alloc::{ sync::Arc, vec::Vec, }; -use bdk_chain::{keychain::KeychainTracker, sparse_chain, BlockId, ConfirmationTime}; +use bdk_chain::{keychain::KeychainTracker, sparse_chain, BlockId, ConfirmationTime, chain_graph}; use bitcoin::secp256k1::Secp256k1; use core::fmt; use core::ops::Deref; @@ -45,6 +45,7 @@ pub(crate) mod utils; #[cfg(feature = "hardware-signer")] #[cfg_attr(docsrs, doc(cfg(feature = "hardware-signer")))] pub mod hardwaresigner; +pub mod persist; pub use utils::IsDust; @@ -79,13 +80,12 @@ const COINBASE_MATURITY: u32 = 100; /// [`Database`]: crate::database::Database /// [`signer`]: crate::signer #[derive(Debug)] -pub struct Wallet { +pub struct Wallet { signers: Arc, change_signers: Arc, keychain_tracker: KeychainTracker, - + persist: persist::Persist, network: Network, - secp: SecpCtx, } @@ -138,18 +138,58 @@ impl fmt::Display for AddressInfo { } impl Wallet { + /// Creates a wallet that does not persist data. + pub fn new_no_persist( + descriptor: E, + change_descriptor: Option, + network: Network, + ) -> Result { + Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { + NewError::Descriptor(e) => e, + NewError::Persist(_) => unreachable!("no persistence so it can't fail") + }) + } +} + +#[derive(Debug)] +pub enum NewError

{ + Descriptor(crate::descriptor::DescriptorError), + Persist(P), +} + +impl

core::fmt::Display for NewError

+where + P: core::fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NewError::Descriptor(e) => e.fmt(f), + NewError::Persist(e) => write!(f, "failed to load wallet from database: {}", e), + } + } +} + +#[cfg(feautre = "std")] +impl std::error::Error for NewError

{} + +impl Wallet { /// Create a wallet. /// /// The only way this can fail is if the descriptors passed in do not match the checksums in `database`. pub fn new( descriptor: E, change_descriptor: Option, + mut db: D, network: Network, - ) -> Result { + ) -> Result> + where + D: persist::Backend, + { let secp = Secp256k1::new(); let mut keychain_tracker = KeychainTracker::default(); - let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)?; + let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network) + .map_err(NewError::Descriptor)?; keychain_tracker .txout_index .add_keychain(KeychainKind::External, descriptor.clone()); @@ -157,7 +197,8 @@ impl Wallet { let change_signers = match change_descriptor { Some(desc) => { let (change_descriptor, change_keymap) = - into_wallet_descriptor_checked(desc, &secp, network)?; + into_wallet_descriptor_checked(desc, &secp, network) + .map_err(NewError::Descriptor)?; let change_signers = Arc::new(SignersContainer::build( change_keymap, @@ -174,10 +215,17 @@ impl Wallet { None => Arc::new(SignersContainer::new()), }; + db.load_into_keychain_tracker(&mut keychain_tracker) + .map_err(NewError::Persist)?; + + + let persist = persist::Persist::new(db); + Ok(Wallet { signers, change_signers, network, + persist, secp, keychain_tracker, }) @@ -370,7 +418,11 @@ impl Wallet { let inputs = tx .input .iter() - .map(|txin| graph.txout(txin.previous_output).map(|txout| txout.value)) + .map(|txin| { + graph + .get_txout(txin.previous_output) + .map(|txout| txout.value) + }) .sum::>(); let outputs = tx.output.iter().map(|txout| txout.value).sum(); let fee = inputs.map(|inputs| inputs.saturating_sub(outputs)); @@ -389,8 +441,11 @@ impl Wallet { pub fn insert_checkpoint( &mut self, block_id: BlockId, - ) -> Result { - self.keychain_tracker.insert_checkpoint(block_id) + ) -> Result { + let changeset = self.keychain_tracker.insert_checkpoint(block_id)?; + let changed = changeset.is_empty(); + self.persist.stage(changeset); + Ok(changed) } /// Add a transaction to the wallet. Will only work if height <= latest checkpoint @@ -398,8 +453,11 @@ impl Wallet { &mut self, tx: Transaction, position: ConfirmationTime, - ) -> Result { - self.keychain_tracker.insert_tx(tx, Some(position)) + ) -> Result> { + let changeset = self.keychain_tracker.insert_tx(tx, position)?; + let changed = changeset.is_empty(); + self.persist.stage(changeset); + Ok(changed) } #[deprecated(note = "use Wallet::transactions instead")] @@ -407,7 +465,7 @@ impl Wallet { pub fn list_transactions(&self, include_raw: bool) -> Vec { self.keychain_tracker .chain() - .iter_txids() + .txids() .map(|(_, txid)| self.get_tx(*txid, include_raw).expect("must exist")) .collect() } @@ -418,14 +476,9 @@ impl Wallet { &self, ) -> impl DoubleEndedIterator + '_ { self.keychain_tracker - .chain() - .iter_txids() - .map(move |(pos, txid)| { - ( - *pos, - self.keychain_tracker.graph().tx(*txid).expect("must exist"), - ) - }) + .chain_graph() + .transactions_in_chain() + .map(|(pos, tx)| (*pos, tx)) } /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature @@ -449,7 +502,7 @@ impl Wallet { let is_coinbase = self .keychain_tracker .graph() - .tx(utxo.outpoint.txid) + .get_tx(utxo.outpoint.txid) .expect("must exist") .is_coin_base(); @@ -501,7 +554,7 @@ impl Wallet { /// # use bdk::{Wallet, KeychainKind}; /// # use bdk::bitcoin::Network; /// # use bdk::database::MemoryDatabase; - /// let wallet = Wallet::new("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet, MemoryDatabase::new())?; + /// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet, MemoryDatabase::new())?; /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* /// println!("secret_key: {}", secret_key); @@ -542,7 +595,7 @@ impl Wallet { /// ``` /// /// [`TxBuilder`]: crate::TxBuilder - pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm, CreateTx> { + pub fn build_tx(&mut self) -> TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, CreateTx> { TxBuilder { wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), params: TxParams::default(), @@ -950,7 +1003,7 @@ impl Wallet { pub fn build_fee_bump( &mut self, txid: Txid, - ) -> Result, Error> { + ) -> Result, Error> { let graph = self.keychain_tracker.graph(); let txout_index = &self.keychain_tracker.txout_index; let tx_and_height = self.keychain_tracker.chain_graph().get_tx_in_chain(txid); @@ -971,6 +1024,11 @@ impl Wallet { } let fee = graph.calculate_fee(&tx).ok_or(Error::FeeRateUnavailable)?; + if fee < 0 { + // It's available but it's wrong so let's say it's unavailable + return Err(Error::FeeRateUnavailable)?; + } + let fee = fee as u64; let feerate = FeeRate::from_wu(fee, tx.weight()); // remove the inputs from the tx and process them @@ -1525,7 +1583,7 @@ impl Wallet { .map_err(MiniscriptPsbtError::Conversion)?; let prev_output = utxo.outpoint; - if let Some(prev_tx) = self.keychain_tracker.graph().tx(prev_output.txid) { + if let Some(prev_tx) = self.keychain_tracker.graph().get_tx(prev_output.txid) { if desc.is_witness() || desc.is_taproot() { psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); } @@ -1644,7 +1702,7 @@ pub(crate) mod test { /// Return a fake wallet that appears to be funded for testing. pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { - let mut wallet = Wallet::new(descriptor, None, Network::Regtest).unwrap(); + let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Regtest).unwrap(); let address = wallet.get_address(AddressIndex::New).address; let tx = Transaction { @@ -2578,7 +2636,7 @@ pub(crate) mod test { #[test] fn test_create_tx_policy_path_no_csv() { let descriptors = get_test_wpkh(); - let mut wallet = Wallet::new(descriptors, None, Network::Regtest).unwrap(); + let mut wallet = Wallet::new_no_persist(descriptors, None, Network::Regtest).unwrap(); let tx = Transaction { version: 0, @@ -3914,7 +3972,7 @@ pub(crate) mod test { #[test] fn test_unused_address() { - let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet).unwrap(); assert_eq!( @@ -3930,7 +3988,7 @@ pub(crate) mod test { #[test] fn test_next_unused_address() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; - let mut wallet = Wallet::new(descriptor, None, Network::Testnet).unwrap(); + let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet).unwrap(); assert_eq!(wallet.derivation_index(KeychainKind::External), None); assert_eq!( @@ -3956,7 +4014,7 @@ pub(crate) mod test { #[test] fn test_peek_address_at_index() { - let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet).unwrap(); assert_eq!( @@ -3988,7 +4046,7 @@ pub(crate) mod test { #[test] fn test_peek_address_at_index_not_derivable() { - let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", + let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", None, Network::Testnet).unwrap(); assert_eq!( @@ -4009,7 +4067,7 @@ pub(crate) mod test { #[test] fn test_returns_index_and_address() { - let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", + let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet).unwrap(); // new index 0 @@ -4068,7 +4126,7 @@ pub(crate) mod test { fn test_get_address() { use crate::descriptor::template::Bip84; let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let mut wallet = Wallet::new( + let mut wallet = Wallet::new_no_persist( Bip84(key, KeychainKind::External), Some(Bip84(key, KeychainKind::Internal)), Network::Regtest, @@ -4094,7 +4152,8 @@ pub(crate) mod test { ); let mut wallet = - Wallet::new(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); + Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest) + .unwrap(); assert_eq!( wallet.get_internal_address(AddressIndex::New), @@ -4114,7 +4173,8 @@ pub(crate) mod test { let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); let mut wallet = - Wallet::new(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); + Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest) + .unwrap(); let mut used_set = HashSet::new(); @@ -4582,7 +4642,7 @@ pub(crate) mod test { // re-create the wallet with an empty db let wallet_empty = - Wallet::new(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap(); + Wallet::new_no_persist(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap(); // signing with an empty db means that we will only look at the psbt to infer the // derivation index @@ -4682,7 +4742,7 @@ pub(crate) mod test { #[test] fn test_spend_coinbase() { let descriptor = get_test_wpkh(); - let mut wallet = Wallet::new(descriptor, None, Network::Regtest).unwrap(); + let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Regtest).unwrap(); let confirmation_height = 5; wallet diff --git a/src/wallet/persist.rs b/src/wallet/persist.rs new file mode 100644 index 0000000000..8e5435fe94 --- /dev/null +++ b/src/wallet/persist.rs @@ -0,0 +1,95 @@ +use crate::KeychainKind; +use bdk_chain::{ + keychain::{KeychainChangeSet, KeychainTracker}, + ConfirmationTime, +}; + +#[derive(Debug)] +pub struct Persist

{ + backend: P, + stage: KeychainChangeSet, +} + +impl

Persist

{ + pub fn new(backend: P) -> Self { + Self { + backend, + stage: Default::default(), + } + } + + pub fn stage(&mut self, changeset: KeychainChangeSet) { + self.stage.append(changeset) + } + + pub fn staged(&self) -> &KeychainChangeSet { + &self.stage + } + + pub fn commit(&mut self) -> Result<(), P::WriteError> + where + P: Backend, + { + self.backend.apply_changeset(&self.stage)?; + self.stage = Default::default(); + Ok(()) + } +} + +pub trait Backend { + type WriteError; + type LoadError; + fn apply_changeset( + &mut self, + changeset: &KeychainChangeSet, + ) -> Result<(), Self::WriteError>; + fn load_into_keychain_tracker( + &mut self, + tracker: &mut KeychainTracker, + ) -> Result<(), Self::LoadError>; +} + +#[cfg(feature = "file-store")] +mod file_store { + use super::*; + use bdk_chain::file_store::{IterError, KeychainStore}; + + pub type FileStore = KeychainStore; + + impl Backend for FileStore { + type WriteError = std::io::Error; + type LoadError = IterError; + fn apply_changeset( + &mut self, + changeset: &KeychainChangeSet, + ) -> Result<(), Self::WriteError> { + self.append_changeset(changeset) + } + fn load_into_keychain_tracker( + &mut self, + tracker: &mut KeychainTracker, + ) -> Result<(), Self::LoadError> { + self.load_into_keychain_tracker(tracker) + } + } +} + +impl Backend for () { + type WriteError = (); + type LoadError = (); + fn apply_changeset( + &mut self, + _changeset: &KeychainChangeSet, + ) -> Result<(), Self::WriteError> { + Ok(()) + } + fn load_into_keychain_tracker( + &mut self, + _tracker: &mut KeychainTracker, + ) -> Result<(), Self::LoadError> { + Ok(()) + } +} + +#[cfg(feature = "file-store")] +pub use file_store::*; diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 64212db84b..7c0368217b 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -116,8 +116,8 @@ impl TxBuilderContext for BumpFee {} /// [`finish`]: Self::finish /// [`coin_selection`]: Self::coin_selection #[derive(Debug)] -pub struct TxBuilder<'a, Cs, Ctx> { - pub(crate) wallet: Rc>, +pub struct TxBuilder<'a, D, Cs, Ctx> { + pub(crate) wallet: Rc>>, pub(crate) params: TxParams, pub(crate) coin_selection: Cs, pub(crate) phantom: PhantomData, @@ -168,7 +168,7 @@ impl Default for FeePolicy { } } -impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> { +impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> { fn clone(&self) -> Self { TxBuilder { wallet: self.wallet.clone(), @@ -180,7 +180,7 @@ impl<'a, Cs: Clone, Ctx> Clone for TxBuilder<'a, Cs, Ctx> { } // methods supported by both contexts, for any CoinSelectionAlgorithm -impl<'a, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, Cs, Ctx> { +impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> { /// Set a custom fee rate pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self { self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate)); @@ -508,7 +508,7 @@ impl<'a, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, Cs, Ct pub fn coin_selection( self, coin_selection: P, - ) -> TxBuilder<'a, P, Ctx> { + ) -> TxBuilder<'a, D, P, Ctx> { TxBuilder { wallet: self.wallet, params: self.params, @@ -573,7 +573,7 @@ impl<'a, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, Cs, Ct } } -impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs, CreateTx> { +impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> { /// Replace the recipients already added with a new list pub fn set_recipients(&mut self, recipients: Vec<(Script, u64)>) -> &mut Self { self.params.recipients = recipients; @@ -644,7 +644,7 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs, CreateTx> { } // methods supported only by bump_fee -impl<'a> TxBuilder<'a, DefaultCoinSelectionAlgorithm, BumpFee> { +impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> { /// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this /// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet /// will attempt to find a change output to shrink instead.