diff --git a/crates/chain/src/rusqlite_impl.rs b/crates/chain/src/rusqlite_impl.rs index 0ce919d86..50897491a 100644 --- a/crates/chain/src/rusqlite_impl.rs +++ b/crates/chain/src/rusqlite_impl.rs @@ -70,6 +70,25 @@ pub fn migrate_schema( Ok(()) } +/// Serde! +pub struct SerdeImpl(pub T); + +impl FromSql for SerdeImpl { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + serde_json::from_str(value.as_str()?) + .map(SerdeImpl) + .map_err(from_sql_error) + } +} + +impl ToSql for SerdeImpl { + fn to_sql(&self) -> rusqlite::Result> { + serde_json::to_string(&self.0) + .map(Into::into) + .map_err(to_sql_error) + } +} + impl FromSql for Impl { fn column_result(value: ValueRef<'_>) -> FromSqlResult { bitcoin::Txid::from_str(value.as_str()?) diff --git a/crates/wallet/src/types.rs b/crates/wallet/src/types.rs index 6ed17b575..e1fda6c47 100644 --- a/crates/wallet/src/types.rs +++ b/crates/wallet/src/types.rs @@ -18,6 +18,25 @@ use bitcoin::{psbt, Weight}; use serde::{Deserialize, Serialize}; +/// Trait that determines if a keychain variant is trusted. +pub trait WalletKeychain { + const DEFAULT_CHANGE_VARIANT: Self; + + /// Returns whether the keychain variant is trusted. + fn is_trusted_variant(&self) -> bool; +} + +impl WalletKeychain for KeychainKind { + const DEFAULT_CHANGE_VARIANT: Self = KeychainKind::Internal; + + fn is_trusted_variant(&self) -> bool { + match self { + KeychainKind::External => false, + KeychainKind::Internal => true, + } + } +} + /// Types of keychains #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub enum KeychainKind { @@ -50,13 +69,13 @@ impl AsRef<[u8]> for KeychainKind { /// /// [`Wallet`]: crate::Wallet #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct LocalOutput { +pub struct LocalOutput { /// Reference to a transaction output pub outpoint: OutPoint, /// Transaction output pub txout: TxOut, /// Type of keychain - pub keychain: KeychainKind, + pub keychain: K, /// Whether this UTXO is spent or not pub is_spent: bool, /// The derivation index for the script pubkey in the wallet @@ -67,21 +86,21 @@ pub struct LocalOutput { /// A [`Utxo`] with its `satisfaction_weight`. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct WeightedUtxo { +pub struct WeightedUtxo { /// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to /// properly maintain the feerate when adding this input to a transaction during coin selection. /// /// [weight units]: https://en.bitcoin.it/wiki/Weight_units pub satisfaction_weight: Weight, /// The UTXO - pub utxo: Utxo, + pub utxo: Utxo, } #[derive(Debug, Clone, PartialEq, Eq)] /// An unspent transaction output (UTXO). -pub enum Utxo { +pub enum Utxo { /// A UTXO owned by the local wallet. - Local(LocalOutput), + Local(LocalOutput), /// A UTXO owned by another wallet. Foreign { /// The location of the output. @@ -94,7 +113,7 @@ pub enum Utxo { }, } -impl Utxo { +impl Utxo { /// Get the location of the UTXO pub fn outpoint(&self) -> OutPoint { match &self { diff --git a/crates/wallet/src/wallet/changeset.rs b/crates/wallet/src/wallet/changeset.rs index 46b2f4321..22565ee07 100644 --- a/crates/wallet/src/wallet/changeset.rs +++ b/crates/wallet/src/wallet/changeset.rs @@ -1,19 +1,23 @@ use bdk_chain::{ - indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime, Merge, + collections::BTreeMap, indexed_tx_graph, keychain_txout, local_chain, tx_graph, + ConfirmationBlockTime, Merge, }; use miniscript::{Descriptor, DescriptorPublicKey}; +use serde::{de::DeserializeOwned, Serialize}; type IndexedTxGraphChangeSet = indexed_tx_graph::ChangeSet; /// A changeset for [`Wallet`](crate::Wallet). -#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(bound( + deserialize = "K: Ord + serde::Deserialize<'de>", + serialize = "K: Ord + serde::Serialize" +))] #[non_exhaustive] -pub struct ChangeSet { - /// Descriptor for recipient addresses. - pub descriptor: Option>, - /// Descriptor for change addresses. - pub change_descriptor: Option>, +pub struct ChangeSet { + /// Descriptor for addresses. + pub descriptors: BTreeMap>, /// Stores the network type of the transaction data. pub network: Option, /// Changes to the [`LocalChain`](local_chain::LocalChain). @@ -24,24 +28,37 @@ pub struct ChangeSet { pub indexer: keychain_txout::ChangeSet, } -impl Merge for ChangeSet { +impl Default for ChangeSet { + fn default() -> Self { + Self { + descriptors: Default::default(), + network: Default::default(), + local_chain: Default::default(), + tx_graph: Default::default(), + indexer: Default::default(), + } + } +} + +impl Merge for ChangeSet { /// Merge another [`ChangeSet`] into itself. fn merge(&mut self, other: Self) { - if other.descriptor.is_some() { - debug_assert!( - self.descriptor.is_none() || self.descriptor == other.descriptor, - "descriptor must never change" - ); - self.descriptor = other.descriptor; - } - if other.change_descriptor.is_some() { - debug_assert!( - self.change_descriptor.is_none() - || self.change_descriptor == other.change_descriptor, - "change descriptor must never change" - ); - self.change_descriptor = other.change_descriptor; + for (k, descriptor) in other.descriptors { + use bdk_chain::collections::btree_map::Entry; + match self.descriptors.entry(k) { + Entry::Vacant(entry) => { + entry.insert(descriptor); + } + Entry::Occupied(entry) => { + debug_assert_eq!( + entry.get(), + &descriptor, + "changeset must not replace existing descriptor under keychain" + ); + } + } } + if other.network.is_some() { debug_assert!( self.network.is_none() || self.network == other.network, @@ -56,8 +73,7 @@ impl Merge for ChangeSet { } fn is_empty(&self) -> bool { - self.descriptor.is_none() - && self.change_descriptor.is_none() + self.descriptors.is_empty() && self.network.is_none() && self.local_chain.is_empty() && self.tx_graph.is_empty() @@ -66,11 +82,15 @@ impl Merge for ChangeSet { } #[cfg(feature = "rusqlite")] -impl ChangeSet { +impl ChangeSet { /// Schema name for wallet. pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet"; /// Name of table to store wallet descriptors and network. pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet"; + /// Name of table to store wallet network. + pub const NETWORK_TABLE_NAME: &'static str = "bdk_network"; + /// Name of table to store descriptors and an associated label (used as the table key). + pub const DESCRIPTOR_TABLE_NAME: &'static str = "bdk_descriptor"; /// Initialize sqlite tables for wallet schema & table. fn init_wallet_sqlite_tables( @@ -82,39 +102,86 @@ impl ChangeSet { descriptor TEXT, \ change_descriptor TEXT, \ network TEXT \ - ) STRICT;", + ) STRICT", Self::WALLET_TABLE_NAME, )]; - crate::rusqlite_impl::migrate_schema(db_tx, Self::WALLET_SCHEMA_NAME, &[schema_v0]) + // TODO: We can't really migrate from V0 to V1 unless type 'K' provides some sort of + // mapping from 'KeychainKind'. + let schema_v1: &[&str] = &[ + &format!( + "CREATE TABLE {} ( \ + id INTEGER PRIMARY KEY NOT NULL CHECK (id = 0), \ + network TEXT NOT NULL \ + ) STRICT", + Self::NETWORK_TABLE_NAME, + ), + &format!( + "CREATE TABLE {} ( \ + label TEXT PRIMARY KEY NOT NULL, \ + descriptor TEXT NOT NULL + ) STRICT", + Self::DESCRIPTOR_TABLE_NAME, + ), + ]; + crate::rusqlite_impl::migrate_schema( + db_tx, + Self::WALLET_SCHEMA_NAME, + &[schema_v0, schema_v1], + ) } /// Recover a [`ChangeSet`] from sqlite database. pub fn from_sqlite(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result { Self::init_wallet_sqlite_tables(db_tx)?; use chain::rusqlite::OptionalExtension; + use chain::rusqlite_impl::SerdeImpl; use chain::Impl; let mut changeset = Self::default(); - let mut wallet_statement = db_tx.prepare(&format!( - "SELECT descriptor, change_descriptor, network FROM {}", - Self::WALLET_TABLE_NAME, + let mut statement = db_tx.prepare(&format!( + "SELECT label, descriptor FROM {}", + Self::DESCRIPTOR_TABLE_NAME ))?; - let row = wallet_statement - .query_row([], |row| { - Ok(( - row.get::<_, Impl>>("descriptor")?, - row.get::<_, Impl>>("change_descriptor")?, - row.get::<_, Impl>("network")?, - )) - }) + let row_iter = statement.query_map([], |row| { + Ok(( + row.get::<_, SerdeImpl>("label")?, + row.get::<_, Impl>>("descriptor")?, + )) + })?; + for row in row_iter { + let (SerdeImpl(label), Impl(descriptor)) = row?; + changeset.descriptors.insert(label, descriptor); + } + + let mut statement = + db_tx.prepare(&format!("SELECT network FROM {}", Self::NETWORK_TABLE_NAME))?; + let row = statement + .query_row([], |row| row.get::<_, Impl>("network")) .optional()?; - if let Some((Impl(desc), Impl(change_desc), Impl(network))) = row { - changeset.descriptor = Some(desc); - changeset.change_descriptor = Some(change_desc); + if let Some(Impl(network)) = row { changeset.network = Some(network); } + // let mut wallet_statement = db_tx.prepare(&format!( + // "SELECT descriptor, change_descriptor, network FROM {}", + // Self::WALLET_TABLE_NAME, + // ))?; + // let row = wallet_statement + // .query_row([], |row| { + // Ok(( + // row.get::<_, Impl>>("descriptor")?, + // row.get::<_, Impl>>("change_descriptor")?, + // row.get::<_, Impl>("network")?, + // )) + // }) + // .optional()?; + // if let Some((Impl(desc), Impl(change_desc), Impl(network))) = row { + // changeset.descriptor = Some(desc); + // changeset.change_descriptor = Some(change_desc); + // changeset.network = Some(network); + // } + changeset.local_chain = local_chain::ChangeSet::from_sqlite(db_tx)?; changeset.tx_graph = tx_graph::ChangeSet::<_>::from_sqlite(db_tx)?; changeset.indexer = keychain_txout::ChangeSet::from_sqlite(db_tx)?; @@ -129,41 +196,63 @@ impl ChangeSet { ) -> chain::rusqlite::Result<()> { Self::init_wallet_sqlite_tables(db_tx)?; use chain::rusqlite::named_params; + use chain::rusqlite_impl::SerdeImpl; use chain::Impl; - let mut descriptor_statement = db_tx.prepare_cached(&format!( - "INSERT INTO {}(id, descriptor) VALUES(:id, :descriptor) ON CONFLICT(id) DO UPDATE SET descriptor=:descriptor", - Self::WALLET_TABLE_NAME, + let mut statement = db_tx.prepare_cached(&format!( + "INSERT INTO {}(label, descriptor) VALUES(:label, descriptor) ON CONFLICT(label) DO UPDATE SET descriptor=:descriptor", + Self::DESCRIPTOR_TABLE_NAME, ))?; - if let Some(descriptor) = &self.descriptor { - descriptor_statement.execute(named_params! { - ":id": 0, + for (label, descriptor) in &self.descriptors { + statement.execute(named_params! { + ":label": SerdeImpl(label), ":descriptor": Impl(descriptor.clone()), })?; } - - let mut change_descriptor_statement = db_tx.prepare_cached(&format!( - "INSERT INTO {}(id, change_descriptor) VALUES(:id, :change_descriptor) ON CONFLICT(id) DO UPDATE SET change_descriptor=:change_descriptor", - Self::WALLET_TABLE_NAME, - ))?; - if let Some(change_descriptor) = &self.change_descriptor { - change_descriptor_statement.execute(named_params! { - ":id": 0, - ":change_descriptor": Impl(change_descriptor.clone()), - })?; - } - - let mut network_statement = db_tx.prepare_cached(&format!( + let mut statement = db_tx.prepare_cached(&format!( "INSERT INTO {}(id, network) VALUES(:id, :network) ON CONFLICT(id) DO UPDATE SET network=:network", - Self::WALLET_TABLE_NAME, + Self::NETWORK_TABLE_NAME, ))?; if let Some(network) = self.network { - network_statement.execute(named_params! { + statement.execute(named_params! { ":id": 0, ":network": Impl(network), })?; } + // let mut descriptor_statement = db_tx.prepare_cached(&format!( + // "INSERT INTO {}(id, descriptor) VALUES(:id, :descriptor) ON CONFLICT(id) DO UPDATE SET descriptor=:descriptor", + // Self::WALLET_TABLE_NAME, + // ))?; + // if let Some(descriptor) = &self.descriptor { + // descriptor_statement.execute(named_params! { + // ":id": 0, + // ":descriptor": Impl(descriptor.clone()), + // })?; + // } + // + // let mut change_descriptor_statement = db_tx.prepare_cached(&format!( + // "INSERT INTO {}(id, change_descriptor) VALUES(:id, :change_descriptor) ON CONFLICT(id) DO UPDATE SET change_descriptor=:change_descriptor", + // Self::WALLET_TABLE_NAME, + // ))?; + // if let Some(change_descriptor) = &self.change_descriptor { + // change_descriptor_statement.execute(named_params! { + // ":id": 0, + // ":change_descriptor": Impl(change_descriptor.clone()), + // })?; + // } + // + // let mut network_statement = db_tx.prepare_cached(&format!( + // "INSERT INTO {}(id, network) VALUES(:id, :network) ON CONFLICT(id) DO UPDATE SET network=:network", + // Self::WALLET_TABLE_NAME, + // ))?; + // if let Some(network) = self.network { + // network_statement.execute(named_params! { + // ":id": 0, + // ":network": Impl(network), + // })?; + // } + self.local_chain.persist_to_sqlite(db_tx)?; self.tx_graph.persist_to_sqlite(db_tx)?; self.indexer.persist_to_sqlite(db_tx)?; @@ -171,7 +260,7 @@ impl ChangeSet { } } -impl From for ChangeSet { +impl From for ChangeSet { fn from(chain: local_chain::ChangeSet) -> Self { Self { local_chain: chain, @@ -180,7 +269,7 @@ impl From for ChangeSet { } } -impl From for ChangeSet { +impl From for ChangeSet { fn from(indexed_tx_graph: IndexedTxGraphChangeSet) -> Self { Self { tx_graph: indexed_tx_graph.tx_graph, @@ -190,7 +279,7 @@ impl From for ChangeSet { } } -impl From> for ChangeSet { +impl From> for ChangeSet { fn from(tx_graph: tx_graph::ChangeSet) -> Self { Self { tx_graph, @@ -199,7 +288,7 @@ impl From> for ChangeSet { } } -impl From for ChangeSet { +impl From for ChangeSet { fn from(indexer: keychain_txout::ChangeSet) -> Self { Self { indexer, diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 4cd900378..f9e62398d 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -180,16 +180,16 @@ pub enum Excess { /// Result of a successful coin selection #[derive(Debug)] -pub struct CoinSelectionResult { +pub struct CoinSelectionResult { /// List of outputs selected for use as inputs - pub selected: Vec, + pub selected: Vec>, /// Total fee amount for the selected utxos in satoshis pub fee_amount: u64, /// Remaining amount after deducing fees and outgoing outputs pub excess: Excess, } -impl CoinSelectionResult { +impl CoinSelectionResult { /// The total value of the inputs selected. pub fn selected_amount(&self) -> u64 { self.selected.iter().map(|u| u.txout().value.to_sat()).sum() @@ -227,14 +227,14 @@ pub trait CoinSelectionAlgorithm: core::fmt::Debug { /// accumulated from added outputs and transaction’s header. /// - `drain_script`: the script to use in case of change #[allow(clippy::too_many_arguments)] - fn coin_select( + fn coin_select( &self, - required_utxos: Vec, - optional_utxos: Vec, + required_utxos: Vec>, + optional_utxos: Vec>, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, - ) -> Result; + ) -> Result, Error>; } /// Simple and dumb coin selection @@ -245,14 +245,14 @@ pub trait CoinSelectionAlgorithm: core::fmt::Debug { pub struct LargestFirstCoinSelection; impl CoinSelectionAlgorithm for LargestFirstCoinSelection { - fn coin_select( + fn coin_select( &self, - required_utxos: Vec, - mut optional_utxos: Vec, + required_utxos: Vec>, + mut optional_utxos: Vec>, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, - ) -> Result { + ) -> Result, Error> { // We put the "required UTXOs" first and make sure the optional UTXOs are sorted, // initially smallest to largest, before being reversed with `.rev()`. let utxos = { @@ -275,14 +275,14 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { pub struct OldestFirstCoinSelection; impl CoinSelectionAlgorithm for OldestFirstCoinSelection { - fn coin_select( + fn coin_select( &self, - required_utxos: Vec, - mut optional_utxos: Vec, + required_utxos: Vec>, + mut optional_utxos: Vec>, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, - ) -> Result { + ) -> Result, Error> { // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from // oldest to newest according to blocktime // For utxo that doesn't exist in DB, they will have lowest priority to be selected @@ -329,12 +329,12 @@ pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Sc } } -fn select_sorted_utxos( - utxos: impl Iterator, +fn select_sorted_utxos( + utxos: impl Iterator)>, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, -) -> Result { +) -> Result, Error> { let mut selected_amount = 0; let mut fee_amount = 0; let selected = utxos @@ -378,16 +378,16 @@ fn select_sorted_utxos( #[derive(Debug, Clone)] // Adds fee information to an UTXO. -struct OutputGroup { - weighted_utxo: WeightedUtxo, +struct OutputGroup { + weighted_utxo: WeightedUtxo, // Amount of fees for spending a certain utxo, calculated using a certain FeeRate fee: u64, // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it effective_value: i64, } -impl OutputGroup { - fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self { +impl OutputGroup { + fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self { let fee = (fee_rate * (TxIn::default() .segwit_weight() @@ -430,23 +430,23 @@ impl BranchAndBoundCoinSelection { const BNB_TOTAL_TRIES: usize = 100_000; impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { - fn coin_select( + fn coin_select( &self, - required_utxos: Vec, - optional_utxos: Vec, + required_utxos: Vec>, + optional_utxos: Vec>, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, - ) -> Result { + ) -> Result, Error> { // Mapping every (UTXO, usize) to an output group - let required_utxos: Vec = required_utxos + let required_utxos: Vec> = required_utxos .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) .collect(); // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative // effective value - let optional_utxos: Vec = optional_utxos + let optional_utxos: Vec> = optional_utxos .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) .filter(|u| u.effective_value.is_positive()) @@ -534,17 +534,17 @@ impl BranchAndBoundCoinSelection { // TODO: make this more Rust-onic :) // (And perhaps refactor with less arguments?) #[allow(clippy::too_many_arguments)] - fn bnb( + fn bnb( &self, - required_utxos: Vec, - mut optional_utxos: Vec, + required_utxos: Vec>, + mut optional_utxos: Vec>, mut curr_value: i64, mut curr_available_value: i64, target_amount: i64, cost_of_change: u64, drain_script: &Script, fee_rate: FeeRate, - ) -> Result { + ) -> Result, Error> { // current_selection[i] will contain true if we are using optional_utxos[i], // false otherwise. Note that current_selection.len() could be less than // optional_utxos.len(), it just means that we still haven't decided if we should keep @@ -635,7 +635,7 @@ impl BranchAndBoundCoinSelection { .into_iter() .zip(best_selection) .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None }) - .collect::>(); + .collect::>>(); let selected_amount = best_selection_value.unwrap(); @@ -653,11 +653,11 @@ impl BranchAndBoundCoinSelection { )) } - fn calculate_cs_result( - mut selected_utxos: Vec, - mut required_utxos: Vec, + fn calculate_cs_result( + mut selected_utxos: Vec>, + mut required_utxos: Vec>, excess: Excess, - ) -> CoinSelectionResult { + ) -> CoinSelectionResult { selected_utxos.append(&mut required_utxos); let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::(); let selected = selected_utxos @@ -674,24 +674,24 @@ impl BranchAndBoundCoinSelection { } // Pull UTXOs at random until we have enough to meet the target -pub(crate) fn single_random_draw( - required_utxos: Vec, - optional_utxos: Vec, +pub(crate) fn single_random_draw( + required_utxos: Vec>, + optional_utxos: Vec>, target_amount: u64, drain_script: &Script, fee_rate: FeeRate, rng: &mut impl RngCore, -) -> CoinSelectionResult { +) -> CoinSelectionResult { let target_amount = target_amount .try_into() .expect("Bitcoin amount to fit into i64"); - let required_utxos: Vec = required_utxos + let required_utxos: Vec> = required_utxos .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) .collect(); - let mut optional_utxos: Vec = optional_utxos + let mut optional_utxos: Vec> = optional_utxos .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) .collect(); @@ -728,9 +728,9 @@ pub(crate) fn single_random_draw( /// Remove duplicate UTXOs. /// /// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept. -pub(crate) fn filter_duplicates(required: I, optional: I) -> (I, I) +pub(crate) fn filter_duplicates(required: I, optional: I) -> (I, I) where - I: IntoIterator + FromIterator, + I: IntoIterator> + FromIterator>, { let mut visited = HashSet::::new(); let required = required diff --git a/crates/wallet/src/wallet/error.rs b/crates/wallet/src/wallet/error.rs index b6c9375a4..96b734b34 100644 --- a/crates/wallet/src/wallet/error.rs +++ b/crates/wallet/src/wallet/error.rs @@ -11,10 +11,10 @@ //! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet) +use crate::descriptor; use crate::descriptor::policy::PolicyError; use crate::descriptor::DescriptorError; use crate::wallet::coin_selection; -use crate::{descriptor, KeychainKind}; use alloc::string::String; use bitcoin::{absolute, psbt, Amount, OutPoint, Sequence, Txid}; use core::fmt; @@ -47,13 +47,13 @@ impl std::error::Error for MiniscriptPsbtError {} /// Error returned from [`TxBuilder::finish`] /// /// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish -pub enum CreateTxError { +pub enum CreateTxError { /// There was a problem with the descriptors passed in Descriptor(DescriptorError), /// There was a problem while extracting and manipulating policies Policy(PolicyError), /// Spending policy is not compatible with this [`KeychainKind`] - SpendingPolicyRequired(KeychainKind), + SpendingPolicyRequired(K), /// Requested invalid transaction version '0' Version0, /// Requested transaction version `1`, but at least `2` is needed to use OP_CSV @@ -104,11 +104,13 @@ pub enum CreateTxError { UnknownUtxo, /// Missing non_witness_utxo on foreign utxo for given `OutPoint` MissingNonWitnessUtxo(OutPoint), + /// Missing associated descriptor for keychain `K` + MissingDescriptor(K), /// Miniscript PSBT error MiniscriptPsbt(MiniscriptPsbtError), } -impl fmt::Display for CreateTxError { +impl fmt::Display for CreateTxError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Descriptor(e) => e.fmt(f), @@ -173,6 +175,9 @@ impl fmt::Display for CreateTxError { CreateTxError::MissingNonWitnessUtxo(outpoint) => { write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint) } + CreateTxError::MissingDescriptor(k) => { + write!(f, "Missing associated descriptor for keychain '{:?}'", k) + } CreateTxError::MiniscriptPsbt(err) => { write!(f, "Miniscript PSBT error: {}", err) } @@ -180,38 +185,38 @@ impl fmt::Display for CreateTxError { } } -impl From for CreateTxError { +impl From for CreateTxError { fn from(err: descriptor::error::Error) -> Self { CreateTxError::Descriptor(err) } } -impl From for CreateTxError { +impl From for CreateTxError { fn from(err: PolicyError) -> Self { CreateTxError::Policy(err) } } -impl From for CreateTxError { +impl From for CreateTxError { fn from(err: MiniscriptPsbtError) -> Self { CreateTxError::MiniscriptPsbt(err) } } -impl From for CreateTxError { +impl From for CreateTxError { fn from(err: psbt::Error) -> Self { CreateTxError::Psbt(err) } } -impl From for CreateTxError { +impl From for CreateTxError { fn from(err: coin_selection::Error) -> Self { CreateTxError::CoinSelection(err) } } #[cfg(feature = "std")] -impl std::error::Error for CreateTxError {} +impl std::error::Error for CreateTxError {} #[derive(Debug)] /// Error returned from [`Wallet::build_fee_bump`] diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 6dce7503b..0ab59c83b 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -113,15 +113,17 @@ 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, + wallet: &Wallet, label: &str, include_blockheight: bool, ) -> Result { let descriptor = wallet .public_descriptor(KeychainKind::External) + .ok_or("missing external descriptor")? .to_string_with_secret( &wallet .get_signers(KeychainKind::External) + .unwrap_or_default() .as_key_map(wallet.secp_ctx()), ); let descriptor = remove_checksum(descriptor); @@ -144,15 +146,17 @@ impl FullyNodedExport { blockheight, }; - let change_descriptor = { - let descriptor = wallet - .public_descriptor(KeychainKind::Internal) - .to_string_with_secret( + let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal) { + Some(descriptor) => { + let descriptor = descriptor.to_string_with_secret( &wallet .get_signers(KeychainKind::Internal) + .unwrap_or_default() .as_key_map(wallet.secp_ctx()), ); - Some(remove_checksum(descriptor)) + Some(remove_checksum(descriptor)) + } + None => None, }; if export.change_descriptor() != change_descriptor { @@ -223,10 +227,16 @@ mod test { use super::*; use crate::Wallet; - fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet { + fn get_test_wallet( + descriptor: &str, + change_descriptor: &str, + network: Network, + ) -> Wallet { use crate::wallet::Update; use bdk_chain::TxGraph; - let mut wallet = Wallet::create(descriptor.to_string(), change_descriptor.to_string()) + let mut wallet = Wallet::create() + .descriptor(KeychainKind::External, descriptor.to_string()) + .descriptor(KeychainKind::Internal, change_descriptor.to_string()) .network(network) .create_wallet_no_persist() .expect("must create wallet"); diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 7217d32fb..f6d9569ec 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -14,7 +14,8 @@ //! This module defines the [`Wallet`]. use crate::{ collections::{BTreeMap, HashMap}, - descriptor::check_wallet_descriptor, + descriptor::{check_wallet_descriptor, policy::Condition}, + DrainTo, }; use alloc::{ boxed::Box, @@ -42,8 +43,8 @@ use bitcoin::{ use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt}; use bitcoin::{constants::genesis_block, Amount}; use bitcoin::{secp256k1::Secp256k1, Weight}; -use chain::Staged; -use core::fmt; +use chain::{Indexer, Staged}; +use core::fmt::{self, Debug}; use core::mem; use core::ops::Deref; use rand_core::RngCore; @@ -52,6 +53,7 @@ use descriptor::error::Error as DescriptorError; use miniscript::{ descriptor::KeyMap, psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}, + Descriptor, DescriptorPublicKey, }; use bdk_chain::tx_graph::CalculateFeeError; @@ -110,18 +112,17 @@ const COINBASE_MATURITY: u32 = 100; /// [`signer`]: crate::signer /// [`take_staged`]: Wallet::take_staged #[derive(Debug)] -pub struct Wallet { - signers: Arc, - change_signers: Arc, +pub struct Wallet { + signers: BTreeMap>, chain: LocalChain, - indexed_graph: IndexedTxGraph>, - stage: ChangeSet, + indexed_graph: IndexedTxGraph>, + stage: ChangeSet, network: Network, secp: SecpCtx, } -impl Staged for Wallet { - type ChangeSet = ChangeSet; +impl Staged for Wallet { + type ChangeSet = ChangeSet; fn staged(&mut self) -> &mut Self::ChangeSet { &mut self.stage @@ -131,11 +132,11 @@ impl Staged for Wallet { /// An update to [`Wallet`]. /// /// It updates [`KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically. -#[derive(Debug, Clone, Default)] -pub struct Update { +#[derive(Debug, Clone)] +pub struct Update { /// Contains the last active derivation indices per keychain (`K`), which is used to update the /// [`KeychainTxOutIndex`]. - pub last_active_indices: BTreeMap, + pub last_active_indices: BTreeMap, /// Update for the wallet's internal [`TxGraph`]. pub graph: TxGraph, @@ -146,8 +147,18 @@ pub struct Update { pub chain: Option, } -impl From> for Update { - fn from(value: FullScanResult) -> Self { +impl Default for Update { + fn default() -> Self { + Self { + last_active_indices: Default::default(), + graph: Default::default(), + chain: Default::default(), + } + } +} + +impl From> for Update { + fn from(value: FullScanResult) -> Self { Self { last_active_indices: value.last_active_indices, graph: value.graph_update, @@ -156,7 +167,7 @@ impl From> for Update { } } -impl From for Update { +impl From for Update { fn from(value: SyncResult) -> Self { Self { last_active_indices: BTreeMap::new(), @@ -169,16 +180,16 @@ impl From for Update { /// A derived address and the index it was found at. /// For convenience this automatically derefs to `Address` #[derive(Debug, PartialEq, Eq)] -pub struct AddressInfo { +pub struct AddressInfo { /// Child index of this address pub index: u32, /// Address pub address: Address, /// Type of keychain - pub keychain: KeychainKind, + pub keychain: K, } -impl Deref for AddressInfo { +impl Deref for AddressInfo { type Target = Address; fn deref(&self) -> &Self::Target { @@ -186,7 +197,7 @@ impl Deref for AddressInfo { } } -impl fmt::Display for AddressInfo { +impl fmt::Display for AddressInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.address) } @@ -194,7 +205,7 @@ impl fmt::Display for AddressInfo { /// The error type when loading a [`Wallet`] from a [`ChangeSet`]. #[derive(Debug, PartialEq)] -pub enum LoadError { +pub enum LoadError { /// There was a problem with the passed-in descriptor(s). Descriptor(crate::descriptor::DescriptorError), /// Data loaded from persistence is missing network type. @@ -202,15 +213,15 @@ pub enum LoadError { /// Data loaded from persistence is missing genesis hash. MissingGenesis, /// Data loaded from persistence is missing descriptor. - MissingDescriptor(KeychainKind), + MissingDescriptor(K), /// Data loaded is unexpected. - Mismatch(LoadMismatch), + Mismatch(LoadMismatch), } -impl fmt::Display for LoadError { +impl fmt::Display for LoadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - LoadError::Descriptor(e) => e.fmt(f), + LoadError::Descriptor(e) => fmt::Display::fmt(e, f), LoadError::MissingNetwork => write!(f, "loaded data is missing network type"), LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"), LoadError::MissingDescriptor(k) => { @@ -222,11 +233,11 @@ impl fmt::Display for LoadError { } #[cfg(feature = "std")] -impl std::error::Error for LoadError {} +impl std::error::Error for LoadError {} /// Represents a mismatch with what is loaded and what is expected from [`LoadParams`]. #[derive(Debug, PartialEq)] -pub enum LoadMismatch { +pub enum LoadMismatch { /// Network does not match. Network { /// The network that is loaded. @@ -244,7 +255,7 @@ pub enum LoadMismatch { /// Descriptor's [`DescriptorId`](bdk_chain::DescriptorId) does not match. Descriptor { /// Keychain identifying the descriptor. - keychain: KeychainKind, + keychain: K, /// The loaded descriptor. loaded: ExtendedDescriptor, /// The expected descriptor. @@ -252,14 +263,14 @@ pub enum LoadMismatch { }, } -impl From for LoadError { - fn from(mismatch: LoadMismatch) -> Self { +impl From> for LoadError { + fn from(mismatch: LoadMismatch) -> Self { Self::Mismatch(mismatch) } } -impl From for LoadWithPersistError { - fn from(mismatch: LoadMismatch) -> Self { +impl From> for LoadWithPersistError { + fn from(mismatch: LoadMismatch) -> Self { Self::InvalidChangeSet(LoadError::Mismatch(mismatch)) } } @@ -281,7 +292,7 @@ pub enum ApplyBlockError { impl fmt::Display for ApplyBlockError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ApplyBlockError::CannotConnect(err) => err.fmt(f), + ApplyBlockError::CannotConnect(err) => fmt::Display::fmt(err, f), ApplyBlockError::UnexpectedConnectedToHash { expected_hash: block_hash, connected_to_hash: checkpoint_hash, @@ -297,7 +308,7 @@ impl fmt::Display for ApplyBlockError { #[cfg(feature = "std")] impl std::error::Error for ApplyBlockError {} -impl Wallet { +impl Wallet { /// Build a new [`Wallet`]. /// /// If you have previously created a wallet, use [`load`](Self::load) instead. @@ -326,60 +337,50 @@ impl Wallet { /// # Ok(()) /// # } /// ``` - pub fn create(descriptor: D, change_descriptor: D) -> CreateParams - where - D: IntoWalletDescriptor + Clone + 'static, - { - CreateParams::new(descriptor, change_descriptor) + pub fn create() -> CreateParams { + CreateParams::::new() } /// Create a new [`Wallet`] with given `params`. /// /// Refer to [`Wallet::create`] for more. - pub fn create_with_params(params: CreateParams) -> Result { + pub fn create_with_params(mut params: CreateParams) -> Result { let secp = SecpCtx::new(); + + let mut stage = ChangeSet::::default(); + let network = params.network; + stage.network = Some(network); + let genesis_hash = params .genesis_hash .unwrap_or(genesis_block(network).block_hash()); let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); + stage.local_chain = chain_changeset; + + let mut signers = BTreeMap::>::new(); + let mut index = KeychainTxOutIndex::::new(params.lookahead); + for (keychain, descriptor_to_extract) in params.descriptors { + let (descriptor, mut keymap) = (descriptor_to_extract)(&secp, network)?; + if let Some(params_keymap) = params.keymaps.remove(&keychain) { + keymap.extend(params_keymap); + } + signers.insert( + keychain.clone(), + Arc::new(SignersContainer::build(keymap, &descriptor, &secp)), + ); + stage + .descriptors + .insert(keychain.clone(), descriptor.clone()); + check_wallet_descriptor(&descriptor)?; + insert_descriptor_in_indexer(&mut index, keychain, descriptor)?; + } + stage.indexer = index.initial_changeset(); - let (descriptor, mut descriptor_keymap) = (params.descriptor)(&secp, network)?; - descriptor_keymap.extend(params.descriptor_keymap); - - let (change_descriptor, mut change_descriptor_keymap) = - (params.change_descriptor)(&secp, network)?; - change_descriptor_keymap.extend(params.change_descriptor_keymap); - - let signers = Arc::new(SignersContainer::build( - descriptor_keymap, - &descriptor, - &secp, - )); - let change_signers = Arc::new(SignersContainer::build( - change_descriptor_keymap, - &change_descriptor, - &secp, - )); - let index = create_indexer(descriptor, change_descriptor, params.lookahead)?; - - let descriptor = index.get_descriptor(KeychainKind::External).cloned(); - let change_descriptor = index.get_descriptor(KeychainKind::Internal).cloned(); let indexed_graph = IndexedTxGraph::new(index); - let indexed_graph_changeset = indexed_graph.initial_changeset(); - - let stage = ChangeSet { - descriptor, - change_descriptor, - local_chain: chain_changeset, - tx_graph: indexed_graph_changeset.tx_graph, - indexer: indexed_graph_changeset.indexer, - network: Some(network), - }; Ok(Wallet { signers, - change_signers, network, chain, indexed_graph, @@ -431,17 +432,17 @@ impl Wallet { /// # Ok(()) /// # } /// ``` - pub fn load() -> LoadParams { - LoadParams::new() + pub fn load() -> LoadParams { + LoadParams::::new() } /// Load [`Wallet`] from the given previously persisted [`ChangeSet`] and `params`. /// /// Refer to [`Wallet::load`] for more. pub fn load_with_params( - changeset: ChangeSet, - params: LoadParams, - ) -> Result, LoadError> { + changeset: ChangeSet, + mut params: LoadParams, + ) -> Result, LoadError> { if changeset.is_empty() { return Ok(None); } @@ -450,19 +451,33 @@ impl Wallet { let chain = LocalChain::from_changeset(changeset.local_chain) .map_err(|_| LoadError::MissingGenesis)?; - let mut descriptor_keymap = params.descriptor_keymap; - let descriptor = changeset - .descriptor - .ok_or(LoadError::MissingDescriptor(KeychainKind::External))?; - check_wallet_descriptor(&descriptor).map_err(LoadError::Descriptor)?; + let mut signers = BTreeMap::>::default(); + for (keychain, descriptor_to_extract) in params.check_descriptors { + let changeset_descriptor = changeset + .descriptors + .get(&keychain) + .ok_or(LoadError::MissingDescriptor(keychain.clone()))?; + + let (exp_descriptor, mut keymap) = + (descriptor_to_extract)(&secp, network).map_err(LoadError::Descriptor)?; + if let Some(params_keymap) = params.keymaps.remove(&keychain) { + keymap.extend(params_keymap); + } + if changeset_descriptor.descriptor_id() != exp_descriptor.descriptor_id() { + return Err(LoadError::Mismatch(LoadMismatch::Descriptor { + keychain: keychain.clone(), + loaded: changeset_descriptor.clone(), + expected: exp_descriptor, + })); + } - let mut change_descriptor_keymap = params.change_descriptor_keymap; - let change_descriptor = changeset - .change_descriptor - .ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))?; - check_wallet_descriptor(&change_descriptor).map_err(LoadError::Descriptor)?; + signers.insert( + keychain.clone(), + Arc::new(SignersContainer::build(keymap, changeset_descriptor, &secp)), + ); + } - // checks + // more checks if let Some(exp_network) = params.check_network { if network != exp_network { return Err(LoadError::Mismatch(LoadMismatch::Network { @@ -479,46 +494,39 @@ impl Wallet { })); } } - if let Some(exp_descriptor) = params.check_descriptor { - let (exp_descriptor, keymap) = - (exp_descriptor)(&secp, network).map_err(LoadError::Descriptor)?; - descriptor_keymap.extend(keymap); - - if descriptor.descriptor_id() != exp_descriptor.descriptor_id() { - return Err(LoadError::Mismatch(LoadMismatch::Descriptor { - keychain: KeychainKind::External, - loaded: descriptor, - expected: exp_descriptor, - })); - } + // if let Some(exp_descriptor) = params.check_descriptor { + // let (exp_descriptor, keymap) = + // (exp_descriptor)(&secp, network).map_err(LoadError::Descriptor)?; + // descriptor_keymap.extend(keymap); + // + // if descriptor.descriptor_id() != exp_descriptor.descriptor_id() { + // return Err(LoadError::Mismatch(LoadMismatch::Descriptor { + // keychain: KeychainKind::External, + // loaded: descriptor, + // expected: exp_descriptor, + // })); + // } + // } + // if let Some(exp_change_descriptor) = params.check_change_descriptor { + // let (exp_change_descriptor, keymap) = + // (exp_change_descriptor)(&secp, network).map_err(LoadError::Descriptor)?; + // change_descriptor_keymap.extend(keymap); + // + // if change_descriptor.descriptor_id() != exp_change_descriptor.descriptor_id() { + // return Err(LoadError::Mismatch(LoadMismatch::Descriptor { + // keychain: KeychainKind::Internal, + // loaded: change_descriptor, + // expected: exp_change_descriptor, + // })); + // } + // } + + let mut index = KeychainTxOutIndex::::new(params.lookahead); + for (keychain, changeset_descriptor) in changeset.descriptors { + check_wallet_descriptor(&changeset_descriptor).map_err(LoadError::Descriptor)?; + insert_descriptor_in_indexer(&mut index, keychain, changeset_descriptor) + .map_err(LoadError::::Descriptor); } - if let Some(exp_change_descriptor) = params.check_change_descriptor { - let (exp_change_descriptor, keymap) = - (exp_change_descriptor)(&secp, network).map_err(LoadError::Descriptor)?; - change_descriptor_keymap.extend(keymap); - - if change_descriptor.descriptor_id() != exp_change_descriptor.descriptor_id() { - return Err(LoadError::Mismatch(LoadMismatch::Descriptor { - keychain: KeychainKind::Internal, - loaded: change_descriptor, - expected: exp_change_descriptor, - })); - } - } - - let signers = Arc::new(SignersContainer::build( - descriptor_keymap, - &descriptor, - &secp, - )); - let change_signers = Arc::new(SignersContainer::build( - change_descriptor_keymap, - &change_descriptor, - &secp, - )); - let index = create_indexer(descriptor, change_descriptor, params.lookahead) - .map_err(LoadError::Descriptor)?; - let mut indexed_graph = IndexedTxGraph::new(index); indexed_graph.apply_changeset(changeset.indexer.into()); indexed_graph.apply_changeset(changeset.tx_graph.into()); @@ -527,7 +535,6 @@ impl Wallet { Ok(Some(Wallet { signers, - change_signers, chain, indexed_graph, stage, @@ -542,7 +549,7 @@ impl Wallet { } /// Iterator over all keychains in this wallet - pub fn keychains(&self) -> impl Iterator { + pub fn keychains(&self) -> impl Iterator { self.indexed_graph.index.keychains() } @@ -554,12 +561,11 @@ impl Wallet { /// /// This panics when the caller requests for an address of derivation index greater than the /// [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) max index. - pub fn peek_address(&self, keychain: KeychainKind, mut index: u32) -> AddressInfo { + pub fn peek_address(&self, keychain: K, mut index: u32) -> Option> { let mut spk_iter = self .indexed_graph .index - .unbounded_spk_iter(keychain) - .expect("keychain must exist"); + .unbounded_spk_iter(keychain.clone())?; if !spk_iter.descriptor().has_wildcard() { index = 0; } @@ -567,11 +573,11 @@ impl Wallet { .nth(index as usize) .expect("derivation index is out of bounds"); - AddressInfo { + Some(AddressInfo { index, address: Address::from_script(&spk, self.network).expect("must have address form"), keychain, - } + }) } /// Attempt to reveal the next address of the given `keychain`. @@ -599,22 +605,20 @@ impl Wallet { /// println!("Next address: {}", next_address.address); /// # Ok::<(), anyhow::Error>(()) /// ``` - pub fn reveal_next_address(&mut self, keychain: KeychainKind) -> AddressInfo { + pub fn reveal_next_address(&mut self, keychain: K) -> Option> { let index = &mut self.indexed_graph.index; let stage = &mut self.stage; - let ((index, spk), index_changeset) = index - .reveal_next_spk(keychain) - .expect("keychain must exist"); + let ((index, spk), index_changeset) = index.reveal_next_spk(keychain.clone())?; stage.merge(index_changeset.into()); - AddressInfo { + Some(AddressInfo { index, address: Address::from_script(spk.as_script(), self.network) .expect("must have address form"), keychain, - } + }) } /// Reveal addresses up to and including the target `index` and return an iterator @@ -628,22 +632,21 @@ impl Wallet { /// calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. pub fn reveal_addresses_to( &mut self, - keychain: KeychainKind, + keychain: K, index: u32, - ) -> impl Iterator + '_ { + ) -> Option> + '_> { let (spks, index_changeset) = self .indexed_graph .index - .reveal_to_target(keychain, index) - .expect("keychain must exist"); + .reveal_to_target(keychain.clone(), index)?; self.stage.merge(index_changeset.into()); - spks.into_iter().map(move |(index, spk)| AddressInfo { + Some(spks.into_iter().map(move |(index, spk)| AddressInfo { index, address: Address::from_script(&spk, self.network).expect("must have address form"), - keychain, - }) + keychain: keychain.clone(), + })) } /// Get the next unused address for the given `keychain`, i.e. the address with the lowest @@ -654,28 +657,26 @@ impl Wallet { /// /// **WARNING**: To avoid address reuse you must persist the changes resulting from one or more /// calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. - pub fn next_unused_address(&mut self, keychain: KeychainKind) -> AddressInfo { + pub fn next_unused_address(&mut self, keychain: K) -> Option> { let index = &mut self.indexed_graph.index; - let ((index, spk), index_changeset) = index - .next_unused_spk(keychain) - .expect("keychain must exist"); + let ((index, spk), index_changeset) = index.next_unused_spk(keychain.clone())?; self.stage .merge(indexed_tx_graph::ChangeSet::from(index_changeset).into()); - AddressInfo { + Some(AddressInfo { index, address: Address::from_script(spk.as_script(), self.network) .expect("must have address form"), keychain, - } + }) } /// Marks an address used of the given `keychain` at `index`. /// /// Returns whether the given index was present and then removed from the unused set. - pub fn mark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { + pub fn mark_used(&mut self, keychain: K, index: u32) -> bool { self.indexed_graph.index.mark_used(keychain, index) } @@ -687,7 +688,7 @@ impl Wallet { /// derived spk. /// /// [`mark_used`]: Self::mark_used - pub fn unmark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { + pub fn unmark_used(&mut self, keychain: K, index: u32) -> bool { self.indexed_graph.index.unmark_used(keychain, index) } @@ -698,16 +699,16 @@ impl Wallet { /// [`reveal_addresses_to`](Self::reveal_addresses_to). pub fn list_unused_addresses( &self, - keychain: KeychainKind, - ) -> impl DoubleEndedIterator + '_ { + keychain: K, + ) -> impl DoubleEndedIterator> + '_ { self.indexed_graph .index - .unused_keychain_spks(keychain) + .unused_keychain_spks(keychain.clone()) .map(move |(index, spk)| AddressInfo { index, address: Address::from_script(spk.as_script(), self.network) .expect("must have address form"), - keychain, + keychain: keychain.clone(), }) } @@ -719,12 +720,12 @@ impl Wallet { /// Finds how the wallet derived the script pubkey `spk`. /// /// Will only return `Some(_)` if the wallet has given out the spk. - pub fn derivation_of_spk(&self, spk: ScriptBuf) -> Option<(KeychainKind, u32)> { + pub fn derivation_of_spk(&self, spk: ScriptBuf) -> Option<(K, u32)> { self.indexed_graph.index.index_of_spk(spk).cloned() } /// Return the list of unspent outputs of this wallet - pub fn list_unspent(&self) -> impl Iterator + '_ { + pub fn list_unspent(&self) -> impl Iterator> + '_ { self.indexed_graph .graph() .filter_chain_unspents( @@ -738,7 +739,7 @@ impl Wallet { /// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed). /// /// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead. - pub fn list_output(&self) -> impl Iterator + '_ { + pub fn list_output(&self) -> impl Iterator> + '_ { self.indexed_graph .graph() .filter_chain_txouts( @@ -769,7 +770,7 @@ impl Wallet { /// script pubkeys the wallet is storing internally). pub fn all_unbounded_spk_iters( &self, - ) -> BTreeMap> + Clone> { + ) -> BTreeMap> + Clone> { self.indexed_graph.index.all_unbounded_spk_iters() } @@ -780,17 +781,14 @@ impl Wallet { /// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters pub fn unbounded_spk_iter( &self, - keychain: KeychainKind, - ) -> impl Iterator> + Clone { - self.indexed_graph - .index - .unbounded_spk_iter(keychain) - .expect("keychain must exist") + keychain: K, + ) -> Option> + Clone> { + self.indexed_graph.index.unbounded_spk_iter(keychain) } /// 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 { + pub fn get_utxo(&self, op: OutPoint) -> Option> { let ((keychain, index), _) = self.indexed_graph.index.txout(op)?; self.indexed_graph .graph() @@ -799,7 +797,7 @@ impl Wallet { self.chain.tip().block_id(), core::iter::once(((), op)), ) - .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo)) + .map(|(_, full_txo)| new_local_utxo(keychain.clone(), index, full_txo)) .next() } @@ -1026,12 +1024,15 @@ impl Wallet { /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature /// values. - pub fn balance(&self) -> Balance { + pub fn balance(&self) -> Balance + where + K: WalletKeychain, + { self.indexed_graph.graph().balance( &self.chain, self.chain.tip().block_id(), self.indexed_graph.index.outpoints().iter().cloned(), - |&(k, _), _| k == KeychainKind::Internal, + |(k, _), _| k.is_trusted_variant(), ) } @@ -1040,34 +1041,33 @@ impl Wallet { /// See [the `signer` module](signer) for an example. pub fn add_signer( &mut self, - keychain: KeychainKind, + keychain: K, ordering: SignerOrdering, signer: Arc, ) { - let signers = match keychain { - KeychainKind::External => Arc::make_mut(&mut self.signers), - KeychainKind::Internal => Arc::make_mut(&mut self.change_signers), - }; - - signers.add_external(signer.id(&self.secp), ordering, signer); + let signers = self.signers.entry(keychain).or_default(); + Arc::make_mut(signers).add_external(signer.id(&self.secp), ordering, signer); } /// Set the keymap for a given keychain. - pub fn set_keymap(&mut self, keychain: KeychainKind, keymap: KeyMap) { - let wallet_signers = match keychain { - KeychainKind::External => Arc::make_mut(&mut self.signers), - KeychainKind::Internal => Arc::make_mut(&mut self.change_signers), - }; - let descriptor = self - .indexed_graph - .index - .get_descriptor(keychain) - .expect("keychain must exist"); - *wallet_signers = SignersContainer::build(keymap, descriptor, &self.secp); + /// + /// Returns `false` if the `keychain` has no associated descriptor. + pub fn set_keymap(&mut self, keychain: K, keymap: KeyMap) -> bool { + let descriptor = self.indexed_graph.index.get_descriptor(keychain.clone()); + match descriptor { + Some(descriptor) => { + self.signers.insert( + keychain, + Arc::new(SignersContainer::build(keymap, descriptor, &self.secp)), + ); + true + } + None => false, + } } - /// Set the keymap for each keychain. - pub fn set_keymaps(&mut self, keymaps: impl IntoIterator) { + /// Set the keymap for each keychain that contains an associated descriptor. + pub fn set_keymaps(&mut self, keymaps: impl IntoIterator) { for (keychain, keymap) in keymaps { self.set_keymap(keychain, keymap); } @@ -1092,11 +1092,8 @@ impl Wallet { /// /// Ok::<(), Box>(()) /// ``` - pub fn get_signers(&self, keychain: KeychainKind) -> Arc { - match keychain { - KeychainKind::External => Arc::clone(&self.signers), - KeychainKind::Internal => Arc::clone(&self.change_signers), - } + pub fn get_signers(&self, keychain: K) -> Option> { + self.signers.get(&keychain).cloned() } /// Start building a transaction. @@ -1127,7 +1124,10 @@ impl Wallet { /// ``` /// /// [`TxBuilder`]: crate::TxBuilder - pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm> { + pub fn build_tx(&mut self) -> TxBuilder<'_, K, DefaultCoinSelectionAlgorithm> + where + K: WalletKeychain, + { TxBuilder { wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), params: TxParams::default(), @@ -1138,54 +1138,30 @@ impl Wallet { pub(crate) fn create_tx( &mut self, coin_selection: Cs, - params: TxParams, + params: TxParams, rng: &mut impl RngCore, - ) -> Result { + ) -> Result> { let keychains: BTreeMap<_, _> = self.indexed_graph.index.keychains().collect(); - let external_descriptor = keychains.get(&KeychainKind::External).expect("must exist"); - let internal_descriptor = keychains.get(&KeychainKind::Internal).expect("must exist"); - let external_policy = external_descriptor - .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)? - .unwrap(); - let internal_policy = internal_descriptor - .extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? - .unwrap(); - - // The policy allows spending external outputs, but it requires a policy path that hasn't been - // provided - if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange - && external_policy.requires_path() - && params.external_policy_path.is_none() - { - return Err(CreateTxError::SpendingPolicyRequired( - KeychainKind::External, - )); - }; - // Same for the internal_policy path - if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden - && internal_policy.requires_path() - && params.internal_policy_path.is_none() - { - return Err(CreateTxError::SpendingPolicyRequired( - KeychainKind::Internal, - )); - }; - - let external_requirements = external_policy.get_condition( - params - .external_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?; - let internal_requirements = internal_policy.get_condition( - params - .internal_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?; - - let requirements = external_requirements.merge(&internal_requirements)?; + let mut requirements = Condition::default(); + for (keychain, descriptor) in &keychains { + if !params.spend_keychain_policy.can_spend(keychain) { + continue; + } + let signers = self.signers.get(keychain).cloned().unwrap_or_default(); + let policy = descriptor + .extract_policy(&signers, BuildSatisfaction::None, &self.secp)? + .expect("must get policy"); + match params.policy_paths.get(keychain) { + Some(policy_path) => { + requirements = requirements.merge(&policy.get_condition(policy_path)?)? + } + None if policy.requires_path() => { + return Err(CreateTxError::SpendingPolicyRequired(keychain.clone())) + } + _ => {} + } + } let version = match params.version { Some(tx_builder::Version(0)) => return Err(CreateTxError::Version0), @@ -1348,18 +1324,17 @@ impl Wallet { self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32())); // get drain script - let drain_script = match params.drain_to { - Some(ref drain_recipient) => drain_recipient.clone(), - None => { - let change_keychain = KeychainKind::Internal; + let drain_script = match ¶ms.drain_to { + DrainTo::Script(drain_spk) => drain_spk.clone(), + DrainTo::Keychain(drain_keychain) => { let ((index, spk), index_changeset) = self .indexed_graph .index - .next_unused_spk(change_keychain) - .expect("keychain must exist"); - self.indexed_graph.index.mark_used(change_keychain, index); + .next_unused_spk(drain_keychain.clone()) + .ok_or(CreateTxError::MissingDescriptor(drain_keychain.clone()))?; + self.indexed_graph.index.mark_used(drain_keychain.clone(), index); self.stage.merge(index_changeset.into()); - spk + spk.clone() } }; @@ -1408,12 +1383,11 @@ impl Wallet { if tx.output.is_empty() { // Uh oh, our transaction has no outputs. // We allow this when: - // - We have a drain_to address and the utxos we must spend (this happens, - // for example, when we RBF) - // - We have a drain_to address and drain_wallet set + // - We have UTXOs that we must spend (this happens, for example, when we RBF) + // - We `drain_wallet` set // Otherwise, we don't know who we should send the funds to, and how much // we should send! - if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) { + if params.drain_wallet || !params.utxos.is_empty() { if let NoChange { dust_threshold, remaining_amount, @@ -1505,7 +1479,11 @@ impl Wallet { pub fn build_fee_bump( &mut self, txid: Txid, - ) -> Result, BuildFeeBumpError> { + change_keychain: K, + ) -> Result, BuildFeeBumpError> + where + K: WalletKeychain, + { let graph = self.indexed_graph.graph(); let txout_index = &self.indexed_graph.index; let chain_tip = self.chain.tip().block_id(); @@ -1557,18 +1535,19 @@ impl Wallet { .into(); let weighted_utxo = match txout_index.index_of_spk(txout.script_pubkey.clone()) { - Some(&(keychain, derivation_index)) => { + Some((keychain, derivation_index)) => { let satisfaction_weight = self - .public_descriptor(keychain) + .public_descriptor(keychain.clone()) + .expect("indexed spk's keychain must have associated descriptor") .max_weight_to_satisfy() .unwrap(); WeightedUtxo { utxo: Utxo::Local(LocalOutput { outpoint: txin.previous_output, txout: txout.clone(), - keychain, + keychain: keychain.clone(), is_spent: true, - derivation_index, + derivation_index: *derivation_index, confirmation_time, }), satisfaction_weight, @@ -1597,24 +1576,14 @@ impl Wallet { }) .collect::, _>>()?; - if tx.output.len() > 1 { - let mut change_index = None; - for (index, txout) in tx.output.iter().enumerate() { - let change_keychain = KeychainKind::Internal; - match txout_index.index_of_spk(txout.script_pubkey.clone()) { - Some((keychain, _)) if *keychain == change_keychain => { - change_index = Some(index) - } - _ => {} - } - } - - if let Some(change_index) = change_index { - tx.output.remove(change_index); - } - } + tx.output.retain( + |txo| match txout_index.index_of_spk(txo.script_pubkey.clone()) { + Some((k, _)) => k != &change_keychain, + None => true, + }, + ); - let params = TxParams { + let params = TxParams:: { // TODO: figure out what rbf option should be? version: Some(tx_builder::Version(tx.version.0)), recipients: tx @@ -1696,12 +1665,7 @@ impl Wallet { return Err(SignerError::NonStandardSighash); } - for signer in self - .signers - .signers() - .iter() - .chain(self.change_signers.signers().iter()) - { + for signer in self.signers.values().flat_map(|s| s.signers()) { signer.sign_transaction(psbt, &sign_options, &self.secp)?; } @@ -1714,17 +1678,13 @@ impl Wallet { } /// Return the spending policies for the wallet's descriptor - pub fn policies(&self, keychain: KeychainKind) -> Result, DescriptorError> { - let signers = match keychain { - KeychainKind::External => &self.signers, - KeychainKind::Internal => &self.change_signers, + pub fn policies(&self, keychain: K) -> Result, DescriptorError> { + let signers = self.signers.get(&keychain).cloned().unwrap_or_default(); + let descriptor = match self.public_descriptor(keychain) { + Some(descriptor) => descriptor, + None => return Ok(None), }; - - self.public_descriptor(keychain).extract_policy( - signers, - BuildSatisfaction::None, - &self.secp, - ) + descriptor.extract_policy(&signers, BuildSatisfaction::None, &self.secp) } /// Returns the descriptor used to create addresses for a particular `keychain`. @@ -1732,11 +1692,8 @@ impl Wallet { /// the same structure but with the all secret keys replaced by their corresponding public key. /// /// This can be used to build a watch-only version of a wallet. - pub fn public_descriptor(&self, keychain: KeychainKind) -> &ExtendedDescriptor { - self.indexed_graph - .index - .get_descriptor(keychain) - .expect("keychain must exist") + pub fn public_descriptor(&self, keychain: K) -> Option<&ExtendedDescriptor> { + self.indexed_graph.index.get_descriptor(keychain) } /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass @@ -1843,17 +1800,16 @@ impl Wallet { /// 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 { + pub fn derivation_index(&self, keychain: K) -> Option { self.indexed_graph.index.last_revealed_index(keychain) } /// The index of the next address that you would get if you were to ask the wallet for a new address - pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 { + pub fn next_derivation_index(&self, keychain: K) -> Option { self.indexed_graph .index .next_index(keychain) - .expect("keychain must exist") - .0 + .map(|(i, _)| i) } /// Informs the wallet that you no longer intend to broadcast a tx that was built from it. @@ -1866,29 +1822,29 @@ impl Wallet { if let Some((keychain, index)) = txout_index.index_of_spk(txout.script_pubkey.clone()) { // NOTE: unmark_used will **not** make something unused if it has actually been used // by a tx in the tracker. It only removes the superficial marking. - txout_index.unmark_used(*keychain, *index); + txout_index.unmark_used(keychain.clone(), *index); } } } fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { - let &(keychain, child) = self + let (keychain, child) = self .indexed_graph .index .index_of_spk(txout.script_pubkey.clone())?; - let descriptor = self.public_descriptor(keychain); - descriptor.at_derivation_index(child).ok() + let descriptor = self.public_descriptor(keychain.clone())?; + descriptor.at_derivation_index(*child).ok() } - fn get_available_utxos(&self) -> Vec<(LocalOutput, Weight)> { + fn get_available_utxos(&self) -> Vec<(LocalOutput, Weight)> { self.list_unspent() - .map(|utxo| { - let keychain = utxo.keychain; - (utxo, { - self.public_descriptor(keychain) + .filter_map(|utxo| { + let keychain = utxo.keychain.clone(); + Some((utxo, { + self.public_descriptor(keychain)? .max_weight_to_satisfy() .unwrap() - }) + })) }) .collect() } @@ -1897,11 +1853,11 @@ impl Wallet { /// transaction and any further that may be used if needed. fn preselect_utxos( &self, - params: &TxParams, + params: &TxParams, current_height: Option, - ) -> (Vec, Vec) { + ) -> (Vec>, Vec>) { let TxParams { - change_policy, + spend_keychain_policy: change_policy, unspendable, utxos, drain_wallet, @@ -2002,9 +1958,9 @@ impl Wallet { fn complete_transaction( &self, tx: Transaction, - selected: Vec, - params: TxParams, - ) -> Result { + selected: Vec>, + params: TxParams, + ) -> Result> { let mut psbt = Psbt::from_unsigned_tx(tx)?; if params.add_global_xpubs { @@ -2081,13 +2037,13 @@ impl Wallet { /// get the corresponding PSBT Input for a LocalUtxo pub fn get_psbt_input( &self, - utxo: LocalOutput, + utxo: LocalOutput, sighash_type: Option, only_witness_utxo: bool, - ) -> Result { + ) -> Result> { // Try to find the prev_script in our db to figure out if this is internal or external, // and the derivation index - let &(keychain, child) = self + let (keychain, child) = self .indexed_graph .index .index_of_spk(utxo.txout.script_pubkey) @@ -2098,9 +2054,11 @@ impl Wallet { ..psbt::Input::default() }; - let desc = self.public_descriptor(keychain); + let desc = self + .public_descriptor(keychain.clone()) + .ok_or(CreateTxError::MissingDescriptor(keychain.clone()))?; let derived_descriptor = desc - .at_derivation_index(child) + .at_derivation_index(*child) .expect("child can't be hardened"); psbt_input @@ -2135,13 +2093,15 @@ impl Wallet { // Try to figure out the keychain and derivation for every input and output for (is_input, index, out) in utxos.into_iter() { - if let Some(&(keychain, child)) = + if let Some((keychain, child)) = self.indexed_graph.index.index_of_spk(out.script_pubkey) { - let desc = self.public_descriptor(keychain); - let desc = desc - .at_derivation_index(child) - .expect("child can't be hardened"); + let desc = match self.public_descriptor(keychain.clone()) { + Some(desc) => desc + .at_derivation_index(*child) + .expect("child can't be hardened"), + None => continue, + }; if is_input { psbt.update_input_with_descriptor(index, &desc) @@ -2159,13 +2119,15 @@ impl Wallet { /// Return the checksum of the public descriptor associated to `keychain` /// /// Internally calls [`Self::public_descriptor`] to fetch the right descriptor - pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String { - self.public_descriptor(keychain) - .to_string() - .split_once('#') - .unwrap() - .1 - .to_string() + pub fn descriptor_checksum(&self, keychain: K) -> Option { + Some( + self.public_descriptor(keychain)? + .to_string() + .split_once('#') + .unwrap() + .1 + .to_string(), + ) } /// Applies an update to the wallet and stages the changes (but does not persist them). @@ -2177,7 +2139,7 @@ impl Wallet { /// to persist staged wallet changes see [`Wallet::reveal_next_address`]. ` /// /// [`commit`]: Self::commit - pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { + pub fn apply_update(&mut self, update: impl Into>) -> Result<(), CannotConnectError> { let update = update.into(); let mut changeset = match update.chain { Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?), @@ -2195,7 +2157,7 @@ impl Wallet { } /// Get a reference of the staged [`ChangeSet`] that are yet to be committed (if any). - pub fn staged(&self) -> Option<&ChangeSet> { + pub fn staged(&self) -> Option<&ChangeSet> { if self.stage.is_empty() { None } else { @@ -2204,7 +2166,7 @@ impl Wallet { } /// Take the staged [`ChangeSet`] to be persisted now (if any). - pub fn take_staged(&mut self) -> Option { + pub fn take_staged(&mut self) -> Option> { self.stage.take() } @@ -2222,7 +2184,7 @@ impl Wallet { } /// Get a reference to the inner [`KeychainTxOutIndex`]. - pub fn spk_index(&self) -> &KeychainTxOutIndex { + pub fn spk_index(&self) -> &KeychainTxOutIndex { &self.indexed_graph.index } @@ -2313,7 +2275,7 @@ impl Wallet { } /// Methods to construct sync/full-scan requests for spk-based chain sources. -impl Wallet { +impl Wallet { /// 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 @@ -2332,12 +2294,12 @@ impl Wallet { /// /// This operation is generally only used when importing or restoring a previously used wallet /// in which the list of used scripts is not known. - pub fn start_full_scan(&self) -> FullScanRequest { + pub fn start_full_scan(&self) -> FullScanRequest { FullScanRequest::from_keychain_txout_index(self.chain.tip(), &self.indexed_graph.index) } } -impl AsRef> for Wallet { +impl AsRef> for Wallet { fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { self.indexed_graph.graph() } @@ -2372,11 +2334,11 @@ where Ok(wallet_name) } -fn new_local_utxo( - keychain: KeychainKind, +fn new_local_utxo( + keychain: K, derivation_index: u32, full_txo: FullTxOut, -) -> LocalOutput { +) -> LocalOutput { LocalOutput { outpoint: full_txo.outpoint, txout: full_txo.txout, @@ -2387,6 +2349,28 @@ fn new_local_utxo( } } +fn insert_descriptor_in_indexer( + indexer: &mut KeychainTxOutIndex, + keychain: K, + descriptor: Descriptor, +) -> Result<(), DescriptorError> { + let inserted = indexer + .insert_descriptor(keychain, descriptor) + .map_err(|e| { + use bdk_chain::indexer::keychain_txout::InsertDescriptorError; + match e { + InsertDescriptorError::DescriptorAlreadyAssigned { .. } => { + crate::descriptor::error::Error::ExternalAndInternalAreTheSame + } + InsertDescriptorError::KeychainAlreadyAssigned { .. } => { + unreachable!("this is the first time we're assigning internal") + } + } + })?; + assert!(inserted, "cannot have recurring keychain"); + Ok(()) +} + fn create_indexer( descriptor: ExtendedDescriptor, change_descriptor: ExtendedDescriptor, diff --git a/crates/wallet/src/wallet/params.rs b/crates/wallet/src/wallet/params.rs index 44d0d1db1..00601aa0f 100644 --- a/crates/wallet/src/wallet/params.rs +++ b/crates/wallet/src/wallet/params.rs @@ -1,4 +1,4 @@ -use alloc::boxed::Box; +use alloc::{boxed::Box, collections::BTreeMap}; use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith}; use bitcoin::{BlockHash, Network}; use miniscript::descriptor::KeyMap; @@ -6,7 +6,7 @@ use miniscript::descriptor::KeyMap; use crate::{ descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor}, utils::SecpCtx, - KeychainKind, Wallet, + Wallet, }; use super::{ChangeSet, LoadError, PersistedWallet}; @@ -29,42 +29,48 @@ where /// Parameters for [`Wallet::create`] or [`PersistedWallet::create`]. #[must_use] -pub struct CreateParams { - pub(crate) descriptor: DescriptorToExtract, - pub(crate) descriptor_keymap: KeyMap, - pub(crate) change_descriptor: DescriptorToExtract, - pub(crate) change_descriptor_keymap: KeyMap, +pub struct CreateParams { + pub(crate) descriptors: BTreeMap, + pub(crate) keymaps: BTreeMap, pub(crate) network: Network, pub(crate) genesis_hash: Option, pub(crate) lookahead: u32, } -impl CreateParams { - /// Construct parameters with provided `descriptor`, `change_descriptor` and `network`. +impl CreateParams { + /// Construct parameters with provided `descriptor`, `change_descriptor`. /// /// Default values: `genesis_hash` = `None`, `lookahead` = [`DEFAULT_LOOKAHEAD`] - pub fn new(descriptor: D, change_descriptor: D) -> Self { + pub fn new() -> Self { Self { - descriptor: make_descriptor_to_extract(descriptor), - descriptor_keymap: KeyMap::default(), - change_descriptor: make_descriptor_to_extract(change_descriptor), - change_descriptor_keymap: KeyMap::default(), + descriptors: BTreeMap::new(), + keymaps: BTreeMap::new(), network: Network::Bitcoin, genesis_hash: None, lookahead: DEFAULT_LOOKAHEAD, } } - /// Extend the given `keychain`'s `keymap`. - pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self { - match keychain { - KeychainKind::External => &mut self.descriptor_keymap, - KeychainKind::Internal => &mut self.change_descriptor_keymap, - } - .extend(keymap); + pub fn descriptor( + mut self, + keychain: K, + descriptor: D, + ) -> Self { + self.descriptors + .insert(keychain, make_descriptor_to_extract(descriptor)); self } + /// Extend the `keymap` of the given `keychain`. + pub fn keymap(mut self, keychain: K, keymap: KeyMap) -> Self { + self.keymaps.entry(keychain).or_default().extend(keymap); + self + } + + /// Create [`Wallet`] without persistence. + pub fn create_wallet_no_persist(self) -> Result, DescriptorError> { + Wallet::::create_with_params(self) + } /// Set `network`. pub fn network(mut self, network: Network) -> Self { self.network = network; @@ -87,76 +93,63 @@ impl CreateParams { pub fn create_wallet( self, db: &mut Db, - ) -> Result>::CreateError> + ) -> Result, as PersistWith>::CreateError> where - Wallet: PersistWith, + Wallet: PersistWith, { - PersistedWallet::create(db, self) + PersistedWallet::::create(db, self) } /// Create [`PersistedWallet`] with the given async `Db`. pub async fn create_wallet_async( self, db: &mut Db, - ) -> Result>::CreateError> + ) -> Result, as PersistAsyncWith>::CreateError> where - Wallet: PersistAsyncWith, + Wallet: PersistAsyncWith, { - PersistedWallet::create_async(db, self).await - } - - /// Create [`Wallet`] without persistence. - pub fn create_wallet_no_persist(self) -> Result { - Wallet::create_with_params(self) + PersistedWallet::::create_async(db, self).await } } /// Parameters for [`Wallet::load`] or [`PersistedWallet::load`]. #[must_use] -pub struct LoadParams { - pub(crate) descriptor_keymap: KeyMap, - pub(crate) change_descriptor_keymap: KeyMap, +pub struct LoadParams { + pub(crate) keymaps: BTreeMap, pub(crate) lookahead: u32, pub(crate) check_network: Option, pub(crate) check_genesis_hash: Option, - pub(crate) check_descriptor: Option, - pub(crate) check_change_descriptor: Option, + pub(crate) check_descriptors: BTreeMap, } -impl LoadParams { +impl LoadParams { /// Construct parameters with default values. /// /// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`] pub fn new() -> Self { Self { - descriptor_keymap: KeyMap::default(), - change_descriptor_keymap: KeyMap::default(), + keymaps: BTreeMap::default(), lookahead: DEFAULT_LOOKAHEAD, check_network: None, check_genesis_hash: None, - check_descriptor: None, - check_change_descriptor: None, + check_descriptors: BTreeMap::default(), } } /// Extend the given `keychain`'s `keymap`. - pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self { - match keychain { - KeychainKind::External => &mut self.descriptor_keymap, - KeychainKind::Internal => &mut self.change_descriptor_keymap, - } - .extend(keymap); + pub fn keymap(mut self, keychain: K, keymap: KeyMap) -> Self { + self.keymaps.entry(keychain).or_default().extend(keymap); self } /// Checks that `descriptor` of `keychain` matches this, and extracts private keys (if /// available). - pub fn descriptors(mut self, descriptor: D, change_descriptor: D) -> Self + pub fn descriptor(mut self, keychain: K, descriptor: D) -> Self where D: IntoWalletDescriptor + 'static, { - self.check_descriptor = Some(make_descriptor_to_extract(descriptor)); - self.check_change_descriptor = Some(make_descriptor_to_extract(change_descriptor)); + self.check_descriptors + .insert(keychain, make_descriptor_to_extract(descriptor)); self } @@ -182,31 +175,34 @@ impl LoadParams { pub fn load_wallet( self, db: &mut Db, - ) -> Result, >::LoadError> + ) -> Result>, as PersistWith>::LoadError> where - Wallet: PersistWith, + Wallet: PersistWith, { - PersistedWallet::load(db, self) + PersistedWallet::::load(db, self) } /// Load [`PersistedWallet`] with the given async `Db`. pub async fn load_wallet_async( self, db: &mut Db, - ) -> Result, >::LoadError> + ) -> Result>, as PersistAsyncWith>::LoadError> where - Wallet: PersistAsyncWith, + Wallet: PersistAsyncWith, { - PersistedWallet::load_async(db, self).await + PersistedWallet::::load_async(db, self).await } /// Load [`Wallet`] without persistence. - pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result, LoadError> { - Wallet::load_with_params(changeset, self) + pub fn load_wallet_no_persist( + self, + changeset: ChangeSet, + ) -> Result>, LoadError> { + Wallet::::load_with_params(changeset, self) } } -impl Default for LoadParams { +impl Default for LoadParams { fn default() -> Self { Self::new() } diff --git a/crates/wallet/src/wallet/persisted.rs b/crates/wallet/src/wallet/persisted.rs index cc9f267f4..9f141f439 100644 --- a/crates/wallet/src/wallet/persisted.rs +++ b/crates/wallet/src/wallet/persisted.rs @@ -3,15 +3,24 @@ use core::fmt; use crate::{descriptor::DescriptorError, Wallet}; /// Represents a persisted wallet. -pub type PersistedWallet = bdk_chain::Persisted; +pub type PersistedWallet = bdk_chain::Persisted>; #[cfg(feature = "rusqlite")] -impl<'c> chain::PersistWith> for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; +impl<'c, K> chain::PersistWith> for Wallet +where + K: core::fmt::Debug + + Clone + + Ord + + Send + + Sync + + serde::Serialize + + serde::de::DeserializeOwned, +{ + type CreateParams = crate::CreateParams; + type LoadParams = crate::LoadParams; type CreateError = CreateWithPersistError; - type LoadError = LoadWithPersistError; + type LoadError = LoadWithPersistError; type PersistError = bdk_chain::rusqlite::Error; fn create( @@ -49,12 +58,21 @@ impl<'c> chain::PersistWith> for Wallet { } #[cfg(feature = "rusqlite")] -impl chain::PersistWith for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; +impl chain::PersistWith for Wallet +where + K: core::fmt::Debug + + Clone + + Ord + + Send + + Sync + + serde::Serialize + + serde::de::DeserializeOwned, +{ + type CreateParams = crate::CreateParams; + type LoadParams = crate::LoadParams; type CreateError = CreateWithPersistError; - type LoadError = LoadWithPersistError; + type LoadError = LoadWithPersistError; type PersistError = bdk_chain::rusqlite::Error; fn create( @@ -88,16 +106,25 @@ impl chain::PersistWith for Wallet { } #[cfg(feature = "file_store")] -impl chain::PersistWith> for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; +impl chain::PersistWith>> for Wallet +where + K: core::fmt::Debug + + Clone + + Ord + + Send + + Sync + + serde::Serialize + + serde::de::DeserializeOwned, +{ + type CreateParams = crate::CreateParams; + type LoadParams = crate::LoadParams; type CreateError = CreateWithPersistError; type LoadError = - LoadWithPersistError>; + LoadWithPersistError>>; type PersistError = std::io::Error; fn create( - db: &mut bdk_file_store::Store, + db: &mut bdk_file_store::Store>, params: Self::CreateParams, ) -> Result { let mut wallet = @@ -110,7 +137,7 @@ impl chain::PersistWith> for Wallet { } fn load( - db: &mut bdk_file_store::Store, + db: &mut bdk_file_store::Store>, params: Self::LoadParams, ) -> Result, Self::LoadError> { let changeset = db @@ -121,7 +148,7 @@ impl chain::PersistWith> for Wallet { } fn persist( - db: &mut bdk_file_store::Store, + db: &mut bdk_file_store::Store>, changeset: &::ChangeSet, ) -> Result<(), Self::PersistError> { db.append_changeset(changeset) @@ -130,14 +157,14 @@ impl chain::PersistWith> for Wallet { /// Error type for [`PersistedWallet::load`]. #[derive(Debug, PartialEq)] -pub enum LoadWithPersistError { +pub enum LoadWithPersistError { /// Error from persistence. Persist(E), /// Occurs when the loaded changeset cannot construct [`Wallet`]. - InvalidChangeSet(crate::LoadError), + InvalidChangeSet(crate::LoadError), } -impl fmt::Display for LoadWithPersistError { +impl fmt::Display for LoadWithPersistError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Persist(err) => fmt::Display::fmt(err, f), @@ -147,7 +174,7 @@ impl fmt::Display for LoadWithPersistError { } #[cfg(feature = "std")] -impl std::error::Error for LoadWithPersistError {} +impl std::error::Error for LoadWithPersistError {} /// Error type for [`PersistedWallet::create`]. #[derive(Debug)] diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs index 962edd56f..cf1d7eca1 100644 --- a/crates/wallet/src/wallet/tx_builder.rs +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -38,9 +38,10 @@ //! # Ok::<(), anyhow::Error>(()) //! ``` +use alloc::collections::BTreeSet; use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; use core::cell::RefCell; -use core::fmt; +use core::fmt::{self, Debug}; use alloc::sync::Arc; @@ -56,7 +57,7 @@ use super::coin_selection::CoinSelectionAlgorithm; use super::utils::shuffle_slice; use super::{CreateTxError, Wallet}; use crate::collections::{BTreeMap, HashSet}; -use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; +use crate::{LocalOutput, Utxo, WalletKeychain, WeightedUtxo}; /// A transaction builder /// @@ -112,23 +113,22 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; /// [`finish`]: Self::finish /// [`coin_selection`]: Self::coin_selection #[derive(Debug)] -pub struct TxBuilder<'a, Cs> { - pub(crate) wallet: Rc>, - pub(crate) params: TxParams, +pub struct TxBuilder<'a, K, Cs> { + pub(crate) wallet: Rc>>, + pub(crate) params: TxParams, pub(crate) coin_selection: Cs, } /// The parameters for transaction creation sans coin selection algorithm. //TODO: TxParams should eventually be exposed publicly. -#[derive(Default, Debug, Clone)] -pub(crate) struct TxParams { +#[derive(Debug, Clone)] +pub(crate) struct TxParams { pub(crate) recipients: Vec<(ScriptBuf, u64)>, pub(crate) drain_wallet: bool, - pub(crate) drain_to: Option, + pub(crate) drain_to: DrainTo, pub(crate) fee_policy: Option, - pub(crate) internal_policy_path: Option>>, - pub(crate) external_policy_path: Option>>, - pub(crate) utxos: Vec, + pub(crate) policy_paths: BTreeMap>>, + pub(crate) utxos: Vec>, pub(crate) unspendable: HashSet, pub(crate) manually_selected_only: bool, pub(crate) sighash: Option, @@ -136,7 +136,7 @@ pub(crate) struct TxParams { pub(crate) locktime: Option, pub(crate) rbf: Option, pub(crate) version: Option, - pub(crate) change_policy: ChangeSpendPolicy, + pub(crate) spend_keychain_policy: SpendKeychainPolicy, pub(crate) only_witness_utxo: bool, pub(crate) add_global_xpubs: bool, pub(crate) include_output_redeem_witness_script: bool, @@ -157,13 +157,52 @@ pub(crate) enum FeePolicy { FeeAmount(u64), } +#[derive(Debug, Clone)] +pub enum DrainTo { + Keychain(K), + Script(ScriptBuf), +} + +impl Default for TxParams { + fn default() -> Self { + Self { + recipients: Default::default(), + drain_wallet: Default::default(), + drain_to: Default::default(), + fee_policy: Default::default(), + policy_paths: Default::default(), + utxos: Default::default(), + unspendable: Default::default(), + manually_selected_only: Default::default(), + sighash: Default::default(), + ordering: Default::default(), + locktime: Default::default(), + rbf: Default::default(), + version: Default::default(), + spend_keychain_policy: Default::default(), + only_witness_utxo: Default::default(), + add_global_xpubs: Default::default(), + include_output_redeem_witness_script: Default::default(), + bumping_fee: Default::default(), + current_height: Default::default(), + allow_dust: Default::default(), + } + } +} + +impl Default for DrainTo { + fn default() -> Self { + Self::Keychain(K::DEFAULT_CHANGE_VARIANT) + } +} + impl Default for FeePolicy { fn default() -> Self { FeePolicy::FeeRate(FeeRate::BROADCAST_MIN) } } -impl<'a, Cs: Clone> Clone for TxBuilder<'a, Cs> { +impl<'a, K: Clone, Cs: Clone> Clone for TxBuilder<'a, K, Cs> { fn clone(&self) -> Self { TxBuilder { wallet: self.wallet.clone(), @@ -174,7 +213,7 @@ impl<'a, Cs: Clone> Clone for TxBuilder<'a, Cs> { } // Methods supported for any CoinSelectionAlgorithm. -impl<'a, Cs> TxBuilder<'a, Cs> { +impl<'a, K: core::fmt::Debug + Clone + Ord, Cs> TxBuilder<'a, K, Cs> { /// Set a custom fee rate. /// /// This method sets the mining fee paid by the transaction as a rate on its size. @@ -269,14 +308,9 @@ impl<'a, Cs> TxBuilder<'a, Cs> { pub fn policy_path( &mut self, policy_path: BTreeMap>, - keychain: KeychainKind, + keychain: K, ) -> &mut Self { - let to_update = match keychain { - KeychainKind::Internal => &mut self.params.internal_policy_path, - KeychainKind::External => &mut self.params.external_policy_path, - }; - - *to_update = Some(policy_path); + self.params.policy_paths.insert(keychain, policy_path); self } @@ -299,7 +333,10 @@ impl<'a, Cs> TxBuilder<'a, Cs> { .collect::, _>>()?; for utxo in utxos { - let descriptor = wallet.public_descriptor(utxo.keychain); + let descriptor = match wallet.public_descriptor(utxo.keychain.clone()) { + Some(d) => d, + None => continue, + }; let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap(); self.params.utxos.push(WeightedUtxo { satisfaction_weight, @@ -478,28 +515,30 @@ impl<'a, Cs> TxBuilder<'a, Cs> { self } - /// Do not spend change outputs + /// Do not spend outputs of the given `keychains` /// /// This effectively adds all the change outputs to the "unspendable" list. See /// [`TxBuilder::unspendable`]. - pub fn do_not_spend_change(&mut self) -> &mut Self { - self.params.change_policy = ChangeSpendPolicy::ChangeForbidden; + pub fn do_not_spend_keychains(&mut self, keychains: impl IntoIterator) -> &mut Self { + self.params.spend_keychain_policy = + SpendKeychainPolicy::Disallow(keychains.into_iter().collect()); self } - /// Only spend change outputs + /// Only spend outputs of the given `keychains` /// /// This effectively adds all the non-change outputs to the "unspendable" list. See /// [`TxBuilder::unspendable`]. - pub fn only_spend_change(&mut self) -> &mut Self { - self.params.change_policy = ChangeSpendPolicy::OnlyChange; + pub fn only_spend_keychains(&mut self, keychains: impl IntoIterator) -> &mut Self { + self.params.spend_keychain_policy = + SpendKeychainPolicy::AllowOnly(keychains.into_iter().collect()); self } /// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and /// [`TxBuilder::only_spend_change`] for some shortcuts. - pub fn change_policy(&mut self, change_policy: ChangeSpendPolicy) -> &mut Self { - self.params.change_policy = change_policy; + pub fn spend_keychain_policy(&mut self, policy: SpendKeychainPolicy) -> &mut Self { + self.params.spend_keychain_policy = policy; self } @@ -543,7 +582,10 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// Overrides the [`CoinSelectionAlgorithm`]. /// /// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder. - pub fn coin_selection(self, coin_selection: P) -> TxBuilder<'a, P> { + pub fn coin_selection( + self, + coin_selection: P, + ) -> TxBuilder<'a, K, P> { TxBuilder { wallet: self.wallet, params: self.params, @@ -666,12 +708,17 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// [`add_utxos`]: Self::add_utxos /// [`drain_wallet`]: Self::drain_wallet pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self { - self.params.drain_to = Some(script_pubkey); + self.params.drain_to = DrainTo::Script(script_pubkey); + self + } + + pub fn drain_to_keychain(&mut self, keychain: K) -> &mut Self { + self.params.drain_to = DrainTo::Keychain(keychain); self } } -impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> { +impl<'a, K: Debug + Clone + Ord, Cs: CoinSelectionAlgorithm> TxBuilder<'a, K, Cs> { /// Finish building the transaction. /// /// Uses the thread-local random number generator (rng). @@ -683,7 +730,7 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> { /// **WARNING**: To avoid change address reuse you must persist the changes resulting from one /// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. #[cfg(feature = "std")] - pub fn finish(self) -> Result { + pub fn finish(self) -> Result> { self.finish_with_aux_rand(&mut bitcoin::key::rand::thread_rng()) } @@ -697,7 +744,7 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> { /// /// **WARNING**: To avoid change address reuse you must persist the changes resulting from one /// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. - pub fn finish_with_aux_rand(self, rng: &mut impl RngCore) -> Result { + pub fn finish_with_aux_rand(self, rng: &mut impl RngCore) -> Result> { self.wallet .borrow_mut() .create_tx(self.coin_selection, self.params, rng) @@ -856,25 +903,29 @@ impl RbfValue { } /// Policy regarding the use of change outputs when creating a transaction -#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] -pub enum ChangeSpendPolicy { - /// Use both change and non-change outputs (default) +#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone)] +pub enum SpendKeychainPolicy { + /// Allow spending all keychains. #[default] - ChangeAllowed, - /// Only use change outputs (see [`TxBuilder::only_spend_change`]) - OnlyChange, - /// Only use non-change outputs (see [`TxBuilder::do_not_spend_change`]) - ChangeForbidden, + AllowAll, + /// Only allow these keychains. + AllowOnly(BTreeSet), + /// Disallow these keychains, allow all other keychains. + Disallow(BTreeSet), } -impl ChangeSpendPolicy { - pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool { +impl SpendKeychainPolicy { + pub(crate) fn can_spend(&self, keychain: &K) -> bool { match self { - ChangeSpendPolicy::ChangeAllowed => true, - ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal, - ChangeSpendPolicy::ChangeForbidden => utxo.keychain == KeychainKind::External, + SpendKeychainPolicy::AllowAll => true, + SpendKeychainPolicy::AllowOnly(allowed) => allowed.contains(keychain), + SpendKeychainPolicy::Disallow(disallowed) => !disallowed.contains(keychain), } } + + pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool { + self.can_spend(&utxo.keychain) + } } #[cfg(test)] @@ -1074,7 +1125,7 @@ mod test { #[test] fn test_change_spend_policy_default() { - let change_spend_policy = ChangeSpendPolicy::default(); + let change_spend_policy = SpendKeychainPolicy::default(); let filtered = get_test_utxos() .into_iter() .filter(|u| change_spend_policy.is_satisfied_by(u)) @@ -1085,7 +1136,7 @@ mod test { #[test] fn test_change_spend_policy_no_internal() { - let change_spend_policy = ChangeSpendPolicy::ChangeForbidden; + let change_spend_policy = SpendKeychainPolicy::ChangeForbidden; let filtered = get_test_utxos() .into_iter() .filter(|u| change_spend_policy.is_satisfied_by(u)) @@ -1097,7 +1148,7 @@ mod test { #[test] fn test_change_spend_policy_only_internal() { - let change_spend_policy = ChangeSpendPolicy::OnlyChange; + let change_spend_policy = SpendKeychainPolicy::OnlyChange; let filtered = get_test_utxos() .into_iter() .filter(|u| change_spend_policy.is_satisfied_by(u)) diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 35413a962..18b0b92b4 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -23,10 +23,11 @@ const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:60002"; fn main() -> Result<(), anyhow::Error> { let db_path = "bdk-electrum-example.db"; - let mut db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; + let mut db = Store::>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; - let wallet_opt = Wallet::load() - .descriptors(EXTERNAL_DESC, INTERNAL_DESC) + let wallet_opt = Wallet::::load() + .descriptor(KeychainKind::External, EXTERNAL_DESC) + .descriptor(KeychainKind::Internal, INTERNAL_DESC) .network(NETWORK) .load_wallet(&mut db)?; let mut wallet = match wallet_opt { diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 4c4fe99ed..9f9b3bc78 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -19,20 +19,28 @@ const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7 const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net"; fn main() -> Result<(), anyhow::Error> { - let mut db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), DB_PATH)?; - - let wallet_opt = Wallet::load() - .descriptors(EXTERNAL_DESC, INTERNAL_DESC) + let mut db = Store::>::open_or_create_new( + DB_MAGIC.as_bytes(), + DB_PATH, + )?; + + let wallet_opt = Wallet::::load() + .descriptor(KeychainKind::External, EXTERNAL_DESC) + .descriptor(KeychainKind::Internal, INTERNAL_DESC) .network(NETWORK) .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, - None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) + None => Wallet::create() + .descriptor(KeychainKind::External, EXTERNAL_DESC) + .descriptor(KeychainKind::Internal, INTERNAL_DESC) .network(NETWORK) .create_wallet(&mut db)?, }; - let address = wallet.next_unused_address(KeychainKind::External); + let address = wallet + .next_unused_address(KeychainKind::External) + .expect("must contain external address"); wallet.persist(&mut db)?; println!( "Next unused address: ({}) {}",